diff --git a/.eslintrc b/.eslintrc index 72ffcd81..7e49a81d 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,6 +1,20 @@ { env: { node: true, + es6: true + }, + + plugins: [ + 'eslint-plugin-import' + ], + + parserOptions: { + ecmaVersion: 9, + sourceType: "module" + }, + + globals: { + Promise: true }, rules: { @@ -87,6 +101,8 @@ vars-on-top: 0, wrap-iife: [2, "inside"], yoda: 2, + no-var: "error", + prefer-arrow-callback: "error", // Strict Mode strict: [2, "never"], @@ -132,7 +148,7 @@ key-spacing: 0, lines-around-comment: 2, linebreak-style: 2, - max-nested-callbacks: [2, 2], + max-nested-callbacks: [2, 4], new-cap: 2, new-parens: 2, newline-after-var: 0, @@ -169,5 +185,18 @@ space-unary-ops: 2, spaced-comment: [2, "always", { block: { markers: ["!"] } }], wrap-regex: 0, + + // Imports + "import/no-extraneous-dependencies": "error" }, + + overrides: [ + { + // Specific rules for test files + files: [ "packages/**/test/**/*-test.js" ], + rules: { + "import/no-extraneous-dependencies": "off" + } + } + ] } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..54fda541 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,90 @@ +name: CI +on: [push, pull_request] +jobs: + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/cache@v2 + with: + path: '**/node_modules' + key: ${{ runner.os }}-lint-modules-${{ hashFiles('**/yarn.lock') }} + - uses: actions/setup-node@v2 + with: + node-version: 14.x + - run: yarn install + - run: yarn run lint + + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + node-version: + - 12.x + - 14.x + - 16.x + steps: + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + - name: Ensure line endings are consistent + run: git config --global core.autocrlf input + - name: Check out repository + uses: actions/checkout@v2 + - uses: actions/cache@v2 + with: + path: '**/node_modules' + key: ${{ runner.os }}-test-modules-${{ hashFiles('**/yarn.lock') }} + - name: Install dependencies + run: yarn install + - name: Run tests + run: yarn run test-ci + - name: Submit coverage results + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.github_token }} + flag-name: run-${{ matrix.node-version }} + parallel: true + + coveralls: + needs: test + runs-on: ubuntu-latest + steps: + - name: Consolidate test coverage from different jobs + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.github_token }} + parallel-finished: true + + docker: + needs: + - test + - lint + runs-on: ubuntu-latest + steps: + - name: Use Node.js + uses: actions/setup-node@v2 + with: + node-version: '14.x' + - name: Check out repository + uses: actions/checkout@v2 + - name: Load cache + uses: actions/cache@v2 + with: + path: '**/node_modules' + key: ${{ runner.os }}-docker-modules-v1-${{ hashFiles('**/yarn.lock') }} + - name: Install dependencies + run: yarn install --pure-lockfile + - name: Install Lerna Docker + run: sh -c "`curl -fsSl https://raw.githubusercontent.com/rubensworks/lerna-docker/master/install.sh`" + - name: Build Docker images + run: ~/.lerna-docker/bin/lerna-docker linkeddatafragments build + - name: Deploy Docker images + if: startsWith(github.ref, 'refs/heads/master') || startsWith(github.ref, 'refs/tags/') + run: ~/.lerna-docker/bin/lerna-docker linkeddatafragments push + env: + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} diff --git a/.gitignore b/.gitignore index 479cd0fe..08d70774 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,14 @@ data summaries *.log *.hdt.index* +.nyc_output -# Ignore deployment configuration files, but keep defaults (for npm package) +# Ignore deployment configuration files, but keep defaults and examples config.json config/*.json !config/config-defaults.json +!config/config-example*.json + +# Ignore dev environment files +.idea +.devcontainer \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 5f939d08..00000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,17 +0,0 @@ -image: node:latest - -before_script: - - npm install - -cache: - key: "$CI_COMMIT_REF_SLUG.$NODE_VERSION" - paths: - - node_modules/ - -test_all: - script: - - npm test - -test_lint: - script: - - npm run lint diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ad25e0ce..00000000 --- a/.travis.yml +++ /dev/null @@ -1,19 +0,0 @@ -language: node_js -node_js: - - "6" - - "8" - - "node" -script: - - npm run lint - - npm test -env: - - CXX=g++-4.8 -addons: - apt: - sources: - - ubuntu-toolchain-r-test - packages: - - g++-4.8 -cache: - directories: - - node_modules diff --git a/AUTHORS b/AUTHORS index 253f5390..ec4a146d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,2 +1,3 @@ Ruben Verborgh (http://ruben.verborgh.org/) Miel Vander Sande +Ruben Taelman diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..585f0aaa --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,111 @@ +# Changelog +All notable changes to this project will be documented in this file. + + +## [v3.3.0](https://github.com/LinkedDataFragments/Server.js/compare/v3.2.1...v3.3.0) - 2021-08-27 + +### Added +* [Add RDFa datasource (#142)](https://github.com/LinkedDataFragments/Server.js/commit/58c64c03539ed5ab25080b6a352e70019e4053ff) + + +## [v3.2.1](https://github.com/LinkedDataFragments/Server.js/compare/v3.2.0...v3.2.1) - 2021-05-10 + +### Fixed +* [Fix illegal mutation of quads, Closes #137](https://github.com/LinkedDataFragments/Server.js/commit/a459b74c6411bc9d042a0e7d4caca079a349fe7f) + + +## [v3.2.0](https://github.com/LinkedDataFragments/Server.js/compare/v3.1.0...v3.2.0) - 2021-01-15 + +### Changed +* [Update to Components.js 4](https://github.com/LinkedDataFragments/Server.js/commit/e90db5c3e439259466fa0cf789e2c4b028b584f2) + + +## [v3.1.0](https://github.com/LinkedDataFragments/Server.js/compare/v3.0.9...v3.1.0) - 2020-08-10 + +### Changed +* [Update to asynciterator v3, Closes #129](https://github.com/LinkedDataFragments/Server.js/commit/b6e3512ec21bba5cfcac79aee52033034d49583b) + +### Fixed +* [Fix missing rdf-string dependency in core, Closes #131](https://github.com/LinkedDataFragments/Server.js/commit/70021d26a4112a02278801ffff604ee803114369) + + +## [v3.0.9](https://github.com/LinkedDataFragments/Server.js/compare/v3.0.8...v3.0.9) - 2020-06-18 + +### Changed +* [Update to HDT-Node 3.x.x](https://github.com/LinkedDataFragments/Server.js/commit/7ae079452f510f13631770282345688828dcc085) + + +## [v3.0.8](https://github.com/LinkedDataFragments/Server.js/compare/v3.0.7...v3.0.8) - 2020-05-11 + +### Fixed +* [Fix crash due to changed function name in N3.js](https://github.com/LinkedDataFragments/Server.js/commit/ea3e7632cfd47e0ae9015f4715a15d40c61e87c8) + + +## [v3.0.7](https://github.com/LinkedDataFragments/Server.js/compare/v3.0.6...v3.0.7) - 2020-04-14 + +### Fixed +* [Fix blank node prefixes in datasources not being relative to base](https://github.com/LinkedDataFragments/Server.js/commit/cf8ee9e9586a0643fde5619b93098a11035b7c78) + + +## [v3.0.6](https://github.com/LinkedDataFragments/Server.js/compare/v3.0.5...v3.0.6) - 2020-04-10 + +### Fixed +* [Fix datasource descriptions being migrated incorrectly](https://github.com/LinkedDataFragments/Server.js/commit/8a54d95c5b67ad09ab626a678388995df366db02) + + +## [v3.0.5](https://github.com/LinkedDataFragments/Server.js/compare/v3.0.4...v3.0.5) - 2020-04-10 + +### Fixed +* [Fix enabled option on datasources being ignored](https://github.com/LinkedDataFragments/Server.js/commit/bf09c363e7c3094e7668b3dc18051bc0489f3ca2) +* [Fix HDT source configs not being migrated correctly](https://github.com/LinkedDataFragments/Server.js/commit/bd52749d513732ee74be44af8012cebb6beff02e) + + +## [v3.0.4](https://github.com/LinkedDataFragments/Server.js/compare/v3.0.3...v3.0.4) - 2020-04-10 + +### Fixed +* [Remove need for softlinks in N3 datasources](https://github.com/LinkedDataFragments/Server.js/commit/2e260c560e0c3a75fedf1817690dd7a7dd2d6cca) +* [Fix hide and enabled config entries not being migrated properly](https://github.com/LinkedDataFragments/Server.js/commit/b298789619fac911db89f1f00b736652c1bdc0cb) + + +## [v3.0.3](https://github.com/LinkedDataFragments/Server.js/compare/v3.0.2...v3.0.3) - 2020-04-10 + +### Fixed +* [Fix migration tool failing to include datasource metadata](https://github.com/LinkedDataFragments/Server.js/commit/9ba4c2f077f8800c1a5f2b06935641c3b1ada513) + + +## [v3.0.2](https://github.com/LinkedDataFragments/Server.js/compare/v3.0.1...v3.0.2) - 2020-04-10 + +### Fixed +* [Fix migration tool handling relative paths incorrectly](https://github.com/LinkedDataFragments/Server.js/commit/34c0215d294f4cd8d3a8de1658decbe915ce63aa) + + +## [v3.0.1](https://github.com/LinkedDataFragments/Server.js/compare/v3.0.0...v3.0.1) - 2020-04-10 + +### Changed +* [Disable QPF support for datasources in migration tool](https://github.com/LinkedDataFragments/Server.js/commit/747b6adf1e049b25889f1e4c5b40c239cd528fdc) + +### Fixed +* [Fix sources querying for all graphs even if QPF is disabled](https://github.com/LinkedDataFragments/Server.js/commit/45fd2ca63283ec51c8cf93147a755de254c1f579) +* [Fix incorrect visualization of pattern string in HTML](https://github.com/LinkedDataFragments/Server.js/commit/516f009c191f845eb7afa057fcfb6b20362f9e65) + + +## [v3.0.0](https://github.com/LinkedDataFragments/Server.js/compare/v2.2.5...v3.0.0) - 2020-04-08 + +### Added +* [Implement Quad Pattern Fragments](https://github.com/LinkedDataFragments/Server.js/commit/f2547b18aaac74b1b3aa01ebfe46ea519d9d098f) +* [Bump JSON-LD dependencies to add support for JSON-LD 1.1](https://github.com/LinkedDataFragments/Server.js/commit/2f365156d24b55277e941ca3a0f78e9796757056) +* [Add config migration tool](https://github.com/LinkedDataFragments/Server.js/commit/be6a7ec54417b9cae21e5c5093a90ab5a4f4d255) +* [Add SparqlDatasource option to force typed literals](https://github.com/LinkedDataFragments/Server.js/commit/b38026fd32bc4034c4aca36dac1c7f024d175b29) + +### Changed +* [Modularize all packages through dependency injection](https://github.com/LinkedDataFragments/Server.js/commit/8671d98fa29672921a654cda1c1d4f19e076b883) +* [Refactor codebase to use RDFJS terms instead of strings](https://github.com/LinkedDataFragments/Server.js/commit/b014f67c55550260dc3f0032594c759bf570e983) +* [Set minimum Node version to 10](https://github.com/LinkedDataFragments/Server.js/commit/4dd1728b978e27370f91fa065d7fc06d46ed78a1) + +### Fixed +* [Fix literals from Virtuoso endpoints being seen as URIs, Closes #94](https://github.com/LinkedDataFragments/Server.js/commit/cba41cdce43deea9e4106b5976608769db879cfb) + + +## [v2.2.5] - 2010-01-015 + +_Start tracking changelog_ diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 2c9856dd..00000000 --- a/Dockerfile +++ /dev/null @@ -1,25 +0,0 @@ -FROM node:8-slim - -# Install location -ENV dir /var/www/ldf-server - -# Copy the server files -ADD . ${dir} - -# Install the node module -RUN apt-get update && \ - apt-get install -y g++ make python && \ - cd ${dir} && npm install && \ - apt-get remove -y g++ make python && apt-get autoremove -y && \ - rm -rf /var/cache/apt/archives - -# Expose the default port -EXPOSE 3000 - -# Run base binary -WORKDIR ${dir} -ENTRYPOINT ["node", "bin/ldf-server"] - -# Default command -CMD ["--help"] - diff --git a/LICENSE.txt b/LICENSE.txt index 3746cfa3..d0fdb948 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright © 2013–2016 Ruben Verborgh, Miel Vander Sande +Copyright © 2013–now Ruben Verborgh, Miel Vander Sande, Ruben Taelman Ghent University – imec, Belgium Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/README.md b/README.md index 25938549..3eef3140 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,17 @@ # Linked Data Fragments Server -[![Build Status](https://travis-ci.org/LinkedDataFragments/Server.js.svg?branch=master)](https://travis-ci.org/LinkedDataFragments/Server.js) -[![npm version](https://badge.fury.io/js/ldf-server.svg)](https://www.npmjs.com/package/ldf-server) -[![Docker Automated Build](https://img.shields.io/docker/automated/linkeddatafragments/server.js.svg)](https://hub.docker.com/r/linkeddatafragments/server.js/) +[![Build status](https://github.com/LinkedDataFragments/Server.js/workflows/CI/badge.svg)](https://github.com/LinkedDataFragments/Server.js/actions?query=workflow%3ACI) +[![Coverage Status](https://coveralls.io/repos/github/LinkedDataFragments/Server.js/badge.svg?branch=master)](https://coveralls.io/github/LinkedDataFragments/Server.js?branch=master) +[![npm version](https://badge.fury.io/js/%40ldf%2Fserver.svg)](https://www.npmjs.com/package/@ldf/server) [![DOI](https://zenodo.org/badge/16891600.svg)](https://zenodo.org/badge/latestdoi/16891600) +This repository contains modules for [Linked Data Fragments (LDF)](https://linkeddatafragments.org/) servers. + +_Find more information about migrating from `ldf-server` 2.x.x [on our wiki](https://github.com/LinkedDataFragments/Server.js/wiki/Release-3.0.0)._ + +## Motivation + On today's Web, Linked Data is published in different ways, which include [data dumps](http://downloads.dbpedia.org/3.9/en/), [subject pages](http://dbpedia.org/page/Linked_data), @@ -17,10 +23,11 @@ is that they are either so powerful that their servers suffer from low availabil ([as is the case with SPARQL](http://sw.deri.org/~aidanh/docs/epmonitorISWC.pdf)), or either don't allow efficient querying. -Instead, this server offers **[Triple Pattern Fragments](http://www.hydra-cg.com/spec/latest/triple-pattern-fragments/)**. -Each Triple Pattern Fragment offers: +Instead, this server offers [Quad Pattern Fragments](https://linkeddatafragments.org/specification/quad-pattern-fragments/) +(a.k.a. **[Triple Pattern Fragments](https://linkeddatafragments.org/specification/triple-pattern-fragments/)**). +Each Quad Pattern Fragment offers: -- **data** that corresponds to a _triple pattern_ +- **data** that corresponds to a _quad/triple pattern_ _([example](http://data.linkeddatafragments.org/dbpedia?subject=&predicate=rdf%3Atype&object=dbpedia-owl%3ARestaurant))_. - **metadata** that consists of the (approximate) total triple count _([example](http://data.linkeddatafragments.org/dbpedia?subject=&predicate=rdf%3Atype&object=))_. @@ -29,19 +36,20 @@ Each Triple Pattern Fragment offers: An example server is available at [data.linkeddatafragments.org](http://data.linkeddatafragments.org/). +## Quick Start + +The easiest way to start using this server is via +[`@ldf/server`](https://github.com/LinkedDataFragments/Server.js/tree/master/packages/server). _(previously known as `ldf-server`)_ -## Install the server +### Install the server -This server requires [Node.js](http://nodejs.org/) 4.0 or higher +This server requires [Node.js](http://nodejs.org/) 10.0 or higher and is tested on OSX and Linux. To install, execute: ```bash -$ [sudo] npm install -g ldf-server +$ [sudo] npm install -g @ldf/server ``` - -## Use the server - ### Configure the data sources First, create a configuration file `config.json` similar to `config/config-example.json`, @@ -50,32 +58,34 @@ For example, this configuration uses an [HDT file](http://www.rdfhdt.org/) and a [SPARQL endpoint](http://www.w3.org/TR/sparql11-protocol/) as sources: ```json { + "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@ldf/server/^3.0.0/components/context.jsonld", + "@id": "urn:ldf-server:my", + "import": "preset-qpf:config-defaults.json", + "title": "My Linked Data Fragments server", - "datasources": { - "dbpedia": { - "title": "DBpedia 2014", - "type": "HdtDatasource", + + "datasources": [ + { + "@id": "ex:myHdtDatasource", + "@type": "HdtDatasource", + "datasourceTitle": "DBpedia 2014", "description": "DBpedia 2014 with an HDT back-end", - "settings": { "file": "data/dbpedia2014.hdt" } + "datasourcePath": "dbpedia", + "hdtFile": "data/dbpedia2014.hdt" }, - "dbpedia-sparql": { - "title": "DBpedia 3.9 (Virtuoso)", - "type": "SparqlDatasource", - "description": "DBpedia 3.9 with a Virtuoso back-end", - "settings": { "endpoint": "http://dbpedia.restdesc.org/", "defaultGraph": "http://dbpedia.org" } + { + "@id": "ex:mySparqlDatasource", + "@type": "SparqlDatasource", + "datasourceTitle": "DBpedia (Virtuoso)", + "description": "DBpedia with a Virtuoso back-end", + "datasourcePath": "dbpedia-sparql", + "sparqlEndpoint": "https://dbpedia.org/sparql" } - } + ] } ``` -The following sources are supported out of the box: -- HDT files ([`HdtDatasource`](https://github.com/LinkedDataFragments/Server.js/blob/master/lib/datasources/HdtDatasource.js) with `file` setting) -- N-Triples documents ([`TurtleDatasource`](https://github.com/LinkedDataFragments/Server.js/blob/master/lib/datasources/TurtleDatasource.js) with `url` setting) -- Turtle documents ([`TurtleDatasource`](https://github.com/LinkedDataFragments/Server.js/blob/master/lib/datasources/TurtleDatasource.js) with `url` setting) -- JSON-LD documents ([`JsonLdDatasource`](https://github.com/LinkedDataFragments/Server.js/blob/master/lib/datasources/JsonLdDatasource.js) with `url` setting) -- SPARQL endpoints ([`SparqlDatasource`](https://github.com/LinkedDataFragments/Server.js/blob/master/lib/datasources/SparqlDatasource.js) with `endpoint` and optionally `defaultGraph` settings) - -Support for new sources is possible by implementing the [`Datasource`](https://github.com/LinkedDataFragments/Server.js/blob/master/lib/datasources/Datasource.js) interface. +_More details on how to configure this server can be found in the README of [`@ldf/server`](https://github.com/LinkedDataFragments/Server.js/tree/master/packages/server)._ ### Start the server @@ -88,120 +98,61 @@ and `4` the number of worker processes. Now visit `http://localhost:5000/` in your browser. -### Reload running server - -You can reload the server without any downtime -in order to load a new configuration or version. -
-In order to do this, you need the process ID of the server master process. -
-One possibility to obtain this are the server logs: -```bash -$ bin/ldf-server config.json -Master 28106 running. -Worker 28107 running on http://localhost:3000/. -``` - -If you send the server a `SIGHUP` signal: -```bash -$ kill -s SIGHUP 28106 -``` -it will reload by replacing its workers. - -Note that crashed or killed workers are always replaced automatically. +## Configure your own server -### _(Optional)_ Set up a reverse proxy +This repository should be used by LDF Server module **developers** as it contains multiple LDF Server modules that can be composed. +We manage this repository as a [monorepo](https://github.com/babel/babel/blob/master/doc/design/monorepo.md) +using [Lerna](https://lerna.js.org/). -A typical Linked Data Fragments server will be exposed -on a public domain or subdomain along with other applications. -Therefore, you need to configure the server to run behind an HTTP reverse proxy. -
-To set this up, configure the server's public URL in your server's `config.json`: -```json -{ - "title": "My Linked Data Fragments server", - "baseURL": "http://data.example.org/", - "datasources": { … } -} -``` -Then configure your reverse proxy to pass requests to your server. -Here's an example for [nginx](http://nginx.org/): -```nginx -server { - server_name data.example.org; - - location / { - proxy_pass http://127.0.0.1:3000$request_uri; - proxy_set_header Host $http_host; - proxy_pass_header Server; - } -} -``` -Change the value `3000` into the port on which your Linked Data Fragments server runs. - -If you would like to proxy the data in a subfolder such as `http://example.org/my/data`, -modify the `baseURL` in your `config.json` to `"http://example.org/my/data"` -and change `location` from `/` to `/my/data` (excluding a trailing slash). +The following modules are available: +* [`@ldf/core`](https://github.com/LinkedDataFragments/Server.js/tree/master/packages/core): Shared functionality for LDF servers. +* [`@ldf/server`](https://github.com/LinkedDataFragments/Server.js/tree/master/packages/server): An LDF server with Quad/Triple Pattern Fragments support. +* [`@ldf/preset-qpf`](https://github.com/LinkedDataFragments/Server.js/tree/master/packages/preset-qpf): Configuration presets for Quad/Triple Pattern Fragments servers. +* [`@ldf/feature-qpf`](https://github.com/LinkedDataFragments/Server.js/tree/master/packages/feature-qpf): Feature that enables [Quad Pattern Fragments](https://linkeddatafragments.org/specification/quad-pattern-fragments/) (a.k.a. [Triple Pattern Fragments](https://linkeddatafragments.org/specification/triple-pattern-fragments/)). +* [`@ldf/feature-summary`](https://github.com/LinkedDataFragments/Server.js/tree/master/packages/feature-summary): Feature that adds summaries to datasources. +* [`@ldf/feature-memento`](https://github.com/LinkedDataFragments/Server.js/tree/master/packages/feature-memento): Feature that enables datetime negotiation using the [Memento Protocol](http://mementoweb.org/about/). +* [`@ldf/feature-webid`](https://github.com/LinkedDataFragments/Server.js/tree/master/packages/feature-webid): Feature that enables authenticated requests from clients with WebID. +* [`@ldf/datasource-hdt`](https://github.com/LinkedDataFragments/Server.js/tree/master/packages/datasource-hdt): Datasource that allows HDT files to be loaded. +* [`@ldf/datasource-jsonld`](https://github.com/LinkedDataFragments/Server.js/tree/master/packages/datasource-jsonld): Datasource that allows JSON-LD files to be loaded. +* [`@ldf/datasource-rdfa`](https://github.com/LinkedDataFragments/Server.js/tree/master/packages/datasource-rdfa): Datasource that allows RDFa files to be loaded. +* [`@ldf/datasource-n3`](https://github.com/LinkedDataFragments/Server.js/tree/master/packages/datasource-n3): Datasource that allows [N-Quads](https://www.w3.org/TR/n-quads/), [N-Triples](https://www.w3.org/TR/n-triples/), [Trig](https://www.w3.org/TR/trig/) and [Turtle](https://www.w3.org/TR/turtle/) files to be loaded. +* [`@ldf/datasource-sparql`](https://github.com/LinkedDataFragments/Server.js/tree/master/packages/datasource-sparql): Datasource that allows SPARQL endpoints to be used as a data proxy. +* [`@ldf/datasource-composite`](https://github.com/LinkedDataFragments/Server.js/tree/master/packages/datasource-composite): Datasource that delegates queries to an sequence of other datasources. -### _(Optional)_ Running under HTTPS +These modules can be used to configure your own LDF server with the features you want. +As an example on how to make such a server, +you can have a look at [`@ldf/server`](https://github.com/LinkedDataFragments/Server.js/tree/master/packages/server), +which is a default server configuration that has all possible features enabled. -HTTPS can be enabled in two ways: natively by the server, or through a proxy (explained above). +## Development Setup -With native HTTPS, the server will establish the SSL layer. Set the following values in your config file to enable this: +If you want to develop new features +or use the (potentially unstable) in-development version, +you can set up a development environment for this server. - { - "protocol": "https", - "ssl": { - "keys" : { - "key": "./private-key-server.key.pem", - "ca": ["./root-ca.crt.pem"], - "cert": "./server-certificate.crt.pem" - } - } - } - - If `protocol`is not specified, it will derive the protocol from the `baseURL`. Hence, HTTPS can also be enabled as such: +LDF Server requires [Node.JS](http://nodejs.org/) 10.0 or higher and the [Yarn](https://yarnpkg.com/en/) package manager. +LDF Server is tested on OSX, Linux and Windows. - { - "baseURL": "https://data.example.org/", - "ssl": { - "keys" : { - "key": "./private-key-server.key.pem", - "ca": ["./root-ca.crt.pem"], - "cert": "./server-certificate.crt.pem" - } - } - } +This project can be setup by cloning and installing it as follows: -If you decide to let a proxy handle HTTPS, use this configuration to run the server as `http`, but construct links as `https` (so clients don't break): - - { - "protocol": "http", - "baseURL": "https://data.example.org/", - } - - -### _(Optional)_ Running in a Docker container - -If you want to rapidly deploy the server as a microservice, you can build a [Docker](https://www.docker.com/) container as follows: - -```bash -$ docker build -t ldf-server . -``` -After that, you can run your newly created container: ```bash -$ docker run -p 3000:3000 -t -i --rm -v $(pwd)/config.json:/tmp/config.json ldf-server /tmp/config.json +$ git clone https://github.com/LinkedDataFragments/Server.js.git +$ cd Server.js +$ yarn install ``` -### _(Optional)_ Host historical version of datasets +**Note: `npm install` is not supported at the moment, as this project makes use of Yarn's [workspaces](https://yarnpkg.com/lang/en/docs/workspaces/) functionality** -You can [enable the Memento protocol](https://github.com/LinkedDataFragments/Server.js/wiki/Configuring-Memento) to offer different versions of an evolving dataset. +This will install the dependencies of all modules, and bootstrap the Lerna monorepo. +After that, all [LDF Server packages](https://github.com/LinkedDataFragments/Server.js/tree/master/packages) are available in the `packages/` folder +and can be used in a development environment. -## License -The Linked Data Fragments server is written by [Ruben Verborgh](http://ruben.verborgh.org/). +Furthermore, this will add [pre-commit hooks](https://www.npmjs.com/package/pre-commit) +to build, lint and test. +These hooks can temporarily be disabled at your own risk by adding the `-n` flag to the commit command. ## License -The Linked Data Fragments client is written by [Ruben Verborgh](http://ruben.verborgh.org/) and colleagues. +The Linked Data Fragments server is written by [Ruben Verborgh](https://ruben.verborgh.org/), Miel Vander Sande, [Ruben Taelman](https://www.rubensworks.net/) and colleagues. This code is copyrighted by [Ghent University – imec](http://idlab.ugent.be/) and released under the [MIT license](http://opensource.org/licenses/MIT). diff --git a/bin/.eslintrc b/bin/.eslintrc deleted file mode 100644 index dddcf5c7..00000000 --- a/bin/.eslintrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - rules: { - no-console: 0, - max-nested-callbacks: 0, - }, -} diff --git a/bin/ldf-server b/bin/ldf-server deleted file mode 100755 index 2d828106..00000000 --- a/bin/ldf-server +++ /dev/null @@ -1,224 +0,0 @@ -#!/usr/bin/env node -/*! @license MIT ©2013-2016 Ruben Verborgh, Ghent University - imec */ -/* Standalone Linked Data Fragments Server */ - -var _ = require('lodash'), - fs = require('fs'), - path = require('path'), - cluster = require('cluster'), - LinkedDataFragmentsServer = require('../lib/LinkedDataFragmentsServer'), - IndexDatasource = require('../lib/datasources/IndexDatasource'), - ViewCollection = require('../lib/views/ViewCollection.js'); - -// Parse arguments -var args = process.argv.slice(2); -if (args.length < 1 || args.length > 3 || /^--?h(elp)?$/.test(args[0])) { - console.log('usage: server config.json [port [workers]]'); - return process.exit(1); -} -var configDefaults = JSON.parse(fs.readFileSync(path.join(__dirname, '../config/config-defaults.json'))), - config = _.defaults(JSON.parse(fs.readFileSync(args[0])), configDefaults), - port = parseInt(args[1], 10) || config.port, - workers = parseInt(args[2], 10) || config.workers, - protocol = config.protocol, - constructors = {}; - -// Determine protocol -if (!protocol) { - var protocolMatch = (config.baseURL || '').match(/^(\w+):/); - protocol = config.protocol = protocolMatch ? protocolMatch[1] : 'http'; -} - -// Start up a cluster master -if (cluster.isMaster) { - // Create workers - console.log('Master %d running on %s://localhost:%d/.', process.pid, protocol, port); - for (var i = 0; i < workers; i++) - cluster.fork(); - - // Respawn crashed workers - cluster.on('listening', function (worker) { - worker.once('exit', function (code, signal) { - if (!worker.suicide) { - console.log('Worker %d died with %s. Starting new worker.', - worker.process.pid, code || signal); - cluster.fork(); - } - }); - }); - - // Respawn workers one by one when receiving a SIGHUP signal - process.on('SIGHUP', function respawn() { - console.log('Respawning workers of master %d.', process.pid); - process.addListener('SIGHUP', respawnPending); - process.removeListener('SIGHUP', respawn); - - // Retrieve a list of old workers that will be replaced by new ones - var workers = Object.keys(cluster.workers).map(function (id) { return cluster.workers[id]; }); - (function respawnNext() { - // If there are still old workers, respawn a new one - if (workers.length) { - // Wait until the new worker starts listening to kill the old one - var newWorker = cluster.fork(); - newWorker.once('listening', function () { - var worker = workers.pop(); - if (!worker) - return newWorker.kill(), respawnNext(); // Dead workers are replaced automatically - worker.once('exit', function () { - console.log('Worker %d replaces killed worker %d.', - newWorker.process.pid, worker.process.pid); - respawnNext(); - }); - worker.kill(); - newWorker.removeListener('exit', abort); - }); - // Abort the respawning process if creating a new worker fails - newWorker.on('exit', abort); - function abort(code, signal) { - if (!newWorker.suicide) { - console.log('Respawning aborted because worker %d died with %s.', - newWorker.process.pid, code || signal); - process.addListener('SIGHUP', respawn); - process.removeListener('SIGHUP', respawnPending); - } - } - } - // No old workers left, so respawning has finished - else { - process.addListener('SIGHUP', respawn); - process.removeListener('SIGHUP', respawnPending); - console.log('Respawned all workers of master %d.', process.pid); - } - })(); - function respawnPending() { console.log('Respawning already in progress'); } - }); -} -// Start up a worker -else { - // Configure preset URLs - var baseURL = config.baseURL = config.baseURL.replace(/\/?$/, '/'), - baseURLRoot = baseURL.match(/^(?:https?:\/\/[^\/]+)?/)[0], - baseURLPath = baseURL.substr(baseURLRoot.length), - blankNodePath = baseURLRoot ? '/.well-known/genid/' : '', - blankNodePrefix = blankNodePath ? baseURLRoot + blankNodePath : 'genid:'; - - // Create all data sources - var datasources = config.datasources, datasourceBase = baseURLPath.substr(1), dereference = config.dereference; - Object.keys(datasources).forEach(function (datasourceName) { - var datasourceConfig = config.datasources[datasourceName], datasourcePath; - delete datasources[datasourceName]; - if (datasourceConfig.enabled !== false) { - try { - // Avoid illegal URI characters in data source path - datasourcePath = datasourceBase + encodeURI(datasourceName); - datasources[datasourcePath] = datasourceConfig; - // Set up blank-node-to-IRI translation, with dereferenceable URLs when possible - datasourceConfig.settings = _.defaults(datasourceConfig.settings || {}, config); - if (!datasourceConfig.settings.blankNodePrefix) { - datasourceConfig.settings.blankNodePrefix = blankNodePrefix + datasourcePath + '/'; - if (blankNodePath) - dereference[blankNodePath + datasourcePath + '/'] = datasourcePath; - } - // Create the data source - var datasource = instantiate(datasourceConfig, '../lib/datasources/'); - datasource.on('error', datasourceError); - datasourceConfig.datasource = datasource; - datasourceConfig.url = baseURLRoot + '/' + datasourcePath + '#dataset'; - datasourceConfig.title = datasourceConfig.title || datasourceName; - } - catch (error) { datasourceError(error); } - function datasourceError(error) { - delete datasources[datasourcePath]; - process.stderr.write('WARNING: skipped datasource ' + datasourceName + '. ' + error.message + '\n'); - } - } - }); - - // Create index data source - var indexPath = datasourceBase.replace(/\/$/, ''); - datasources[indexPath] = datasources[indexPath] || { - url: baseURLRoot + '/' + indexPath + '#dataset', - role: 'index', - title: 'dataset index', - datasource: new IndexDatasource({ datasources: datasources }), - }; - - // Set up assets - config.assetsPath = baseURLPath + 'assets/'; - - // Set up routers, views, and controllers - config.routers = instantiateAll(config.routers, '../lib/routers/'); - config.views = new ViewCollection(); - config.views.addViews(instantiateAll(findFiles('../lib/views', /\.js$/))); - config.controllers = instantiateAll(config.controllers, '../lib/controllers/'); - - // Set up logging - var loggingSettings = _.defaults(config.logging, configDefaults.logging); - config.log = console.log; - if (loggingSettings.enabled) { - var accesslog = require('access-log'); - config.accesslogger = function (request, response) { - accesslog(request, response, loggingSettings.format, function (logEntry) { - if (loggingSettings.file) { - fs.appendFile(loggingSettings.file, logEntry + '\n', function (error) { - error && process.stderr.write('Error when writing to access log file: ' + error); - }); - } - else console.log(logEntry); - }); - }; - } - - // Create server, and start it when all data sources are ready - var server = new LinkedDataFragmentsServer(config), - pending = _.size(datasources); - _.each(datasources, function (settings) { - var ready = _.once(startWhenReady); - settings.datasource.once('initialized', ready); - settings.datasource.once('error', ready); - }); - function startWhenReady() { - if (!--pending) { - server.listen(port); - console.log('Worker %d running on %s://localhost:%d/.', process.pid, protocol, port); - } - } - - // Terminate gracefully if possible - process.once('SIGINT', function () { - console.log('Stopping worker', process.pid); - server.stop(); - process.on('SIGINT', function () { process.exit(1); }); - }); -} - - -// Instantiates an object from the given description -function instantiate(description, includePath) { - var type = description.type || description, - typePath = path.join(includePath ? path.resolve(__dirname, includePath) : '', type), - Constructor = constructors[typePath] || (constructors[typePath] = require(typePath)), - extensions = config.extensions && config.extensions[type] || [], - settings = _.defaults(description.settings || {}, { - extensions: extensions.map(function (x) { return instantiate(x, includePath); }), - }, config); - return new Constructor(settings, config); -} - -// Instantiates all objects from the given descriptions -function instantiateAll(descriptions, includePath) { - return (_.isArray(descriptions) ? _.map : _.mapValues)(descriptions, - function (description) { return instantiate(description, includePath); }); -} - -// Recursively finds files in a folder whose name matches the pattern -function findFiles(folder, pattern, includeCurrentFolder) { - folder = path.resolve(__dirname, folder); - return _.flatten(_.compact(fs.readdirSync(folder).map(function (name) { - name = path.join(folder, name); - if (fs.statSync(name).isDirectory()) - return findFiles(name, pattern, true); - else if (includeCurrentFolder && pattern.test(name)) - return name; - }))); -} diff --git a/config/config-composite.json b/config/config-composite.json deleted file mode 100644 index e581237d..00000000 --- a/config/config-composite.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "title": "My Composite Linked Data Fragments server", - - "datasources": { - "test-composite": { - "title": "Composite Test", - "type": "CompositeDatasource", - "description": "A test composite datasource", - "settings": { - "references": [ "hdt", "ttl", "jsonld", "hdtext" ] - } - }, - "hdt": { - "hide": true, - "title": "HDT", - "type": "HdtDatasource", - "description": "A test HDT datasource", - "settings": { "file": "test/assets/test.hdt" } - }, - "ttl": { - "hide": true, - "title": "Turtle", - "type": "TurtleDatasource", - "description": "A test turtle datasource", - "settings": { "file": "test/assets/test.ttl" } - }, - "jsonld": { - "hide": true, - "title": "JSONLD", - "type": "JsonLdDatasource", - "description": "A test jsonld datasource", - "settings": { "file": "test/assets/test.jsonld" } - }, - "hdtext": { - "hide": true, - "title": "HDT-EXT", - "type": "ExternalHdtDatasource", - "description": "A blank test HDT datasource", - "settings": { "file": "test/assets/test-blank.hdt" } - } - } -} diff --git a/config/config-defaults.json b/config/config-defaults.json deleted file mode 100644 index 6b14d9dc..00000000 --- a/config/config-defaults.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "title": "Linked Data Fragments server", - "baseURL": "/", - - "port": 3000, - "workers": 1, - - "datasources": {}, - - "dereference": {}, - - "prefixes": { - "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", - "rdfs": "http://www.w3.org/2000/01/rdf-schema#", - "owl": "http://www.w3.org/2002/07/owl#", - "xsd": "http://www.w3.org/2001/XMLSchema#", - "hydra": "http://www.w3.org/ns/hydra/core#", - "void": "http://rdfs.org/ns/void#" - }, - - "routers": [ - "DatasourceRouter", - "TriplePatternRouter", - "PageRouter" - ], - - "controllers": [ - "SummaryController", - "TimegateController", - "TriplePatternFragmentsController", - "AssetsController", - "DereferenceController", - "NotFoundController" - ], - - "response": { - "headers": { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Headers": "Accept-Datetime,Accept", - "Access-Control-Expose-Headers": "Content-Location,Link,Memento-Datetime" - } - }, - - "extensions": { - "TriplePatternFragmentsController": [ - "MementoControllerExtension" - ] - }, - - "ssl": { - "keys" : { - "key": "config/certs/localhost-server.key", - "cert": "config/certs/localhost-server.crt", - "ca": [ "config/certs/localhost-ca.crt" ] - } - }, - - "logging": { - "enabled": false, - "file": "access.log", - "format": null - } -} diff --git a/config/config-example-advanced.json b/config/config-example-advanced.json deleted file mode 100644 index af77151e..00000000 --- a/config/config-example-advanced.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "title": "My Linked Data Fragments server", - "baseURL": "http://example.org/", - - "datasources": { - "dbpedia": { - "title": "DBpedia 2014", - "description": "DBpedia 2014 with an HDT back-end", - "license": "Creative Commons Attribution-ShareAlike 3.0", - "licenseUrl": "https://creativecommons.org/licenses/by-sa/3.0/", - "copyright": "The DBpedia dataset is Open Knowledge.", - "homepage": "http://dbpedia.org/", - "type": "HdtDatasource", - "settings": { "file": "data/dbpedia2014.hdt" } - }, - "dbpedia-sparql": { - "title": "DBpedia 3.9 (Virtuoso)", - "type": "SparqlDatasource", - "description": "DBpedia 3.9 with a Virtuoso back-end", - "settings": { "endpoint": "http://dbpedia.restdesc.org/", "defaultGraph": "http://dbpedia.org" } - } - }, - - "dereference": { - "/resource/": "dbpedia" - }, - - "routers": [ - { "type": "DatasourceRouter" }, - { "type": "TriplePatternRouter" }, - { "type": "PageRouter", "settings": { "pageSize": 100 } } - ], - - "prefixes": { - "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", - "rdfs": "http://www.w3.org/2000/01/rdf-schema#", - "owl": "http://www.w3.org/2002/07/owl#", - "skos": "http://www.w3.org/2004/02/skos/core#", - "xsd": "http://www.w3.org/2001/XMLSchema#", - "dc": "http://purl.org/dc/terms/", - "dcterms": "http://purl.org/dc/terms/", - "dc11": "http://purl.org/dc/elements/1.1/", - "foaf": "http://xmlns.com/foaf/0.1/", - "geo": "http://www.w3.org/2003/01/geo/wgs84_pos#", - "dbpedia": "http://dbpedia.org/resource/", - "dbpedia-owl": "http://dbpedia.org/ontology/", - "dbpprop": "http://dbpedia.org/property/", - "hydra": "http://www.w3.org/ns/hydra/core#", - "void": "http://rdfs.org/ns/void#" - }, - - "logging": { - "enabled": true, - "file": "access.log" - } -} diff --git a/config/config-example-memento.json b/config/config-example-memento.json deleted file mode 100644 index 626bc41b..00000000 --- a/config/config-example-memento.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "title": "Memento-aware server", - - "datasources": { - "dbpedia2015": { - "title": "DBpedia 2015", - "type": "HdtDatasource", - "settings": { - "file": "data/dbpedia2015en.hdt" - }, - "memento": { - "interval": ["2014-09-14T11:59:59Z", "2015-04-15T00:00:00Z"] - } - }, - "dbpedia2014": { - "title": "DBpedia 2014", - "type": "HdtDatasource", - "settings": { - "file": "data/dbpedia2014en.hdt" - }, - "memento": { - "interval": ["2013-06-15T11:59:59Z", "2014-09-15T00:00:00Z"] - } - } - }, - - "timegates": { - "baseURL": "/timegate/", - "mementos": { - "dbpedia": { - "versions": [ - "dbpedia2015", - "dbpedia2014" - ] - } - } - }, - - "prefixes": { - "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", - "rdfs": "http://www.w3.org/2000/01/rdf-schema#", - "xsd": "http://www.w3.org/2001/XMLSchema#", - "dc": "http://purl.org/dc/terms/", - "foaf": "http://xmlns.com/foaf/0.1/", - "dbpedia": "http://dbpedia.org/resource/", - "dbpedia-owl": "http://dbpedia.org/ontology/", - "dbpprop": "http://dbpedia.org/property/", - "hydra": "http://www.w3.org/ns/hydra/core#", - "void": "http://rdfs.org/ns/void#" - } -} diff --git a/config/config-example.json b/config/config-example.json deleted file mode 100644 index c217e22d..00000000 --- a/config/config-example.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "title": "My Linked Data Fragments server", - - "datasources": { - "dbpedia": { - "title": "DBpedia 2014", - "type": "HdtDatasource", - "description": "DBpedia 2014 with an HDT back-end", - "settings": { "file": "data/dbpedia2014.hdt" } - }, - "dbpedia-sparql": { - "title": "DBpedia 3.9 (Virtuoso)", - "type": "SparqlDatasource", - "description": "DBpedia 3.9 with a Virtuoso back-end", - "settings": { "endpoint": "http://dbpedia.restdesc.org/", "defaultGraph": "http://dbpedia.org" } - } - }, - - "prefixes": { - "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", - "rdfs": "http://www.w3.org/2000/01/rdf-schema#", - "xsd": "http://www.w3.org/2001/XMLSchema#", - "dc": "http://purl.org/dc/terms/", - "foaf": "http://xmlns.com/foaf/0.1/", - "dbpedia": "http://dbpedia.org/resource/", - "dbpedia-owl": "http://dbpedia.org/ontology/", - "dbpprop": "http://dbpedia.org/property/", - "hydra": "http://www.w3.org/ns/hydra/core#", - "void": "http://rdfs.org/ns/void#" - } -} diff --git a/lerna.json b/lerna.json new file mode 100644 index 00000000..a8d78de8 --- /dev/null +++ b/lerna.json @@ -0,0 +1,21 @@ +{ + "lerna": "3.4.0", + "command": { + "publish": { + "ignoreChanges": [ + "*.md" + ], + "message": "Bump to release version %s", + "allowBranch": "master", + "npmClient": "npm" + } + }, + "packages": [ + "packages/*" + ], + "useWorkspaces": true, + "version": "3.3.0", + "loglevel": "success", + "registry": "https://registry.npmjs.org/", + "npmClient": "yarn" +} diff --git a/lib/controllers/AssetsController.js b/lib/controllers/AssetsController.js deleted file mode 100644 index 88becd7c..00000000 --- a/lib/controllers/AssetsController.js +++ /dev/null @@ -1,60 +0,0 @@ -/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ -/* An AssetsController responds to requests for assets */ - -var Controller = require('./Controller'), - fs = require('fs'), - path = require('path'), - mime = require('mime'), - Util = require('../Util'); - -// Creates a new AssetsController -function AssetsController(options) { - if (!(this instanceof AssetsController)) - return new AssetsController(options); - options = options || {}; - Controller.call(this, options); - - // Set up path matching - var assetsPath = options.assetsPath || '/assets/'; - this._matcher = new RegExp('^' + Util.toRegExp(assetsPath) + '(.+)|^/(\\w*)\\.ico$'); - - // Read all assets - var assetsFolder = options.assetsFolder || path.join(__dirname, '../../assets/'); - this._assets = {}; - this._readAssetsFolder(assetsFolder, ''); -} -Controller.extend(AssetsController); - -// Recursively reads assets in the folder, assigning them to the URL path -AssetsController.prototype._readAssetsFolder = function (assetsFolder, assetsPath) { - fs.readdirSync(assetsFolder).forEach(function (name) { - var filename = path.join(assetsFolder, name), stats = fs.statSync(filename); - // Read an asset file into memory - if (stats.isFile()) { - var assetType = mime.lookup(filename); - this._assets[assetsPath + name.replace(/[.][^.]+$/, '')] = { - type: assetType.indexOf('text/') ? assetType : assetType + ';charset=utf-8', - contents: fs.readFileSync(filename), - }; - } - // Read all asset files in a folder - else if (stats.isDirectory()) - this._readAssetsFolder(filename, assetsPath + name + '/'); - }, this); -}; - -// Try to serve the requested asset -AssetsController.prototype._handleRequest = function (request, response, next) { - var assetMatch = request.url.match(this._matcher), asset; - if (asset = assetMatch && this._assets[assetMatch[1] || assetMatch[2]]) { - response.writeHead(200, { - 'Content-Type': asset.type, - 'Cache-Control': 'public,max-age=1209600', // 14 days - }); - response.end(asset.contents); - } - else - next(); -}; - -module.exports = AssetsController; diff --git a/lib/controllers/Controller.js b/lib/controllers/Controller.js deleted file mode 100644 index 15e70a58..00000000 --- a/lib/controllers/Controller.js +++ /dev/null @@ -1,108 +0,0 @@ -/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ -/* Controller is a base class for HTTP request handlers */ - -var url = require('url'), - _ = require('lodash'), - ViewCollection = require('../views/ViewCollection'), - Util = require('../Util'), - parseForwarded = require('forwarded-parse'); - -// Creates a new Controller -function Controller(options) { - if (!(this instanceof Controller)) - return new Controller(options); - options = options || {}; - this._prefixes = options.prefixes || {}; - this._datasources = options.datasources || {}; - this._views = options.views && options.views.matchView ? - options.views : new ViewCollection(options.views); - - // Set up base URL (if we're behind a proxy, this allows reconstructing the actual request URL) - this._baseUrl = _.mapValues(url.parse(options.baseURL || '/'), function (value, key) { - return value && !/^(?:href|path|search|hash)$/.test(key) ? value : undefined; - }); -} - -// Makes Controller the prototype of the given class -Controller.extend = function extend(child) { - child.prototype = Object.create(this.prototype); - child.prototype.constructor = child; - child.extend = extend; -}; - -// Tries to process the HTTP request -Controller.prototype.handleRequest = function (request, response, next, settings) { - // Add a `parsedUrl` field to `request`, - // containing the parsed request URL, resolved against the base URL - if (!request.parsedUrl) { - // Keep the request's path and query, but take over all other defined baseURL properties - request.parsedUrl = _.defaults(_.pick(url.parse(request.url, true), 'path', 'pathname', 'query'), - this._baseUrl, - this._getForwarded(request), - this._getXForwardHeaders(request), - { protocol: 'http:', host: request.headers.host }); - } - - // Try to handle the request - var self = this; - try { this._handleRequest(request, response, done, settings); } - catch (error) { done(error); } - function done(error) { - if (self) { - // Send a 406 response if no suitable view was found - if (error instanceof ViewCollection.ViewCollectionError) - return self._handleNotAcceptable(request, response, next); - self = null; - next(error); - } - } -}; - -// Get host and protocol from HTTP's Forwarded header -Controller.prototype._getForwarded = function (request) { - if (!request.headers.forwarded) - return {}; - try { - var forwarded = _.defaults.apply(this, parseForwarded(request.headers.forwarded)); - return { - protocol: forwarded.proto ? forwarded.proto + ':' : undefined, - host: forwarded.host, - }; - } - catch (error) { return {}; } -}; - -// Get host and protocol from HTTP's X-Forwarded-* headers -Controller.prototype._getXForwardHeaders = function (request) { - return { - protocol: request.headers['x-forwarded-proto'] ? request.headers['x-forwarded-proto'] + ':' : undefined, - host: request.headers['x-forwarded-host'], - }; -}; - -// Tries to process the HTTP request in an implementation-specific way -Controller.prototype._handleRequest = function (request, response, next, settings) { - next(); -}; - -// Serves an error indicating content negotiation failure -Controller.prototype._handleNotAcceptable = function (request, response, next) { - response.writeHead(406, { 'Content-Type': Util.MIME_PLAINTEXT }); - response.end('No suitable content type found.\n'); -}; - -// Finds an appropriate view using content negotiation -Controller.prototype._negotiateView = function (viewName, request, response) { - // Indicate that the response is content-negotiated - var vary = response.getHeader('Vary'); - response.setHeader('Vary', 'Accept' + (vary ? ', ' + vary : '')); - // Negotiate a view - var viewMatch = this._views.matchView(viewName, request); - response.setHeader('Content-Type', viewMatch.responseType || viewMatch.type); - return viewMatch.view; -}; - -// Cleans resources used by the controller -Controller.prototype.close = function () { }; - -module.exports = Controller; diff --git a/lib/controllers/DereferenceController.js b/lib/controllers/DereferenceController.js deleted file mode 100644 index 2ab60ee3..00000000 --- a/lib/controllers/DereferenceController.js +++ /dev/null @@ -1,40 +0,0 @@ -/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ -/* A DeferenceController responds to dereferencing requests */ - -var Controller = require('./Controller'), - url = require('url'), - _ = require('lodash'), - Util = require('../Util'); - -// Creates a new DeferenceController -function DeferenceController(options) { - if (!(this instanceof DeferenceController)) - return new DeferenceController(options); - options = options || {}; - Controller.call(this, options); - - var paths = this._paths = options.dereference || {}; - if (!_.isEmpty(paths)) - this._matcher = new RegExp('^(' + Object.keys(paths).map(Util.toRegExp).join('|') + ')'); -} -Controller.extend(DeferenceController); - -// This default matcher never matches -DeferenceController.prototype._matcher = /$0^/; - -// Dereferences a URL by redirecting to its subject fragment of a certain data source -DeferenceController.prototype._handleRequest = function (request, response, next) { - var match = this._matcher.exec(request.url), datasource; - if (datasource = match && this._paths[match[1]]) { - var entity = url.format(_.defaults({ - pathname: '/' + datasource, - query: { subject: url.format(request.parsedUrl) }, - }, request.parsedUrl)); - response.writeHead(303, { 'Location': entity, 'Content-Type': Util.MIME_PLAINTEXT }); - response.end(entity); - } - else - next(); -}; - -module.exports = DeferenceController; diff --git a/lib/controllers/ErrorController.js b/lib/controllers/ErrorController.js deleted file mode 100644 index 8a06e7d6..00000000 --- a/lib/controllers/ErrorController.js +++ /dev/null @@ -1,31 +0,0 @@ -/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ -/* An ErrorController responds to requests that caused an error */ - -var Controller = require('./Controller'), - Util = require('../Util'); - -// Creates a new ErrorController -function ErrorController(options) { - if (!(this instanceof ErrorController)) - return new ErrorController(options); - Controller.call(this, options); -} -Controller.extend(ErrorController); - -// Serves an error response -ErrorController.prototype._handleRequest = function (request, response, next) { - // Try to write an error response through an appropriate view - var error = response.error || (response.error = new Error('Unknown error')), - view = this._negotiateView('Error', request, response), - metadata = { prefixes: this._prefixes, datasources: this._datasources, error: error }; - response.writeHead(500); - view.render(metadata, request, response); -}; - -// Writes the error in plaintext if no view was found -ErrorController.prototype._handleNotAcceptable = function (request, response, next) { - response.writeHead(500, { 'Content-Type': Util.MIME_PLAINTEXT }); - response.end('Application error: ' + response.error.message + '\n'); -}; - -module.exports = ErrorController; diff --git a/lib/controllers/MementoControllerExtension.js b/lib/controllers/MementoControllerExtension.js deleted file mode 100644 index de6188e4..00000000 --- a/lib/controllers/MementoControllerExtension.js +++ /dev/null @@ -1,57 +0,0 @@ -/*! @license MIT ©2016 Miel Vander Sande, Ghent University - imec */ -/* A MementoControllerExtension extends Triple Pattern Fragments responses with Memento headers. */ - -var Controller = require('./Controller'), - url = require('url'), - _ = require('lodash'); - -// Creates a new MementoControllerExtension -function MementoControllerExtension(settings) { - if (!(this instanceof MementoControllerExtension)) - return new MementoControllerExtension(settings); - - var timegates = settings.timegates || {}, timegateMap = {}; - this._timegateBaseUrl = timegates.baseURL || '/timegate/'; - this._timegateMap = timegateMap; - _.forIn(timegates.mementos || {}, function (setting, key) { - setting.versions.forEach(function (entry) { - timegateMap[entry] = { memento: key, - original: setting.originalBaseURL || ((settings.baseURL || '/') + key), - }; - }); - }); -} -Controller.extend(MementoControllerExtension); - -// Add Memento Link headers -MementoControllerExtension.prototype._handleRequest = function (request, response, next, settings) { - var datasource = settings.query.datasource, - memento = settings.datasource.memento, - requestQuery = request.url.match(/\?.*|$/)[0]; - - // Add link to original if it is a memento - if (memento && memento.interval && memento.interval.length === 2) { - var timegatePath = this._timegateBaseUrl + this._timegateMap[datasource].memento, - timegateUrl = url.format(_.defaults({ pathname: timegatePath }, request.parsedUrl)), - originalUrl = this._timegateMap[datasource].original + requestQuery, - datetime = new Date(memento.interval[0]).toUTCString(); - - response.setHeader('Link', '<' + originalUrl + '>;rel=original, <' + timegateUrl + '>;rel=timegate'); - response.setHeader('Memento-Datetime', datetime); - } - // Add timegate link if resource is not a memento - else { - var timegateSettings = settings.datasource.timegate, timegate; - // If a timegate URL is given, use it - if (typeof timegateSettings === 'string') - timegate = timegateSettings + requestQuery; - // If the timegate configuration is true, use local timegate - else if (timegateSettings === true) - timegate = url.format(_.defaults({ pathname: this._timegateBaseUrl + datasource }, request.parsedUrl)); - if (timegate) - response.setHeader('Link', '<' + timegate + '>;rel=timegate'); - } - next(); -}; - -module.exports = MementoControllerExtension; diff --git a/lib/controllers/NotFoundController.js b/lib/controllers/NotFoundController.js deleted file mode 100644 index 45ffe612..00000000 --- a/lib/controllers/NotFoundController.js +++ /dev/null @@ -1,33 +0,0 @@ -/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ -/* A NotFoundController responds to requests that cannot be resolved */ - -var Controller = require('./Controller'), - Util = require('../Util'); - -// Creates a new NotFoundController -function NotFoundController(options) { - if (!(this instanceof NotFoundController)) - return new NotFoundController(options); - Controller.call(this, options); -} -Controller.extend(NotFoundController); - -// Serves a 404 response -NotFoundController.prototype._handleRequest = function (request, response, next) { - // Cache 404 responses - response.setHeader('Cache-Control', 'public,max-age=3600'); - - // Render the 404 message using the appropriate view - var view = this._negotiateView('NotFound', request, response), - metadata = { url: request.url, prefixes: this._prefixes, datasources: this._datasources }; - response.writeHead(404); - view.render(metadata, request, response); -}; - -// Writes the 404 in plaintext if no view was found -NotFoundController.prototype._handleNotAcceptable = function (request, response, next) { - response.writeHead(404, { 'Content-Type': Util.MIME_PLAINTEXT }); - response.end(request.url + ' not found\n'); -}; - -module.exports = NotFoundController; diff --git a/lib/controllers/SummaryController.js b/lib/controllers/SummaryController.js deleted file mode 100644 index 149e00ba..00000000 --- a/lib/controllers/SummaryController.js +++ /dev/null @@ -1,51 +0,0 @@ -/*! @license MIT ©2015-2016 Miel Vander Sande, Ghent University - imec */ -/* An SummaryController responds to requests for summaries */ - -var Controller = require('./Controller'), - fs = require('fs'), - path = require('path'), - StreamParser = require('n3').StreamParser, - Util = require('../Util'); - -// Creates a new SummaryController -function SummaryController(options) { - if (!(this instanceof SummaryController)) - return new SummaryController(options); - options = options || {}; - Controller.call(this, options); - - // Settings for data summaries - var summaries = options.summaries || {}; - this._summariesFolder = path.join(__dirname, summaries.dir || '../../summaries'); - - // Set up path matching - this._summariesPath = summaries.path || '/summaries/', - this._matcher = new RegExp('^' + Util.toRegExp(this._summariesPath) + '(.+)$'); -} -Controller.extend(SummaryController); - -// Tries to serve the requested summary -SummaryController.prototype._handleRequest = function (request, response, next) { - var summaryMatch = this._matcher && this._matcher.exec(request.url), datasource; - if (datasource = summaryMatch && summaryMatch[1]) { - var summaryFile = path.join(this._summariesFolder, datasource + '.ttl'); - - // Read summary triples from file - var streamParser = new StreamParser({ blankNodePrefix: '' }), - inputStream = fs.createReadStream(summaryFile); - // If the summary cannot be read, invoke the next controller without error - inputStream.on('error', function (error) { next(); }); - inputStream.pipe(streamParser); - - // Set caching - response.setHeader('Cache-Control', 'public,max-age=604800'); // 14 days - - // Render the summary - var view = this._negotiateView('Summary', request, response); - view.render({ prefixes: this._prefixes, results: streamParser }, request, response); - } - else - next(); -}; - -module.exports = SummaryController; diff --git a/lib/controllers/TimegateController.js b/lib/controllers/TimegateController.js deleted file mode 100644 index 448ffcb4..00000000 --- a/lib/controllers/TimegateController.js +++ /dev/null @@ -1,145 +0,0 @@ -/*! @license MIT ©2015-2016 Miel Vander Sande, Ghent University - imec */ -/* An TimegateController responds to timegate requests */ - -var Controller = require('./Controller'), - _ = require('lodash'), - url = require('url'), - Util = require('../Util'); - -// Creates a new TimegateController -function TimegateController(options) { - if (!(this instanceof TimegateController)) - return new TimegateController(options); - options = options || {}; - Controller.call(this, options); - - // TODO: check wether datasources for timegates exist - // var datasources = options.datasources || {}; - - // Settings for timegate - var timegates = options.timegates || {}; - this._timemaps = _.mapValues(timegates.mementos, function (mementos) { - return _.assign(mementos, { - timemap: sortTimemap(_.map(_.pick(options.datasources, mementos.versions), - function (datasource, key) { - return _.assign(datasource.memento, { - datasource: key, - interval: (datasource.memento.interval || [0, 0]).map(toDate), - }); - })), - }); - }); - this._routers = options.routers || []; - - // Set up path matching - this._timegatePath = timegates.baseUrl || '/timegate/', - this._matcher = new RegExp('^' + Util.toRegExp(this._timegatePath) + '(.+?)\/?(?:\\?.*)?$'); -} -Controller.extend(TimegateController); - -// Perform time negotiation if applicable -TimegateController.prototype._handleRequest = function (request, response, next) { - var timegateMatch = this._matcher.exec(request.url), - datasource = timegateMatch && timegateMatch[1], - timemapDetails = datasource && this._timemaps[datasource]; - - // Is this resource a well-configured timegate? - if (timemapDetails) { - // For OPTIONS (preflight) requests, send only headers (avoiding expensive lookups) - if (request.method === 'OPTIONS') - return response.end(); - - // Try to find the memento closest to the requested date - var acceptDatetime = toDate(request.headers['accept-datetime']), - memento = this._getClosestMemento(timemapDetails.timemap, acceptDatetime); - if (memento) { - // Determine the URL of the memento - var mementoUrl = _.assign(request.parsedUrl, { pathname: memento.datasource }); - mementoUrl = url.format(mementoUrl); - - // Determine the URL of the original resource - var originalBaseURL = timemapDetails.originalBaseURL, originalUrl; - if (!originalBaseURL) - originalUrl = _.defaults({ pathname: datasource }, request.parsedUrl); - else - originalUrl = _.assign(url.parse(originalBaseURL), { query: request.parsedUrl.query }); - originalUrl = url.format(originalUrl); - - // Perform 200-style negotiation (https://tools.ietf.org/html/rfc7089#section-4.1.2) - response.setHeader('Link', '<' + originalUrl + '>;rel="original",' + - '<' + mementoUrl + '>;rel="memento";' + - 'datetime="' + memento.interval[0].toUTCString() + '"'); - response.setHeader('Vary', 'Accept-Datetime'); - response.setHeader('Content-Location', mementoUrl); - // Set request URL to the memento URL, which should be handled by a next controller - request.url = mementoUrl.replace(/^[^:]+:\/\/[^\/]+/, ''); - delete request.parsedUrl; - } - } - next(); -}; - -/* - * @param timemap: [{"datasource": "data source name", "interval": [start, end]}, ...] - the start, end values can either be Date objects, or ISO 8601 string. - * @param accept_datetime: the requested datetime value as Date object, or ISO 8601 string. - * @param sorted: bool. If not sorted, the timemap will be sorted using the start time in the interval. - - * eg: - var timemap = [ - {"datasource": "dbpedia_2012", "interval": ["2011-10-20T12:22:24Z", new Date("2012-10-19T12:22:24Z")]}, - {"datasource": "dbpedia_2015", "interval": ["2014-10-20T12:22:24Z", ""]}, - {"datasource": "dbpedia_2013", "interval": [new Date("2012-10-20T12:22:24Z"), new Date("2013-10-19T12:22:24Z")]}, - {"datasource": "dbpedia_2014", "interval": ["2013-10-20T12:22:24Z", "2014-10-19T12:22:24Z"]} - ]; - get_closest_memento(timemap, "2011-10-20T12:22:24Z", false); -*/ -TimegateController.prototype._getClosestMemento = function (timemap, acceptDatetime, unsorted) { - // NOTE: assuming that the interval is always specified as [start_date, end_date] - // empty timemap can't give any mementos - if (timemap.length === 0) - return null; - - // convert accept datetime to timestamp - acceptDatetime = toDate(acceptDatetime).getTime(); - - // If accept datetime is invalid, exit - if (isNaN(acceptDatetime)) return null; - // Sort timemap first if it is not sorted - if (unsorted) sortTimemap(timemap); - - // if the accept_datetime is less than the first memento, return first memento - var firstMemento = timemap[0], - firstMementoDatetime = toDate(firstMemento.interval[0]).getTime(); - if (acceptDatetime <= firstMementoDatetime) return firstMemento; - - // return the latest memento if the accept datetime is after it - var lastMemento = timemap[timemap.length - 1], - lastMementoDatetime = toDate(lastMemento.interval[1]).getTime(); - if (acceptDatetime >= lastMementoDatetime) return lastMemento; - - // check if the accept datetime falls within any intervals defined in the data sources. - for (var i = 0, memento; memento = timemap[i]; i++) { - var startTime = memento.interval[0].getTime(), - endTime = memento.interval[1].getTime(); - if (isFinite(startTime) && isFinite(endTime)) { - if (startTime > acceptDatetime) return timemap[i - 1]; - if (startTime <= acceptDatetime && endTime >= acceptDatetime) return memento; - } - } - return null; -}; - -// Sort the timemap by interval start date -function sortTimemap(timemap) { - return timemap.sort(function (a, b) { - return a.interval[0].getTime() - b.interval[0].getTime(); - }); -} - -// Convert the value to a date -function toDate(value) { - return typeof value === 'string' ? new Date(value) : (value || new Date()); -} - -module.exports = TimegateController; diff --git a/lib/controllers/TriplePatternFragmentsController.js b/lib/controllers/TriplePatternFragmentsController.js deleted file mode 100644 index 11267212..00000000 --- a/lib/controllers/TriplePatternFragmentsController.js +++ /dev/null @@ -1,105 +0,0 @@ -/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ -/* A TriplePatternFragmentsController responds to requests for fragments */ - -var Controller = require('./Controller'), - url = require('url'), - _ = require('lodash'), - N3Util = require('n3').Util; - -// Creates a new TriplePatternFragmentsController -function TriplePatternFragmentsController(options) { - if (!(this instanceof TriplePatternFragmentsController)) - return new TriplePatternFragmentsController(options); - options = options || {}; - Controller.call(this, options); - this._routers = options.routers || []; - this._extensions = options.extensions || []; -} -Controller.extend(TriplePatternFragmentsController); - -// Try to serve the requested fragment -TriplePatternFragmentsController.prototype._handleRequest = function (request, response, next) { - // Create the query from the request by calling the fragment routers - var requestParams = { url: request.parsedUrl }, - query = this._routers.reduce(function (query, router) { - try { router.extractQueryParams(requestParams, query); } - catch (e) { /* ignore routing errors */ } - return query; - }, { features: [] }); - - // Execute the query on the data source - var datasourceSettings = query.features.datasource && this._datasources[query.datasource]; - delete query.features.datasource; - if (!datasourceSettings || !datasourceSettings.datasource.supportsQuery(query)) - return next(); - - // Generate the query result - var view = this._negotiateView('TriplePatternFragments', request, response), - settings = this._createFragmentMetadata(request, query, datasourceSettings); - settings.results = datasourceSettings.datasource.select(query, - function (error) { error && next(error); }); - - // Execute the extensions and render the query result - var extensions = this._extensions, extensionId = 0; - (function nextExtension(error) { - // Log a possible error with the previous extension - if (error) - process.stderr.write(error.stack + '\n'); - // Execute the next extension - if (extensionId < extensions.length) - extensions[extensionId++].handleRequest(request, response, nextExtension, settings); - // Render the query result - else - view.render(settings, request, response); - })(); -}; - -// Creates metadata about the requested fragment -TriplePatternFragmentsController.prototype._createFragmentMetadata = -function (request, query, datasourceSettings) { - // TODO: these URLs should be generated by the routers - var requestUrl = request.parsedUrl, - // maintain the originally requested query string to avoid encoding differences - origQuery = request.url.replace(/[^?]+/, ''), - pageUrl = url.format(requestUrl).replace(/\?.*/, origQuery), - paramsNoPage = _.omit(requestUrl.query, 'page'), - currentPage = parseInt(requestUrl.query.page, 10) || 1, - datasourceUrl = url.format(_.omit(requestUrl, 'query')), - fragmentUrl = url.format(_.defaults({ query: paramsNoPage }, requestUrl)), - fragmentPageUrlBase = fragmentUrl + (/\?/.test(fragmentUrl) ? '&' : '?') + 'page=', - indexUrl = url.format(_.omit(requestUrl, 'search', 'query', 'pathname')) + '/'; - - // Generate a textual representation of the pattern - query.patternString = '{ ' + - (query.subject ? '<' + query.subject + '> ' : '?s ') + - (query.predicate ? '<' + query.predicate + '> ' : '?p ') + - (N3Util.isIRI(query.object) ? '<' + query.object + '> ' : (query.object || '?o')) + ' }'; - - return { - datasource: _.assign(_.omit(datasourceSettings, 'datasource'), { - index: indexUrl + '#dataset', - url: datasourceUrl + '#dataset', - templateUrl: datasourceUrl + '{?subject,predicate,object}', - }), - fragment: { - url: fragmentUrl, - pageUrl: pageUrl, - firstPageUrl: fragmentPageUrlBase + '1', - nextPageUrl: fragmentPageUrlBase + (currentPage + 1), - previousPageUrl: currentPage > 1 ? fragmentPageUrlBase + (currentPage - 1) : null, - }, - query: query, - prefixes: this._prefixes, - datasources: this._datasources, - }; -}; - -// Close all data sources -TriplePatternFragmentsController.prototype.close = function () { - for (var datasourceName in this._datasources) { - try { this._datasources[datasourceName].datasource.close(); } - catch (error) { /* ignore closing errors */ } - } -}; - -module.exports = TriplePatternFragmentsController; diff --git a/lib/controllers/WebIDControllerExtension.js b/lib/controllers/WebIDControllerExtension.js deleted file mode 100644 index b39bc835..00000000 --- a/lib/controllers/WebIDControllerExtension.js +++ /dev/null @@ -1,120 +0,0 @@ -/*! @license MIT ©2016 Miel Vander Sande, Ghent University - imec */ -/* A WebIDControllerExtension extends Triple Pattern Fragments responses with WebID authentication. */ - -var http = require('http'), - lru = require('lru-cache'), - parseCacheControl = require('parse-cache-control'), - N3 = require('n3'), - n3parser = N3.Parser, - N3Util = N3.Util, - Util = require('../Util'), - Controller = require('./Controller'); - -var CERT_NS = 'http://www.w3.org/ns/auth/cert#'; - -// Creates a new WebIDControllerExtension -function WebIDControllerExtension(settings) { - if (!(this instanceof WebIDControllerExtension)) - return new WebIDControllerExtension(settings); - Controller.call(this, settings); - - this._cache = lru(50); - this._protocol = settings.protocol; -} -Controller.extend(WebIDControllerExtension); - -// Add WebID Link headers -WebIDControllerExtension.prototype._handleRequest = function (request, response, next, settings) { - // Get WebID from certificate - if (this._protocol !== 'https') // This WebID implementation requires HTTPS - return next(); - - var self = this, - certificate = request.connection.getPeerCertificate(); - - if (!(certificate.subject && certificate.subject.subjectAltName)) - return this._handleForbidden(request, response, { reason: 'No WebID found in client certificate.' }); - - var webID = certificate.subject.subjectAltName.replace('uniformResourceIdentifier:', ''); - this._verifyWebID(webID, certificate.modulus, parseInt(certificate.exponent, 16), - function (error, verified, reason) { - if (!verified) - return self._handleForbidden(request, response, { webID: webID, reason: reason }); - - next(); - }); -}; - -// Verify webID -WebIDControllerExtension.prototype._verifyWebID = function (webID, modulus, exponent, callback) { - // request & parse - var parser = n3parser(), - id = {}; - - // parse webID - function parseTriple(error, triple, prefixes) { - if (error) - callback('Cannot parse WebID: ' + error); - else if (triple) { - switch (triple.predicate) { - case CERT_NS + 'modulus': - // Add modulus - var literalValue = N3Util.getLiteralValue(triple.object); - // Apply parsing method by nodejs - id.modulus = literalValue.slice(literalValue.indexOf('00:') === 0 ? 3 : 0).replace(/:/g, '').toUpperCase(); - break; - case CERT_NS + 'exponent': - // Add exponent - id.exponent = parseInt(N3Util.getLiteralValue(triple.object), 10); - break; - } - } - } - - function verify(m, e) { - if (m && m === modulus && e && e === exponent) - callback(null, true); - else - callback(null, false, 'WebID does not match certificate: ' + m + ' - ' + e + ' (webid) <> ' + modulus + ' - ' + exponent + ' (cert)'); - } - - // Try to get WebID from cache - var cachedId = this._cache.get(webID), self = this; - - if (cachedId) - verify(cachedId.modulus, cachedId.exponent); - else { - var req = http.request(webID, function (res) { - res.setEncoding('utf8'); - - parser.parse(res, parseTriple); - - res.on('end', function () { - var cacheControl = parseCacheControl(res.headers['Cache-Control'] || ''); - self._cache.set(webID, id, cacheControl['max-age'] || 0); - verify(id.modulus, id.exponent); - }); - }); - - req.on('error', function (e) { - callback(null, false, 'Unabled to download ' + webID + ' (' + e.message + ').'); - }); - - req.end(); - } -}; - -WebIDControllerExtension.prototype._handleForbidden = function (request, response, options) { - // Render the 404 message using the appropriate view - var view = this._negotiateView('Forbidden', request, response), - metadata = { url: request.url, prefixes: this._prefixes, datasources: this._datasources, reason: options.reason }; - response.writeHead(401); - view.render(metadata, request, response); -}; - -WebIDControllerExtension.prototype._handleNotAcceptable = function (request, response, options) { - response.writeHead(401, { 'Content-Type': Util.MIME_PLAINTEXT }); - response.end('Access to ' + request.url + ' is not allowed, verification for WebID ' + (options.webID || '') + ' failed. Reason: ' + (options.reason || '')); -}; - -module.exports = WebIDControllerExtension; diff --git a/lib/datasources/CompositeDatasource.js b/lib/datasources/CompositeDatasource.js deleted file mode 100644 index fcfbbfd0..00000000 --- a/lib/datasources/CompositeDatasource.js +++ /dev/null @@ -1,191 +0,0 @@ -/*! @license MIT ©2016 Ruben Taelman, Ghent University - imec */ -/* A CompositeDatasource delegates queries to an consecutive list of datasources. */ - -var Datasource = require('./Datasource'), - LRU = require('lru-cache'); - -// Creates a new CompositeDatasource -function CompositeDatasource(options) { - if (!(this instanceof CompositeDatasource)) - return new CompositeDatasource(options); - Datasource.call(this, options); - - if (!options.references) - throw new Error("A CompositeDatasource requires a `references` array of datasource id's in its settings."); - - var allDatasources = options.datasources; - this._datasources = {}; - this._datasourceNames = []; - for (var i = 0; i < options.references.length; i++) { - var datasourceName = options.references[i]; - var datasource = allDatasources[datasourceName]; - if (!datasource) - throw new Error('No datasource ' + datasourceName + ' could be found!'); - if (datasource.enabled !== false) { - this._datasources[datasourceName] = datasource; - this._datasourceNames.push(datasourceName); - } - } - this._countCache = new LRU({ max: 1000, maxAge: 1000 * 60 * 60 * 3 }); -} -Datasource.extend(CompositeDatasource); - -// Checks whether the data source can evaluate the given query -CompositeDatasource.prototype.supportsQuery = function (query) { - for (var datasourceName in this._datasources) { - if (this._getDatasourceByName(datasourceName).supportsQuery(query)) - return true; - } - return false; -}; - -// Find a datasource by datasource name -CompositeDatasource.prototype._getDatasourceByName = function (datasourceName) { - return this._datasources[datasourceName].datasource; -}; - -// Find a datasource by datasource id inside this composition -CompositeDatasource.prototype._getDatasourceById = function (datasourceIndex) { - return this._datasources[this._datasourceNames[datasourceIndex]].datasource; -}; - -// Count the amount of triple in the query result to get an exact count. -CompositeDatasource.prototype._getExactCount = function (datasource, query, callback) { - // Try to find a cache match - var cacheKey = query.subject + '|' + query.predicate + '|' + query.object; - var cache = this._countCache, count = cache.get(cacheKey); - if (count) return setImmediate(callback, count); - - // Otherwise, count all the triples manually - var emptyQuery = { offset: 0, subject: query.subject, predicate: query.predicate, object: query.object }; - var exactCount = 0; - var triplesCounter = { - _push: function (triple) { exactCount++; }, - close: function () { - // Cache large values; small ones are calculated fast anyway - if (exactCount > 1000) - cache.set(cacheKey, exactCount); - callback(exactCount); - }, - }; - datasource._executeQuery(emptyQuery, triplesCounter, noop); -}; - -// Recursively find all required datasource composition info to perform a query. -// The callback will provide the parameters: -// Datasource id to start querying from -// The offset to use to start querying from the given datasource id -// The total count for all datasources -CompositeDatasource.prototype._getDatasourceInfo = function (query, absoluteOffset, callback) { - var self = this; - var emptyQuery = { - offset: 0, limit: 1, - subject: query.subject, predicate: query.predicate, object: query.object, - }; - return findRecursive(0, absoluteOffset, -1, -1, 0, callback, true); - - function findRecursive(datasourceIndex, offset, chosenDatasource, chosenOffset, totalCount, hasExactCount) { - if (datasourceIndex >= self._datasourceNames.length) - // We checked all datasources, return our accumulated information - callback(chosenDatasource, chosenOffset, totalCount, hasExactCount); - else { - var datasource = self._getDatasourceById(datasourceIndex); - var metadataReader = { - _push: noop, - close: noop, - setProperty: function (name, metadata) { - if (name !== 'metadata') return; - // If we are still looking for an appropriate datasource, we need exact counts - var count = metadata.totalCount, exact = metadata.hasExactCount; - if (offset > 0 && !exact) { - self._getExactCount(datasource, query, function (exactCount) { - count = exactCount; - exact = true; - continueRecursion(); - }); - } - else - continueRecursion(); - - function continueRecursion() { - if (chosenDatasource < 0 && offset < count) { - // We can start querying from this datasource - setImmediate(function () { - findRecursive(datasourceIndex + 1, offset - count, datasourceIndex, offset, - totalCount + count, hasExactCount && exact); - }); - } - else { - // We forward our accumulated information and go check the next datasource - setImmediate(function () { - findRecursive(datasourceIndex + 1, offset - count, chosenDatasource, chosenOffset, - totalCount + count, hasExactCount && exact); - }); - } - } - }, - }; - datasource._executeQuery(emptyQuery, metadataReader); - } - } -}; - -// Writes the results of the query to the given triple stream -CompositeDatasource.prototype._executeQuery = function (query, destination) { - var offset = query.offset || 0, limit = query.limit || Infinity; - var self = this; - this._getDatasourceInfo(query, offset, function (datasourceIndex, relativeOffset, totalCount, hasExactCount) { - if (datasourceIndex < 0) { - // No valid datasource has been found - destination.setProperty('metadata', { totalCount: totalCount, hasExactCount: hasExactCount }); - destination.close(); - } - else { - // Send query to first applicable datasource and optionally emit triples from consecutive datasources - destination.setProperty('metadata', { totalCount: totalCount, hasExactCount: hasExactCount }); - - // Modify our triple stream so that if all results from one datasource have arrived, - // check if we haven't reached the limit and if so, trigger a new query for the next datasource. - var emitted = 0; - countItems(destination, function (localEmittedCount) { - // This is called after the last element has been pushed - - // If we haven't reached our limit, try to fill it with other datasource query results. - emitted += localEmittedCount; - datasourceIndex++; - if (emitted < limit && datasourceIndex < self._datasourceNames.length) { - var localLimit = limit - emitted; - var subQuery = { offset: 0, limit: localLimit, - subject: query.subject, predicate: query.predicate, object: query.object }; - self._getDatasourceById(datasourceIndex)._executeQuery(subQuery, destination, noop); - return false; - } - else - return true; - }); - - // Initiate query to the first datasource. - var subQuery = { offset: relativeOffset, limit: limit, - subject: query.subject, predicate: query.predicate, object: query.object }; - self._getDatasourceById(datasourceIndex)._executeQuery(subQuery, destination, noop); - } - }); - - // Counts the number of triples and sends them through the callback, - // only closing the iterator when the callback returns true. - function countItems(destination, closeCallback) { - var count = 0, originalPush = destination._push, originalClose = destination.close; - destination._push = function (element) { - count++; - originalPush.call(destination, element); - }; - destination.close = function () { - if (closeCallback(count)) - originalClose.call(destination); - }; - } -}; - -function noop() {} - -module.exports = CompositeDatasource; diff --git a/lib/datasources/Datasource.js b/lib/datasources/Datasource.js deleted file mode 100644 index 0f23a4c1..00000000 --- a/lib/datasources/Datasource.js +++ /dev/null @@ -1,171 +0,0 @@ -/*! @license MIT ©2014-2016 Ruben Verborgh, Ghent University - imec */ -/* A Datasource provides base functionality for queryable access to a source of triples. */ - -var fs = require('fs'), - _ = require('lodash'), - BufferedIterator = require('asynciterator').BufferedIterator, - EventEmitter = require('events'); - -// Creates a new Datasource -function Datasource(options) { - if (!(this instanceof Datasource)) - return new Datasource(); - EventEmitter.call(this); - - // Set the options - options = options || {}; - this._request = options.request || require('request'); - this._blankNodePrefix = options.blankNodePrefix || 'genid:'; - this._blankNodePrefixLength = this._blankNodePrefix.length; - - // Initialize the datasource asynchronously - setImmediate(function (self) { - var done = _.once(function (error) { - if (error) - self.emit('error', error); - else { - self.initialized = true; - self.emit('initialized'); - } - }); - try { self._initialize(done); } - catch (error) { done(error); } - }, this); -} -Datasource.prototype = new EventEmitter(); - -// Makes Datasource the prototype of the given class -Datasource.extend = function extend(child, supportedFeatureList) { - child.prototype = Object.create(this.prototype); - child.prototype.constructor = child; - child.extend = extend; - - // Expose the supported query features - if (supportedFeatureList && supportedFeatureList.length) { - var supportedFeatures = {}; - for (var i = 0; i < supportedFeatureList.length; i++) - supportedFeatures[supportedFeatureList[i]] = true; - Object.defineProperty(child.prototype, 'supportedFeatures', { - enumerable: true, - value: Object.freeze(supportedFeatures), - }); - } -}; - -// Whether the datasource can be queried -Datasource.prototype.initialized = false; - -// Prepares the datasource for querying -Datasource.prototype._initialize = function (done) { - done(); -}; - -// The query features supported by this data source -Object.defineProperty(Datasource.prototype, 'supportedFeatures', { - enumerable: true, - value: Object.freeze({}), -}); - -// Checks whether the data source can evaluate the given query -Datasource.prototype.supportsQuery = function (query) { - // An uninitialized datasource does not support any query - if (!this.initialized) - return false; - - // A query is supported if the data source supports all of its features - var features = query.features, supportedFeatures = this.supportedFeatures, feature; - if (features) { - for (feature in features) { - if (features[feature] && !supportedFeatures[feature]) - return false; - } - return true; - } - // A query without features is supported if this data source has at least one feature - else { - for (feature in supportedFeatures) { - if (supportedFeatures[feature]) - return true; - } - return false; - } -}; - -// Selects the triples that match the given query, returning a triple stream -Datasource.prototype.select = function (query, onError) { - if (!this.initialized) - return onError && onError(new Error('The datasource is not initialized yet')); - if (!this.supportsQuery(query)) - return onError && onError(new Error('The datasource does not support the given query')); - - // Translate blank nodes IRIs in the query to blank nodes - var blankNodePrefix = this._blankNodePrefix, blankNodePrefixLength = this._blankNodePrefixLength; - if (query.subject && query.subject.indexOf(blankNodePrefix) === 0) - (query = _.clone(query)).subject = '_:' + query.subject.substr(blankNodePrefixLength); - if (query.object && query.object.indexOf(blankNodePrefix) === 0) - (query = _.clone(query)).object = '_:' + query.object.substr(blankNodePrefixLength); - - // Translate blank nodes in the result to blank node IRIs - var destination = new BufferedIterator(), outputTriples; - outputTriples = destination.map(function (triple) { - if (triple.subject[0] === '_') triple.subject = blankNodePrefix + triple.subject.substr(2); - if (triple.object[0] === '_') triple.object = blankNodePrefix + triple.object.substr(2); - return triple; - }); - outputTriples.copyProperties(destination, ['metadata']); - onError && outputTriples.on('error', onError); - - // Execute the query - try { this._executeQuery(query, destination); } - catch (error) { outputTriples.emit('error', error); } - return outputTriples; -}; - -// Writes the results of the query to the given destination -Datasource.prototype._executeQuery = function (query, destination) { - throw new Error('_executeQuery has not been implemented'); -}; - -// Retrieves a stream through HTTP or the local file system -Datasource.prototype._fetch = function (options) { - var self = this, stream, - url = options.url, protocolMatch = /^(?:([a-z]+):)?/.exec(url); - switch (protocolMatch[1] || 'file') { - // Fetch a representation through HTTP(S) - case 'http': - case 'https': - stream = this._request(options); - stream.on('response', function (response) { - if (response.statusCode >= 300) { - setImmediate(function () { - stream.emit('error', new Error(url + ' returned ' + response.statusCode)); - }); - } - }); - break; - // Read a file from the local filesystem - case 'file': - stream = fs.createReadStream(url.substr(protocolMatch[0].length), { encoding: 'utf8' }); - break; - default: - stream = new EventEmitter(); - setImmediate(function () { - stream.emit('error', new Error('Unknown protocol: ' + protocolMatch[1])); - }); - } - - // If the stream has no other error handlers attached (besides this one), - // emit the stream error as a datasource error - stream.on('error', function (error) { - if (stream.listenerCount('error') === 1) - self.emit('error', error); - }); - return stream; -}; - -// Closes the data source, freeing possible resources used -Datasource.prototype.close = function (callback) { - callback && callback(); -}; - -module.exports = Datasource; diff --git a/lib/datasources/EmptyDatasource.js b/lib/datasources/EmptyDatasource.js deleted file mode 100644 index d45f5c5a..00000000 --- a/lib/datasources/EmptyDatasource.js +++ /dev/null @@ -1,17 +0,0 @@ -/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ -/* An empty data source doesn't contain any triples. */ - -var MemoryDatasource = require('./MemoryDatasource'); - -// Creates a new EmptyDatasource -function EmptyDatasource(options) { - if (!(this instanceof EmptyDatasource)) - return new EmptyDatasource(options); - MemoryDatasource.call(this, options); -} -MemoryDatasource.extend(EmptyDatasource); - -// Retrieves all triples in the datasource -EmptyDatasource.prototype._getAllTriples = function (addTriple, done) { done(); }; - -module.exports = EmptyDatasource; diff --git a/lib/datasources/ExternalHdtDatasource.js b/lib/datasources/ExternalHdtDatasource.js deleted file mode 100644 index 208495bf..00000000 --- a/lib/datasources/ExternalHdtDatasource.js +++ /dev/null @@ -1,75 +0,0 @@ -/*! @license MIT ©2014-2016 Ruben Verborgh, Ghent University - imec */ -/* An ExternalHdtDatasource uses an external process to query an HDT document. */ - -var Datasource = require('./Datasource'), - fs = require('fs'), - path = require('path'), - N3Parser = require('n3').Parser, - spawn = require('child_process').spawn; - -var hdtUtility = path.join(__dirname, '../../node_modules/.bin/hdt'); - -// Creates a new ExternalHdtDatasource -function ExternalHdtDatasource(options) { - if (!(this instanceof ExternalHdtDatasource)) - return new ExternalHdtDatasource(options); - Datasource.call(this, options); - - // Test whether the HDT file exists - this._options = options = options || {}; - this._hdtFile = (options.file || '').replace(/^file:\/\//, ''); -} -Datasource.extend(ExternalHdtDatasource, ['triplePattern', 'limit', 'offset', 'totalCount']); - -// Prepares the datasource for querying -ExternalHdtDatasource.prototype._initialize = function (done) { - if (this._options.checkFile !== false) { - if (!fs.existsSync(this._hdtFile)) - return done(new Error('Not an HDT file: ' + this._hdtFile)); - if (!fs.existsSync(hdtUtility)) - return done(new Error('hdt not found: ' + hdtUtility)); - } - done(); -}; - -// Writes the results of the query to the given triple stream -ExternalHdtDatasource.prototype._executeQuery = function (query, destination) { - // Execute the external HDT utility - var hdtFile = this._hdtFile, offset = query.offset || 0, limit = query.limit || Infinity, - hdt = spawn(hdtUtility, [ - '--query', (query.subject || '?s') + ' ' + - (query.predicate || '?p') + ' ' + (query.object || '?o'), - '--offset', offset, '--limit', limit, '--format', 'turtle', - '--', hdtFile, - ], { stdio: ['ignore', 'pipe', 'ignore'] }); - // Parse the result triples - hdt.stdout.setEncoding('utf8'); - var parser = new N3Parser(), tripleCount = 0, estimatedTotalCount = 0, hasExactCount = true; - parser.parse(hdt.stdout, function (error, triple) { - if (error) - destination.emit('error', new Error('Invalid query result: ' + error.message)); - else if (triple) - tripleCount++, destination._push(triple); - else { - // Ensure the estimated total count is as least as large as the number of triples - if (tripleCount && estimatedTotalCount < offset + tripleCount) - estimatedTotalCount = offset + (tripleCount < query.limit ? tripleCount : 2 * tripleCount); - destination.setProperty('metadata', { totalCount: estimatedTotalCount, hasExactCount: hasExactCount }); - destination.close(); - } - }); - parser._prefixes._ = '_:'; // Ensure blank nodes are named consistently - - // Extract the estimated number of total matches from the first (comment) line - hdt.stdout.once('data', function (header) { - estimatedTotalCount = parseInt(header.match(/\d+/), 10) || 0; - hasExactCount = header.indexOf('estimated') < 0; - }); - - // Report query errors - hdt.on('exit', function (exitCode) { - exitCode && destination.emit('error', new Error('Could not query ' + hdtFile)); - }); -}; - -module.exports = ExternalHdtDatasource; diff --git a/lib/datasources/HdtDatasource.js b/lib/datasources/HdtDatasource.js deleted file mode 100644 index 6afe84bc..00000000 --- a/lib/datasources/HdtDatasource.js +++ /dev/null @@ -1,63 +0,0 @@ -/*! @license MIT ©2014-2016 Ruben Verborgh, Ghent University - imec */ -/* An HdtDatasource loads and queries an HDT document in-process. */ - -var Datasource = require('./Datasource'), - hdt = require('hdt'), - ExternalHdtDatasource = require('./ExternalHdtDatasource'); - -// Creates a new HdtDatasource -function HdtDatasource(options) { - if (!(this instanceof HdtDatasource)) - return new HdtDatasource(options); - Datasource.call(this, options); - - options = options || {}; - // Switch to external HDT datasource if the `external` flag is set - if (options.external) - return new ExternalHdtDatasource(options); - this._hdtFile = (options.file || '').replace(/^file:\/\//, ''); -} -Datasource.extend(HdtDatasource, ['triplePattern', 'limit', 'offset', 'totalCount']); - -// Loads the HDT datasource -HdtDatasource.prototype._initialize = function (done) { - var datasource = this; - hdt.fromFile(this._hdtFile).then(function (hdtDocument) { - datasource._hdtDocument = hdtDocument; - }).then(done, done); -}; - -// Writes the results of the query to the given triple stream -HdtDatasource.prototype._executeQuery = function (query, destination) { - this._hdtDocument.searchTriples(query.subject, query.predicate, query.object, - { limit: query.limit, offset: query.offset }) - .then(function (result) { - var triples = result.triples, - estimatedTotalCount = result.totalCount, - hasExactCount = result.hasExactCount; - // Ensure the estimated total count is as least as large as the number of triples - var tripleCount = triples.length, offset = query.offset || 0; - if (tripleCount && estimatedTotalCount < offset + tripleCount) - estimatedTotalCount = offset + (tripleCount < query.limit ? tripleCount : 2 * tripleCount); - destination.setProperty('metadata', { totalCount: estimatedTotalCount, hasExactCount: hasExactCount }); - // Add the triples to the output - for (var i = 0; i < tripleCount; i++) - destination._push(triples[i]); - destination.close(); - }, - function (error) { destination.emit('error', error); }); -}; - -// Closes the data source -HdtDatasource.prototype.close = function (done) { - // Close the HDT document if it is open - if (this._hdtDocument) { - this._hdtDocument.close().then(done, done); - delete this._hdtDocument; - } - // If initialization was still pending, close immediately after initializing - else if (!this.initialized) - this.on('initialized', this.close.bind(this, done)); -}; - -module.exports = HdtDatasource; diff --git a/lib/datasources/IndexDatasource.js b/lib/datasources/IndexDatasource.js deleted file mode 100644 index e8b8effb..00000000 --- a/lib/datasources/IndexDatasource.js +++ /dev/null @@ -1,39 +0,0 @@ -/*! @license MIT ©2014-2016 Ruben Verborgh, Ghent University - imec */ -/* An IndexDatasource is a datasource that lists other data sources. */ - -var MemoryDatasource = require('./MemoryDatasource'); - -var rdf = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', - rdfs = 'http://www.w3.org/2000/01/rdf-schema#', - dc = 'http://purl.org/dc/terms/', - voID = 'http://rdfs.org/ns/void#'; - -// Creates a new IndexDatasource -function IndexDatasource(options) { - if (!(this instanceof IndexDatasource)) - return new IndexDatasource(options); - MemoryDatasource.call(this, options); - this._datasources = options ? options.datasources : {}; -} -MemoryDatasource.extend(IndexDatasource); - -// Creates triples for each data source -IndexDatasource.prototype._getAllTriples = function (addTriple, done) { - for (var name in this._datasources) { - var datasource = this._datasources[name], datasourceUrl = datasource.url; - if (!datasource.hide) { - triple(datasourceUrl, rdf + 'type', voID + 'Dataset'); - triple(datasourceUrl, rdfs + 'label', datasource.title, true); - triple(datasourceUrl, dc + 'title', datasource.title, true); - triple(datasourceUrl, dc + 'description', datasource.description, true); - } - } - function triple(subject, predicate, object, isLiteral) { - if (subject && predicate && object) - addTriple(subject, predicate, isLiteral ? '"' + object + '"' : object); - } - delete this._datasources; - done(); -}; - -module.exports = IndexDatasource; diff --git a/lib/datasources/JsonLdDatasource.js b/lib/datasources/JsonLdDatasource.js deleted file mode 100644 index 4b25d90c..00000000 --- a/lib/datasources/JsonLdDatasource.js +++ /dev/null @@ -1,64 +0,0 @@ -/*! @license MIT ©2014-2016 Ruben Verborgh, Ghent University - imec */ -/* An JsonLdDatasource fetches data from a JSON-LD document. */ - -var MemoryDatasource = require('./MemoryDatasource'), - jsonld = require('jsonld'); - -var ACCEPT = 'application/ld+json;q=1.0,application/json;q=0.7'; - -// Creates a new JsonLdDatasource -function JsonLdDatasource(options) { - if (!(this instanceof JsonLdDatasource)) - return new JsonLdDatasource(options); - MemoryDatasource.call(this, options); - this._url = options && (options.url || options.file); -} -MemoryDatasource.extend(JsonLdDatasource); - -// Retrieves all triples from the document -JsonLdDatasource.prototype._getAllTriples = function (addTriple, done) { - // Read the JSON-LD document - var json = '', - document = this._fetch({ url: this._url, headers: { accept: ACCEPT } }); - document.on('data', function (data) { json += data; }); - document.on('end', function () { - // Parse the JSON document - try { json = JSON.parse(json); } - catch (error) { return done(error); } - // Convert the JSON-LD to triples - extractTriples(json, addTriple, done); - }); -}; - -// Extracts triples from a JSON-LD document -function extractTriples(json, addTriple, done) { - jsonld.toRDF(json, function (error, triples) { - for (var graphName in triples) { - triples[graphName].forEach(function (triple) { - addTriple(triple.subject.value, - triple.predicate.value, - convertEntity(triple.object)); - }); - } - done(error); - }); -} - -// Converts a jsonld.js entity to the N3.js in-memory representation -function convertEntity(entity) { - // Return IRIs and blank nodes as-is - if (entity.type !== 'literal') - return entity.value; - else { - // Add a language tag to the literal if present - if ('language' in entity) - return '"' + entity.value + '"@' + entity.language; - // Add a datatype to the literal if present - if (entity.datatype !== 'http://www.w3.org/2001/XMLSchema#string') - return '"' + entity.value + '"^^' + entity.datatype; - // Otherwise, return the regular literal - return '"' + entity.value + '"'; - } -} - -module.exports = JsonLdDatasource; diff --git a/lib/datasources/MemoryDatasource.js b/lib/datasources/MemoryDatasource.js deleted file mode 100644 index 849ca3cc..00000000 --- a/lib/datasources/MemoryDatasource.js +++ /dev/null @@ -1,38 +0,0 @@ -/*! @license MIT ©2014-2016 Ruben Verborgh, Ghent University - imec */ -/* A MemoryDatasource queries a set of in-memory triples. */ - -var Datasource = require('./Datasource'), - N3Store = require('n3').Store; - -// Creates a new MemoryDatasource -function MemoryDatasource(options) { - if (!(this instanceof MemoryDatasource)) - return new MemoryDatasource(options); - Datasource.call(this, options); -} -Datasource.extend(MemoryDatasource, ['triplePattern', 'limit', 'offset', 'totalCount']); - -// Prepares the datasource for querying -MemoryDatasource.prototype._initialize = function (done) { - var tripleStore = this._tripleStore = new N3Store(); - this._getAllTriples(function (s, p, o, g) { tripleStore.addTriple(s, p, o, g); }, done); -}; - -// Retrieves all triples in the datasource -MemoryDatasource.prototype._getAllTriples = function (addTriple, done) { - throw new Error('_getAllTriples is not implemented'); -}; - -// Writes the results of the query to the given triple stream -MemoryDatasource.prototype._executeQuery = function (query, destination) { - var offset = query.offset || 0, limit = query.limit || Infinity, - triples = this._tripleStore.findByIRI(query.subject, query.predicate, query.object); - // Send the metadata - destination.setProperty('metadata', { totalCount: triples.length, hasExactCount: true }); - // Send the requested subset of triples - for (var i = offset, l = Math.min(offset + limit, triples.length); i < l; i++) - destination._push(triples[i]); - destination.close(); -}; - -module.exports = MemoryDatasource; diff --git a/lib/datasources/SparqlDatasource.js b/lib/datasources/SparqlDatasource.js deleted file mode 100644 index b3e86b51..00000000 --- a/lib/datasources/SparqlDatasource.js +++ /dev/null @@ -1,149 +0,0 @@ -/*! @license MIT ©2014-2016 Ruben Verborgh, Ghent University - imec */ -/* A SparqlDatasource provides queryable access to a SPARQL endpoint. */ - -var Datasource = require('./Datasource'), - N3 = require('n3'), - LRU = require('lru-cache'); - -var ENDPOINT_ERROR = 'Error accessing SPARQL endpoint'; -var INVALID_TURTLE_RESPONSE = 'The endpoint returned an invalid Turtle response.'; - -// Creates a new SparqlDatasource -function SparqlDatasource(options) { - if (!(this instanceof SparqlDatasource)) - return new SparqlDatasource(options); - Datasource.call(this, options); - this._countCache = new LRU({ max: 1000, maxAge: 1000 * 60 * 60 * 3 }); - - // Set endpoint URL and default graph - options = options || {}; - this._endpoint = this._endpointUrl = (options.endpoint || '').replace(/[\?#][^]*$/, ''); - if (!options.defaultGraph) - this._endpointUrl += '?query='; - else - this._endpointUrl += '?default-graph-uri=' + encodeURIComponent(options.defaultGraph) + '&query='; -} -Datasource.extend(SparqlDatasource, ['triplePattern', 'limit', 'offset', 'totalCount']); - -// Writes the results of the query to the given triple stream -SparqlDatasource.prototype._executeQuery = function (query, destination) { - // Create the HTTP request - var sparqlPattern = this._createTriplePattern(query), self = this, - constructQuery = this._createConstructQuery(sparqlPattern, query.offset, query.limit), - request = { url: this._endpointUrl + encodeURIComponent(constructQuery), - headers: { accept: 'text/turtle;q=1.0,application/n-triples;q=0.5,text/n3;q=0.3' }, - }; - - // Fetch and parse matching triples - (new N3.Parser()).parse(this._request(request, emitError), function (error, triple) { - if (!error) { - if (triple) - destination._push(triple); - else - destination.close(); - } - // Virtuoso sometimes sends invalid Turtle, so try N-Triples. - // We don't just accept N-Triples right away because it is slower, - // and some Virtuoso versions don't support it and/or get conneg wrong. - else { - request.headers.accept = 'application/n-triples'; - return (new N3.Parser()).parse(self._request(request, emitError), function (error, triple) { - if (error) - emitError(new Error(INVALID_TURTLE_RESPONSE)); - else if (triple) - destination._push(triple); - else - destination.close(); - }); - } - }); - - // Determine the total number of matching triples - this._getPatternCount(sparqlPattern, function (error, totalCount) { - if (error) - emitError(error); - else if (typeof totalCount === 'number') - destination.setProperty('metadata', { totalCount: totalCount, hasExactCount: true }); - }); - - // Emits an error on the triple stream - function emitError(error) { - error && destination.emit('error', new Error(ENDPOINT_ERROR + ' ' + self._endpoint + ': ' + error.message)); - } -}; - -// Retrieves the (approximate) number of triples that match the SPARQL pattern -SparqlDatasource.prototype._getPatternCount = function (sparqlPattern, callback) { - // Try to find a cache match - var cache = this._countCache, count = cache.get(sparqlPattern); - if (count) return setImmediate(callback, null, count); - - // Execute the count query - var countResponse = this._request({ - url: this._endpointUrl + encodeURIComponent(this._createCountQuery(sparqlPattern)), - headers: { accept: 'text/csv' }, - timeout: 7500, - }, callback); - - // Parse SPARQL response in CSV format (2 lines: variable name / count value) - var csv = ''; - countResponse.on('data', function (data) { csv += data; }); - countResponse.on('end', function () { - var countMatch = csv.match(/\d+/); - if (!countMatch) - callback(new Error('COUNT query failed.')); - else { - var count = parseInt(countMatch[0], 10); - // Cache large values; small ones are calculated fast anyway - if (count > 100000) - cache.set(sparqlPattern, count); - callback(null, count); - } - }); -}; - -// Creates a CONSTRUCT query from the given SPARQL pattern -SparqlDatasource.prototype._createConstructQuery = function (sparqlPattern, offset, limit) { - var query = ['CONSTRUCT', sparqlPattern, 'WHERE', sparqlPattern]; - // Even though the SPARQL spec indicates that - // LIMIT and OFFSET might be meaningless without ORDER BY, - // this doesn't seem a problem in practice. - // Furthermore, sorting can be slow. Therefore, don't sort. - limit && query.push('LIMIT', limit); - offset && query.push('OFFSET', offset); - return query.join(' '); -}; - -// Creates a SELECT COUNT(*) query from the given SPARQL pattern -SparqlDatasource.prototype._createCountQuery = function (sparqlPattern) { - return 'SELECT (COUNT(*) AS ?c) WHERE ' + sparqlPattern; -}; - -// Creates a SPARQL pattern for the given triple pattern -SparqlDatasource.prototype._createTriplePattern = function (triple) { - var query = ['{'], literalMatch; - - // Add a possible subject IRI - triple.subject ? query.push('<', triple.subject, '> ') : query.push('?s '); - - // Add a possible predicate IRI - triple.predicate ? query.push('<', triple.predicate, '> ') : query.push('?p '); - - // Add a possible object IRI or literal - if (N3.Util.isIRI(triple.object)) - query.push('<', triple.object, '>'); - else if (!(literalMatch = /^"([^]*)"(?:(@[^"]+)|\^\^([^"]+))?$/.exec(triple.object))) - query.push('?o'); - else { - if (!/["\\]/.test(literalMatch[1])) - query.push('"', literalMatch[1], '"'); - else - query.push('"""', literalMatch[1].replace(/(["\\])/g, '\\$1'), '"""'); - literalMatch[2] ? query.push(literalMatch[2]) - : literalMatch[3] && query.push('^^<', literalMatch[3], '>'); - } - - return query.push('}'), query.join(''); -}; - -module.exports = SparqlDatasource; diff --git a/lib/datasources/TurtleDatasource.js b/lib/datasources/TurtleDatasource.js deleted file mode 100644 index 9c71bbbb..00000000 --- a/lib/datasources/TurtleDatasource.js +++ /dev/null @@ -1,27 +0,0 @@ -/*! @license MIT ©2014-2016 Ruben Verborgh, Ghent University - imec */ -/* A TurtleDatasource fetches data from a Turtle document. */ - -var MemoryDatasource = require('./MemoryDatasource'), - N3Parser = require('n3').Parser; - -var ACCEPT = 'text/turtle;q=1.0,application/n-triples;q=0.7,text/n3;q=0.6'; - -// Creates a new TurtleDatasource -function TurtleDatasource(options) { - if (!(this instanceof TurtleDatasource)) - return new TurtleDatasource(options); - MemoryDatasource.call(this, options); - this._url = options && (options.url || options.file); -} -MemoryDatasource.extend(TurtleDatasource); - -// Retrieves all triples from the document -TurtleDatasource.prototype._getAllTriples = function (addTriple, done) { - var document = this._fetch({ url: this._url, headers: { accept: ACCEPT } }, done); - N3Parser._resetBlankNodeIds(); - new N3Parser().parse(document, function (error, triple) { - triple ? addTriple(triple) : done(error); - }); -}; - -module.exports = TurtleDatasource; diff --git a/lib/routers/DatasourceRouter.js b/lib/routers/DatasourceRouter.js deleted file mode 100644 index 22fd960c..00000000 --- a/lib/routers/DatasourceRouter.js +++ /dev/null @@ -1,16 +0,0 @@ -/*! @license MIT ©2014-2016 Ruben Verborgh, Ghent University - imec */ -/* A DatasourceRouter routes URLs to data sources. */ - -// Creates a new DatasourceRouter -function DatasourceRouter() { - if (!(this instanceof DatasourceRouter)) - return new DatasourceRouter(); -} - -// Extracts the data source parameter from the request and adds it to the query -DatasourceRouter.prototype.extractQueryParams = function (request, query) { - (query.features || (query.features = {})).datasource = true; - query.datasource = /^\/?(.*)$/.exec(request.url && request.url.pathname || '')[1]; -}; - -module.exports = DatasourceRouter; diff --git a/lib/routers/PageRouter.js b/lib/routers/PageRouter.js deleted file mode 100644 index 4f5beefc..00000000 --- a/lib/routers/PageRouter.js +++ /dev/null @@ -1,25 +0,0 @@ -/*! @license MIT ©2014-2016 Ruben Verborgh, Ghent University - imec */ -/* A PageRouter routes page numbers to offsets */ - -// Creates a new PageRouter with the given page size, which defaults to 100. -function PageRouter(config) { - if (!(this instanceof PageRouter)) - return new PageRouter(config); - config = config || {}; - this.pageSize = isFinite(config.pageSize) && config.pageSize > 1 ? ~~config.pageSize : 100; -} - -// Extracts a page parameter from the request and adds it to the query -PageRouter.prototype.extractQueryParams = function (request, query) { - var page = request.url && request.url.query && request.url.query.page, - features = query.features || (query.features = {}); - - // Set the limit to the page size - features.limit = true, query.limit = this.pageSize; - - // If a page is given, adjust the offset - if (page && /^\d+$/.test(page) && (page = parseInt(page, 10)) > 1) - features.offset = true, query.offset = this.pageSize * (page - 1); -}; - -module.exports = PageRouter; diff --git a/lib/routers/TriplePatternRouter.js b/lib/routers/TriplePatternRouter.js deleted file mode 100644 index 459903fd..00000000 --- a/lib/routers/TriplePatternRouter.js +++ /dev/null @@ -1,48 +0,0 @@ -/*! @license MIT ©2014-2016 Ruben Verborgh, Ghent University - imec */ -/* A TriplePatternRouter routes basic triple patterns */ - -var iriMatcher = /^(][^"<>]*)>?$/; -var literalMatcher = /^("[^]*")(?:|\^\^]+)>?|@[a-z0-9\-]+)$/i; -var prefixedNameMatcher = /^([a-z0-9\-]*):([^\/#:]*)$/i; - -// Creates a new TriplePatternRouter -function TriplePatternRouter(config) { - if (!(this instanceof TriplePatternRouter)) - return new TriplePatternRouter(config); - this._prefixes = config && config.prefixes || {}; -} - -// Extracts triple pattern parameters from the request and add them to the query -TriplePatternRouter.prototype.extractQueryParams = function (request, query) { - var queryString = request.url && request.url.query, match, hasTriplePattern = false; - - // Try to extract a subject IRI - if (queryString.subject && (match = iriMatcher.exec(queryString.subject))) - hasTriplePattern = query.subject = match[1] ? match[2] : this._expandIRI(match[2]); - - // Try to extract a predicate IRI - if (queryString.predicate && (match = iriMatcher.exec(queryString.predicate))) - hasTriplePattern = query.predicate = match[1] ? match[2] : this._expandIRI(match[2]); - - // Try to extract an object - if (queryString.object) { - // The object can be an IRI… - if (match = iriMatcher.exec(queryString.object)) - hasTriplePattern = query.object = match[1] ? match[2] : this._expandIRI(match[2]); - // or the object can be a literal (with a type or language) - else if (match = literalMatcher.exec(queryString.object)) - hasTriplePattern = query.object = match[2] ? match[1] + '^^' + this._expandIRI(match[2]) : match[0]; - } - - // Indicate in the query whether the triple pattern feature was used - if (hasTriplePattern !== false) - (query.features || (query.features = {})).triplePattern = true; -}; - -// Expands a prefixed named into a full IRI -TriplePatternRouter.prototype._expandIRI = function (name) { - var match = prefixedNameMatcher.exec(name), prefix; - return match && (prefix = this._prefixes[match[1]]) ? prefix + match[2] : name; -}; - -module.exports = TriplePatternRouter; diff --git a/lib/views/HtmlView.js b/lib/views/HtmlView.js deleted file mode 100644 index 3e91731b..00000000 --- a/lib/views/HtmlView.js +++ /dev/null @@ -1,46 +0,0 @@ -/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ -/* HtmlView is a base class for views that generate HTML responses. */ - -var View = require('./View'), - qejs = require('qejs'), - q = require('q'), - path = require('path'), - _ = require('lodash'), - N3Util = require('n3').Util; - -// Creates a new HTML view with the given name and settings -function HtmlView(viewName, settings) { - if (!(this instanceof HtmlView)) - return new HtmlView(viewName, settings); - var defaults = { - cache: true, N3Util: N3Util, - assetsPath: '/', baseURL: '/', - title: '', header: settings && settings.title, - }; - View.call(this, viewName, 'text/html', _.defaults({}, settings, defaults)); -} -View.extend(HtmlView); - -// Renders the template with the given name to the response -HtmlView.prototype._renderTemplate = function (templateName, options, request, response, done) { - // Initialize all view extensions - var extensions = options.extensions || (options.extensions = {}); - for (var extension in extensions) { - if (!extensions[extension]) - extensions[extension] = this._renderViewExtensionContents(extension, options, request, response); - } - - // Render the template with its options - qejs.renderFile(path.join(__dirname, templateName + '.html'), options) - .then(function (html) { response.write(html); done(); }) - .fail(function (error) { done(error); }); -}; - -// Renders the view extensions to a string, returned through a promise -HtmlView.prototype._renderViewExtensionContents = function (name, options, request, response) { - var buffer = '', writer = { write: function (data) { buffer += data; }, end: _.noop }; - return q.ninvoke(this, '_renderViewExtensions', name, options, request, writer) - .then(function () { return buffer; }); -}; - -module.exports = HtmlView; diff --git a/lib/views/RdfView.js b/lib/views/RdfView.js deleted file mode 100644 index 5d38155e..00000000 --- a/lib/views/RdfView.js +++ /dev/null @@ -1,146 +0,0 @@ -/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ -/* HtmlView is a base class for views that generate RDF responses. */ - -var View = require('./View'), - N3 = require('n3'), - jsonld = require('jsonld'), - _ = require('lodash'); - -var dcTerms = 'http://purl.org/dc/terms/', - rdf = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', - hydra = 'http://www.w3.org/ns/hydra/core#', - voID = 'http://rdfs.org/ns/void#'; - -var primaryTopic = 'http://xmlns.com/foaf/0.1/primaryTopic'; - -var contentTypes = 'application/trig;q=0.9,application/n-quads;q=0.7,' + - 'application/ld+json;q=0.8,application/json;q=0.8,' + - 'text/turtle;q=0.6,application/n-triples;q=0.5,text/n3;q=0.6'; - -// Creates a new RDF view with the given name and settings -function RdfView(viewName, settings) { - if (!(this instanceof RdfView)) - return new RdfView(viewName, settings); - View.call(this, viewName, contentTypes, settings); -} -View.extend(RdfView); - -// Renders the view with the given settings to the response -RdfView.prototype._render = function (settings, request, response, done) { - // Add generic writer settings - settings.fragmentUrl = settings.fragment && settings.fragment.url || ''; - settings.metadataGraph = settings.fragmentUrl + '#metadata'; - settings.contentType = response.getHeader('Content-Type'); - - // Write the triples with a content-type-specific writer - var self = this, - writer = /json/.test(settings.contentType) ? this._createJsonLdWriter(settings, response, done) - : this._createN3Writer(settings, response, done); - settings.writer = writer; - function main() { self._generateRdf(settings, writer.data, writer.meta, after); } - function after() { self._renderViewExtensions('After', settings, request, response, writer.end); } - function before() { self._renderViewExtensions('Before', settings, request, response, main); } - before(); -}; - -// Generates triples and quads by sending them to the data and/or metadata callbacks -RdfView.prototype._generateRdf = function (settings, data, metadata, done) { - throw new Error('The _generateRdf method is not yet implemented.'); -}; - -// Renders the specified view extension -RdfView.prototype._renderViewExtension = function (extension, options, request, response, done) { - // only view extensions that generate triples are supported - if (extension._generateRdf) - extension._generateRdf(options, options.writer.data, options.writer.meta, done); -}; - -// Adds details about the datasources -RdfView.prototype._addDatasources = function (settings, data, metadata) { - var datasources = settings.datasources; - for (var datasourceName in datasources) { - var datasource = datasources[datasourceName]; - metadata(datasource.url, rdf + 'type', voID + 'Dataset'); - metadata(datasource.url, rdf + 'type', hydra + 'Collection'); - metadata(datasource.url, dcTerms + 'title', '"' + datasource.title + '"'); - } -}; - -// Creates a writer for Turtle/N-Triples/TriG/N-Quads -RdfView.prototype._createN3Writer = function (settings, response, done) { - var writer = new N3.Writer({ format: settings.contentType, prefixes: settings.prefixes }), - supportsGraphs = /trig|quad/.test(settings.contentType), metadataGraph; - return { - // Adds the data triple to the output - data: function (s, p, o, g) { - writer.addTriple(s, p, o, supportsGraphs ? g : null); - }, - // Adds the metadata triple to the output - meta: function (s, p, o) { - // Relate the metadata graph to the data - if (supportsGraphs && !metadataGraph) { - metadataGraph = settings.metadataGraph; - writer.addTriple(metadataGraph, primaryTopic, settings.fragmentUrl, metadataGraph); - } - // Write the triple - if (s && p && o && !N3.Util.isLiteral(s)) - writer.addTriple(s, p, o, metadataGraph); - }, - // Ends the output and flushes the stream - end: function () { - writer.end(function (error, output) { - response.write(error ? '' : output); - done(); - }); - }, - }; -}; - -// Creates a writer for JSON-LD -RdfView.prototype._createJsonLdWriter = function (settings, response, done) { - // Initialize triples, prefixes, and document base - var quads = { '@default': [] }, metadata = quads[settings.metadataGraph] = [], - prefixes = settings.prefixes || {}, context = _.omit(prefixes, ''), base = prefixes['']; - base && (context['@base'] = base); - return { - // Adds the data triple to the output - data: function (s, p, o, g) { - if (!p) g = s.graph, o = s.object, p = s.predicate, s = s.subject; - if (!g) g = '@default'; - var graph = quads[g] || (quads[g] = []); - graph.push(toJsonLdTriple(s, p, o)); - }, - // Adds the metadata triple to the output - meta: function (s, p, o) { - if (s && p && o && !N3.Util.isLiteral(s)) - metadata.push(toJsonLdTriple(s, p, o)); - }, - // Ends the output and flushes the stream - end: function () { - jsonld.fromRDF(quads, { format: false, useNativeTypes: true }, - function (error, json) { - jsonld.compact(error ? {} : json, context, function (error, compacted) { - response.write(JSON.stringify(compacted, null, ' ') + '\n'); - done(error); - }); - }); - }, - }; -}; - -// Converts a triple to the JSON-LD library representation -function toJsonLdTriple(subject, predicate, object) { - return { - subject: { value: subject, type: subject[0] !== '_' ? 'IRI' : 'blank node' }, - predicate: { value: predicate, type: predicate[0] !== '_' ? 'IRI' : 'blank node' }, - object: !N3.Util.isLiteral(object) ? - { value: object, type: object[0] !== '_' ? 'IRI' : 'blank node' } : - { - value: N3.Util.getLiteralValue(object), - datatype: N3.Util.getLiteralType(object), - language: N3.Util.getLiteralLanguage(object), - }, - }; -} - -module.exports = RdfView; diff --git a/lib/views/View.js b/lib/views/View.js deleted file mode 100644 index 2ffa8f1a..00000000 --- a/lib/views/View.js +++ /dev/null @@ -1,97 +0,0 @@ -/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ -/* View is a base class for objects that generate server responses. */ - -var _ = require('lodash'); - -// Creates a view with the given name -function View(viewName, contentTypes, defaults) { - if (!(this instanceof View)) - return new View(viewName, contentTypes, defaults); - this.name = viewName || ''; - this._parseContentTypes(contentTypes); - this._defaults = defaults || {}; -} - -// Makes View the prototype of the given class -View.extend = function extend(child) { - child.prototype = Object.create(this.prototype); - child.prototype.constructor = child; - child.extend = extend; -}; - -// Parses a string of content types into an array of objects -// i.e., 'a/b,q=0.7' => [{ type: 'a/b', responseType: 'a/b;charset=utf-8', quality: 0.7 }] -// The "type" represents the MIME type, -// whereas "responseType" contains the value of the Content-Type header with encoding. -View.prototype._parseContentTypes = function (contentTypes) { - var matcher = this._supportedContentTypeMatcher = Object.create(null); - if (typeof contentTypes === 'string') { - contentTypes = contentTypes.split(',').map(function (typeString) { - var contentType = typeString.match(/[^;,]*/)[0], - responseType = contentType + ';charset=utf-8', - quality = typeString.match(/;q=([0-9.]+)/); - matcher[contentType] = matcher[responseType] = true; - return { - type: contentType, - responseType: responseType, - quality: quality ? Math.min(Math.max(parseFloat(quality[1]), 0.0), 1.0) : 1.0, - }; - }); - } - this.supportedContentTypes = contentTypes || []; -}; - -// Indicates whether the view supports the given content type -View.prototype.supportsContentType = function (contentType) { - return this._supportedContentTypeMatcher[contentType]; -}; - -// Renders the view with the given options to the response -View.prototype.render = function (options, request, response, done) { - // Initialize view-specific settings - var settings = _.defaults({}, options, this._defaults); - if (!settings.contentType) - settings.contentType = response.getHeader('Content-Type'); - // Render the view and end the response when done - this._render(settings, request, response, function (error) { - if (error) - response.emit('error', error); - response.end(); - done && done(); - }); -}; - -// Gets extensions with the given name for this view -View.prototype._getViewExtensions = function (name, contentType) { - var extensions = this._defaults.views ? this._defaults.views.getViews(this.name + ':' + name) : []; - if (extensions.length) { - extensions = extensions.filter(function (extension) { - return extension.supportsContentType(contentType); - }); - } - return extensions; -}; - -// Renders the extensions with the given name for this view -View.prototype._renderViewExtensions = function (name, options, request, response, done) { - var self = this, extensions = this._getViewExtensions(name, options.contentType), i = 0; - (function next() { - if (i < extensions.length) - self._renderViewExtension(extensions[i++], options, request, response, next); - else - done(); - })(); -}; - -// Renders the specified view extension -View.prototype._renderViewExtension = function (extension, options, request, response, done) { - extension.render(options, request, response, done); -}; - -// Renders the view with the given settings to the response -// (settings combines the view defaults with instance-specific options) -View.prototype._render = function (settings, request, response, done) { - throw new Error('The _render method is not yet implemented.'); -}; - -module.exports = View; diff --git a/lib/views/ViewCollection.js b/lib/views/ViewCollection.js deleted file mode 100644 index a2fa7ef9..00000000 --- a/lib/views/ViewCollection.js +++ /dev/null @@ -1,61 +0,0 @@ -/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ -/* - A ViewCollection provides access to content-negotiated views by name. - - It can hold multiple views with the same name. - `matchViews` performs content negotiation to find the most appropriate view for a response. - `getViews` returns all views with a given name. -*/ - -var _ = require('lodash'), - negotiate = require('negotiate'), - Util = require('../Util'); - -var ViewCollectionError = ViewCollection.ViewCollectionError = - Util.createErrorType('ViewCollectionError'); - -// Creates a new ViewCollection -function ViewCollection(views) { - if (!(this instanceof ViewCollection)) - return new ViewCollection(views); - this._views = {}; // Views keyed by name - this._viewMatchers = {}; // Views matchers keyed by name; each one matches one content type - views && this.addViews(views); -} - -// Adds the given view to the collection -ViewCollection.prototype.addView = function (view) { - // Add the view to the list per type - (this._views[view.name] || (this._views[view.name] = [])).push(view); - // Add a match entry for each content type supported by the view - var matchers = this._viewMatchers[view.name] || (this._viewMatchers[view.name] = []); - view.supportedContentTypes.forEach(function (contentType) { - matchers.push(_.extend({ view: view }, contentType)); - }); -}; - -// Adds the given views to the collection -ViewCollection.prototype.addViews = function (views) { - for (var i = 0; i < views.length; i++) - this.addView(views[i]); -}; - -// Gets all views with the given name -ViewCollection.prototype.getViews = function (name) { - return this._views[name] || []; -}; - -// Gets the best match for views with the given name that accommodate the request -ViewCollection.prototype.matchView = function (name, request) { - // Retrieve the views with the given name - var viewList = this._viewMatchers[name]; - if (!viewList || !viewList.length) - throw new ViewCollectionError('No view named ' + name + ' found.'); - // Negotiate the view best matching the request's requirements - var viewDetails = negotiate.choose(viewList, request)[0]; - if (!viewDetails) - throw new ViewCollectionError('No matching view named ' + name + ' found.'); - return viewDetails; -}; - -module.exports = ViewCollection; diff --git a/lib/views/error/ErrorHtmlView.js b/lib/views/error/ErrorHtmlView.js deleted file mode 100644 index 8eecbdb2..00000000 --- a/lib/views/error/ErrorHtmlView.js +++ /dev/null @@ -1,19 +0,0 @@ -/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ -/* An ErrorRdfView represents a 500 response in HTML. */ - -var HtmlView = require('../HtmlView'); - -// Creates a new ErrorHtmlView -function ErrorHtmlView(settings) { - if (!(this instanceof ErrorHtmlView)) - return new ErrorHtmlView(settings); - HtmlView.call(this, 'Error', settings); -} -HtmlView.extend(ErrorHtmlView); - -// Renders the view with the given settings to the response -ErrorHtmlView.prototype._render = function (settings, request, response, done) { - this._renderTemplate('error/error', settings, request, response, done); -}; - -module.exports = ErrorHtmlView; diff --git a/lib/views/error/ErrorRdfView.js b/lib/views/error/ErrorRdfView.js deleted file mode 100644 index 5fa4e8c3..00000000 --- a/lib/views/error/ErrorRdfView.js +++ /dev/null @@ -1,20 +0,0 @@ -/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ -/* An ErrorRdfView represents a 500 response in RDF. */ - -var RdfView = require('../RdfView'); - -// Creates a new ErrorRdfView -function ErrorRdfView(settings) { - if (!(this instanceof ErrorRdfView)) - return new ErrorRdfView(settings); - RdfView.call(this, 'Error', settings); -} -RdfView.extend(ErrorRdfView); - -// Generates triples and quads by sending them to the data and/or metadata callbacks -ErrorRdfView.prototype._generateRdf = function (settings, data, metadata, done) { - this._addDatasources(settings, data, metadata); - done(); -}; - -module.exports = ErrorRdfView; diff --git a/lib/views/forbidden/ForbiddenHtmlView.js b/lib/views/forbidden/ForbiddenHtmlView.js deleted file mode 100644 index f8a1db5a..00000000 --- a/lib/views/forbidden/ForbiddenHtmlView.js +++ /dev/null @@ -1,19 +0,0 @@ -/*! @license MIT ©2015-2016 Miel Vander Sande, Ghent University - imec */ -/* A ForbiddenHtmlView represents a 401 response in HTML. */ - -var HtmlView = require('../HtmlView'); - -// Creates a new ForbiddenHtmlView -function ForbiddenHtmlView(settings) { - if (!(this instanceof ForbiddenHtmlView)) - return new ForbiddenHtmlView(settings); - HtmlView.call(this, 'Forbidden', settings); -} -HtmlView.extend(ForbiddenHtmlView); - -// Renders the view with the given settings to the response -ForbiddenHtmlView.prototype._render = function (settings, request, response, done) { - this._renderTemplate('forbidden/forbidden', settings, request, response, done); -}; - -module.exports = ForbiddenHtmlView; diff --git a/lib/views/memento/TriplePatternFragmentsHtmlView-Memento.js b/lib/views/memento/TriplePatternFragmentsHtmlView-Memento.js deleted file mode 100644 index 0bb6b693..00000000 --- a/lib/views/memento/TriplePatternFragmentsHtmlView-Memento.js +++ /dev/null @@ -1,24 +0,0 @@ -/*! @license MIT ©2016 Ruben Verborgh, Ghent University - imec */ -/* A MementoHtmlViewExtension extends the Triple Pattern Fragments HTML view with Memento details. */ - -var HtmlView = require('../HtmlView'); - -// Creates a new MementoHtmlViewExtension -function MementoHtmlViewExtension(settings) { - if (!(this instanceof MementoHtmlViewExtension)) - return new MementoHtmlViewExtension(settings); - HtmlView.call(this, 'TriplePatternFragments:Before', settings); -} -HtmlView.extend(MementoHtmlViewExtension); - -// Renders the view with the given settings to the response -MementoHtmlViewExtension.prototype._render = function (settings, request, response, done) { - if (!settings.datasource.memento) - return done(); - this._renderTemplate('memento/memento-details', { - start: settings.datasource.memento.interval[0], - end: settings.datasource.memento.interval[1], - }, request, response, done); -}; - -module.exports = MementoHtmlViewExtension; diff --git a/lib/views/notfound/NotFoundHtmlView.js b/lib/views/notfound/NotFoundHtmlView.js deleted file mode 100644 index 1ade299f..00000000 --- a/lib/views/notfound/NotFoundHtmlView.js +++ /dev/null @@ -1,19 +0,0 @@ -/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ -/* A NotFoundRdfView represents a 404 response in HTML. */ - -var HtmlView = require('../HtmlView'); - -// Creates a new NotFoundHtmlView -function NotFoundHtmlView(settings) { - if (!(this instanceof NotFoundHtmlView)) - return new NotFoundHtmlView(settings); - HtmlView.call(this, 'NotFound', settings); -} -HtmlView.extend(NotFoundHtmlView); - -// Renders the view with the given settings to the response -NotFoundHtmlView.prototype._render = function (settings, request, response, done) { - this._renderTemplate('notfound/notfound', settings, request, response, done); -}; - -module.exports = NotFoundHtmlView; diff --git a/lib/views/notfound/NotFoundRdfView.js b/lib/views/notfound/NotFoundRdfView.js deleted file mode 100644 index 4dd3e45a..00000000 --- a/lib/views/notfound/NotFoundRdfView.js +++ /dev/null @@ -1,20 +0,0 @@ -/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ -/* A NotFoundRdfView represents a 404 response in RDF. */ - -var RdfView = require('../RdfView'); - -// Creates a new NotFoundRdfView -function NotFoundRdfView(settings) { - if (!(this instanceof NotFoundRdfView)) - return new NotFoundRdfView(settings); - RdfView.call(this, 'NotFound', settings); -} -RdfView.extend(NotFoundRdfView); - -// Generates triples and quads by sending them to the data and/or metadata callbacks -NotFoundRdfView.prototype._generateRdf = function (settings, data, metadata, done) { - this._addDatasources(settings, data, metadata); - done(); -}; - -module.exports = NotFoundRdfView; diff --git a/lib/views/summary/SummaryRdfView.js b/lib/views/summary/SummaryRdfView.js deleted file mode 100644 index 25c00325..00000000 --- a/lib/views/summary/SummaryRdfView.js +++ /dev/null @@ -1,21 +0,0 @@ -/*! @license MIT ©2015-2016 Miel Vander Sande, Ghent University - imec */ -/* A SummaryRdfView represents a data summary in RDF. */ - -var RdfView = require('../RdfView'); - -// Creates a new SummaryRdfView -function SummaryRdfView(settings) { - if (!(this instanceof SummaryRdfView)) - return new SummaryRdfView(settings); - RdfView.call(this, 'Summary', settings); -} -RdfView.extend(SummaryRdfView); - -// Generates triples and quads by sending them to the data and/or metadata callbacks -SummaryRdfView.prototype._generateRdf = function (settings, data, metadata, done) { - // Add summary triples - settings.results.on('data', data); - settings.results.on('end', done); -}; - -module.exports = SummaryRdfView; diff --git a/lib/views/summary/TriplePattternFragmentsHtmlView-Summary.js b/lib/views/summary/TriplePattternFragmentsHtmlView-Summary.js deleted file mode 100644 index 71cf5bde..00000000 --- a/lib/views/summary/TriplePattternFragmentsHtmlView-Summary.js +++ /dev/null @@ -1,29 +0,0 @@ -/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ -/* A SummaryHtmlViewExtension extends the Triple Pattern Fragments RDF view with a summary link. */ - -var HtmlView = require('../HtmlView'); - -// Creates a new SummaryHtmlViewExtension -function SummaryHtmlViewExtension(settings) { - if (!(this instanceof SummaryHtmlViewExtension)) - return new SummaryHtmlViewExtension(settings); - HtmlView.call(this, 'TriplePatternFragments:Before', settings); -} -HtmlView.extend(SummaryHtmlViewExtension); - -// Renders the view with the given settings to the response -SummaryHtmlViewExtension.prototype._render = function (settings, request, response, done) { - // If summaries are enabled, connect the datasource to its summary - // TODO: summary should be of/off per dataset - if (settings.summaries && settings.summaries.enabled) { - // TODO: summary URL should be generated by router - settings.summary = { - url: settings.baseURL + 'summaries/' + encodeURIComponent(settings.query.datasource), - }; - this._renderTemplate('summary/summary-link', settings, request, response, done); - } - else - done(); -}; - -module.exports = SummaryHtmlViewExtension; diff --git a/lib/views/summary/TriplePattternFragmentsRdfView-Summary.js b/lib/views/summary/TriplePattternFragmentsRdfView-Summary.js deleted file mode 100644 index c0a59bcb..00000000 --- a/lib/views/summary/TriplePattternFragmentsRdfView-Summary.js +++ /dev/null @@ -1,28 +0,0 @@ -/*! @license MIT ©2015-2016 Miel Vander Sande, Ghent University - imec */ -/* A SummaryRdfViewExtension extends the Triple Pattern Fragments RDF view with a summary link. */ - -var RdfView = require('../RdfView'); - -var ds = 'http://semweb.mmlab.be/ns/datasummaries#'; - -// Creates a new SummaryRdfViewExtension -function SummaryRdfViewExtension(settings) { - if (!(this instanceof SummaryRdfViewExtension)) - return new SummaryRdfViewExtension(settings); - RdfView.call(this, 'TriplePatternFragments:After', settings); -} -RdfView.extend(SummaryRdfViewExtension); - -// Generates triples and quads by sending them to the data and/or metadata callbacks -SummaryRdfViewExtension.prototype._generateRdf = function (settings, data, metadata, done) { - // If summaries are enabled, connect the datasource to its summary - // TODO: summary should be of/off per dataset - if (settings.summaries && settings.summaries.enabled) { - // TODO: summary URL should be generated by router - metadata(settings.datasource.url, ds + 'hasDatasetSummary', - settings.baseURL + 'summaries/' + encodeURIComponent(settings.query.datasource)); - } - done(); -}; - -module.exports = SummaryRdfViewExtension; diff --git a/lib/views/triplepatternfragments/TriplePatternFragmentsHtmlView.js b/lib/views/triplepatternfragments/TriplePatternFragmentsHtmlView.js deleted file mode 100644 index 8445f5e0..00000000 --- a/lib/views/triplepatternfragments/TriplePatternFragmentsHtmlView.js +++ /dev/null @@ -1,33 +0,0 @@ -/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ -/* A TriplePatternFragmentsRdfView represents a Triple Pattern Fragment in HTML. */ - -var HtmlView = require('../HtmlView'); - -// Creates a new TriplePatternFragmentsHtmlView -function TriplePatternFragmentsHtmlView(settings) { - if (!(this instanceof TriplePatternFragmentsHtmlView)) - return new TriplePatternFragmentsHtmlView(settings); - HtmlView.call(this, 'TriplePatternFragments', settings); -} -HtmlView.extend(TriplePatternFragmentsHtmlView); - -// Renders the view with the given settings to the response -TriplePatternFragmentsHtmlView.prototype._render = function (settings, request, response, done) { - // Read the data and metadata - var self = this, triples = settings.triples = [], results = settings.results; - results.on('data', function (triple) { triples.push(triple); }); - results.on('end', function () { settings.metadata && renderHtml(); }); - results.getProperty('metadata', function (metadata) { - settings.metadata = metadata; - results.ended && renderHtml(); - }); - - // Generates the HTML after the data and metadata have been retrieved - function renderHtml() { - var template = settings.datasource.role === 'index' ? 'index' : 'datasource'; - settings.extensions = { Before: null, After: null }; - self._renderTemplate('triplepatternfragments/' + template, settings, request, response, done); - } -}; - -module.exports = TriplePatternFragmentsHtmlView; diff --git a/lib/views/triplepatternfragments/TriplePatternFragmentsRdfView.js b/lib/views/triplepatternfragments/TriplePatternFragmentsRdfView.js deleted file mode 100644 index 61214b98..00000000 --- a/lib/views/triplepatternfragments/TriplePatternFragmentsRdfView.js +++ /dev/null @@ -1,82 +0,0 @@ -/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ -/* A TriplePatternFragmentsRdfView represents a Triple Pattern Fragment in RDF. */ - -var RdfView = require('../RdfView'); - -var dcTerms = 'http://purl.org/dc/terms/', - rdf = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', - xsd = 'http://www.w3.org/2001/XMLSchema#', - hydra = 'http://www.w3.org/ns/hydra/core#', - voID = 'http://rdfs.org/ns/void#'; - -// Creates a new TriplePatternFragmentsRdfView -function TriplePatternFragmentsRdfView(settings) { - if (!(this instanceof TriplePatternFragmentsRdfView)) - return new TriplePatternFragmentsRdfView(settings); - RdfView.call(this, 'TriplePatternFragments', settings); -} -RdfView.extend(TriplePatternFragmentsRdfView); - -// Generates triples and quads by sending them to the data and/or metadata callbacks -TriplePatternFragmentsRdfView.prototype._generateRdf = function (settings, data, metadata, done) { - var datasource = settings.datasource, fragment = settings.fragment, query = settings.query, - results = settings.results, metadataDone = false; - - // Add data source metadata - metadata(datasource.index, hydra + 'member', datasource.url); - metadata(datasource.url, rdf + 'type', voID + 'Dataset'); - metadata(datasource.url, rdf + 'type', hydra + 'Collection'); - metadata(datasource.url, voID + 'subset', fragment.pageUrl); - if (fragment.url !== fragment.pageUrl) - metadata(datasource.url, voID + 'subset', fragment.url); - - // Add data source controls - metadata(datasource.url, hydra + 'search', '_:triplePattern'); - metadata('_:triplePattern', hydra + 'template', '"' + datasource.templateUrl + '"'); - metadata('_:triplePattern', hydra + 'variableRepresentation', hydra + 'ExplicitRepresentation'); - metadata('_:triplePattern', hydra + 'mapping', '_:subject'); - metadata('_:triplePattern', hydra + 'mapping', '_:predicate'); - metadata('_:triplePattern', hydra + 'mapping', '_:object'); - metadata('_:subject', hydra + 'variable', '"subject"'); - metadata('_:subject', hydra + 'property', rdf + 'subject'); - metadata('_:predicate', hydra + 'variable', '"predicate"'); - metadata('_:predicate', hydra + 'property', rdf + 'predicate'); - metadata('_:object', hydra + 'variable', '"object"'); - metadata('_:object', hydra + 'property', rdf + 'object'); - - // Add fragment metadata - results.getProperty('metadata', function (meta) { - // General fragment metadata - metadata(fragment.url, voID + 'subset', fragment.pageUrl); - metadata(fragment.pageUrl, rdf + 'type', hydra + 'PartialCollectionView'); - metadata(fragment.pageUrl, dcTerms + 'title', - '"Linked Data Fragment of ' + (datasource.title || '') + '"@en'); - metadata(fragment.pageUrl, dcTerms + 'description', - '"Triple Pattern Fragment of the \'' + (datasource.title || '') + '\' dataset ' + - 'containing triples matching the pattern ' + query.patternString + '."@en'); - metadata(fragment.pageUrl, dcTerms + 'source', datasource.url); - - // Total pattern matches count - var totalCount = meta.totalCount; - metadata(fragment.pageUrl, hydra + 'totalItems', '"' + totalCount + '"^^' + xsd + 'integer'); - metadata(fragment.pageUrl, voID + 'triples', '"' + totalCount + '"^^' + xsd + 'integer'); - - // Page metadata - metadata(fragment.pageUrl, hydra + 'itemsPerPage', '"' + query.limit + '"^^' + xsd + 'integer'); - metadata(fragment.pageUrl, hydra + 'first', fragment.firstPageUrl); - if (query.offset) - metadata(fragment.pageUrl, hydra + 'previous', fragment.previousPageUrl); - if (totalCount >= query.limit + (query.offset || 0)) - metadata(fragment.pageUrl, hydra + 'next', fragment.nextPageUrl); - - // End if the data was also written - metadataDone = true; - results.ended && done(); - }); - - // Add fragment data - results.on('data', data); - results.on('end', function () { metadataDone && done(); }); -}; - -module.exports = TriplePatternFragmentsRdfView; diff --git a/lib/views/triplepatternfragments/datasource.html b/lib/views/triplepatternfragments/datasource.html deleted file mode 100644 index 0331d415..00000000 --- a/lib/views/triplepatternfragments/datasource.html +++ /dev/null @@ -1,4 +0,0 @@ -<% /* @license MIT ©2013-2016 Ruben Verborgh, Ghent University - imec */ -%> -<% title = datasource.title + ' | ' + title %> -<% inherits('base') %> -<%- render('fragment') %> diff --git a/lib/views/triplepatternfragments/index.html b/lib/views/triplepatternfragments/index.html deleted file mode 100644 index d991fa44..00000000 --- a/lib/views/triplepatternfragments/index.html +++ /dev/null @@ -1,26 +0,0 @@ -<% /* @license MIT ©2013-2016 Ruben Verborgh, Ghent University - imec */ -%> -<% inherits('base') %> -<% -var rdf = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', - rdfs = 'http://www.w3.org/2000/01/rdf-schema#', - dc = 'http://purl.org/dc/terms/', - voID = 'http://rdfs.org/ns/void#'; -%> - -<% if (!query.features.triplePattern) { %> -
-

Available datasets

-

Browse the following datasets as Triple Pattern Fragments:

-
- <% for (var datasourceName in datasources) { - var datasource = datasources[datasourceName]; - if (datasource.role === 'index' || datasource.hide) continue; %> -
<%= datasource.title %>
-
<%= datasource.description || ' ' %>
- <% } %> -
-

The current dataset index contains metadata about these datasets.

-
-<% } %> - -<%- render('fragment') %> diff --git a/module.js b/module.js deleted file mode 100644 index 98f35434..00000000 --- a/module.js +++ /dev/null @@ -1,29 +0,0 @@ -/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ -/* Exports of the ldf-server package for use as a submodule (as opposed to standalone) */ - -var fs = require('fs'), - path = require('path'); - -// Export all modules in the `lib` folder -module.exports = readModules(path.join(__dirname, 'lib')); - -// Reads modules in the given folder and subfolders -function readModules(folder) { - var modules = {}; - fs.readdirSync(folder).forEach(function (name) { - var location = path.join(folder, name), item = fs.statSync(location); - // Add script files by including them - if (item.isFile()) { - var scriptMatch = name.match(/(\w+)\.js$/); - if (scriptMatch) { - try { modules[scriptMatch[1]] = require(location); } - catch (error) { /* ignore modules that cannot be instantiated */ } - } - } - // Add folders by recursing over them - else if (item.isDirectory()) { - modules[name] = readModules(location); - } - }); - return modules; -} diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 11d466df..00000000 --- a/package-lock.json +++ /dev/null @@ -1,2031 +0,0 @@ -{ - "name": "ldf-server", - "version": "2.2.5", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "access-log": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/access-log/-/access-log-0.3.9.tgz", - "integrity": "sha1-AcJpW6fn0y21KI7z8U5k1nGfOtE=", - "optional": true, - "requires": { - "strftime": "~0.6.2" - } - }, - "acorn": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.5.3.tgz", - "integrity": "sha512-jd5MkIUlbbmb07nXH0DT3y7rDVtkzDi4XZOUVWAer8ajmF/DTSSbl5oNFyDOl/OXA33Bl79+ypHhl2pN20VeOQ==", - "dev": true - }, - "acorn-jsx": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", - "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", - "dev": true, - "requires": { - "acorn": "^3.0.4" - }, - "dependencies": { - "acorn": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", - "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", - "dev": true - } - } - }, - "ajv": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.7.0.tgz", - "integrity": "sha512-RZXPviBTtfmtka9n9sy1N5M5b82CbxWIR6HIis4s3WQTXDJamc/0gpCWNGz6EWdWp4DOfjzJfhz/AS9zVPjjWg==", - "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ajv-keywords": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.5.1.tgz", - "integrity": "sha1-MU3QpLM2j609/NxU7eYXG4htrzw=", - "dev": true - }, - "ansi-escapes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", - "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=", - "dev": true - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", - "dev": true, - "requires": { - "array-uniq": "^1.0.1" - } - }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", - "dev": true - }, - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", - "dev": true - }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - }, - "assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true - }, - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", - "dev": true - }, - "asynciterator": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/asynciterator/-/asynciterator-1.1.1.tgz", - "integrity": "sha512-NLWjVfwc/I1k/aS9eLH/zfxnN6Ci/WHJiJCfwHcyLlPUnYIY2WHDMYycBIpobyi3qCVGYdqTY4NV0LFURbqZ/g==", - "requires": { - "immediate": "^3.2.3" - } - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" - }, - "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" - }, - "babel-code-frame": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", - "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "esutils": "^2.0.2", - "js-tokens": "^3.0.2" - } - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true - }, - "buffer-from": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.0.0.tgz", - "integrity": "sha512-83apNb8KK0Se60UE1+4Ukbe3HbfELJ6UlI4ldtOGs7So4KD26orJM8hIY9lxdzP+UpItH1Yh/Y8GUvNFWFFRxA==", - "dev": true - }, - "caller-path": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", - "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", - "dev": true, - "requires": { - "callsites": "^0.2.0" - } - }, - "callsites": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", - "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", - "dev": true - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" - }, - "chai": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-3.5.0.tgz", - "integrity": "sha1-TQJjewZ/6Vi9v906QOxW/vc3Mkc=", - "dev": true, - "requires": { - "assertion-error": "^1.0.1", - "deep-eql": "^0.1.3", - "type-detect": "^1.0.0" - } - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "circular-json": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", - "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", - "dev": true - }, - "cli-cursor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", - "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", - "dev": true, - "requires": { - "restore-cursor": "^1.0.1" - } - }, - "cli-width": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", - "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", - "dev": true - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", - "dev": true - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, - "combined-stream": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", - "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "commander": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", - "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", - "dev": true - }, - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "cookiejar": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.1.tgz", - "integrity": "sha1-Qa1XsbVVlR7BcUEqgZQrHoIA00o=", - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", - "dev": true, - "requires": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "d": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", - "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", - "dev": true, - "requires": { - "es5-ext": "^0.10.9" - } - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { - "assert-plus": "^1.0.0" - } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-eql": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", - "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=", - "dev": true, - "requires": { - "type-detect": "0.1.1" - }, - "dependencies": { - "type-detect": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", - "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=", - "dev": true - } - } - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true - }, - "del": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", - "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", - "dev": true, - "requires": { - "globby": "^5.0.0", - "is-path-cwd": "^1.0.0", - "is-path-in-cwd": "^1.0.0", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "rimraf": "^2.2.8" - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" - }, - "diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", - "dev": true - }, - "doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "es5-ext": { - "version": "0.10.41", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.41.tgz", - "integrity": "sha512-MYK02wXfwTMie5TEJWPolgOsXEmz7wKCQaGzgmRjZOoV6VLG8I5dSv2bn6AOClXhK64gnSQTQ9W9MKvx87J4gw==", - "dev": true, - "requires": { - "es6-iterator": "~2.0.3", - "es6-symbol": "~3.1.1", - "next-tick": "1" - } - }, - "es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" - } - }, - "es6-map": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", - "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "~0.10.14", - "es6-iterator": "~2.0.1", - "es6-set": "~0.1.5", - "es6-symbol": "~3.1.1", - "event-emitter": "~0.3.5" - } - }, - "es6-promise": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-2.3.0.tgz", - "integrity": "sha1-lu258v2wGZWCKyY92KratnSBgbw=" - }, - "es6-set": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", - "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "~0.10.14", - "es6-iterator": "~2.0.1", - "es6-symbol": "3.1.1", - "event-emitter": "~0.3.5" - } - }, - "es6-symbol": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", - "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "~0.10.14" - } - }, - "es6-weak-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", - "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "^0.10.14", - "es6-iterator": "^2.0.1", - "es6-symbol": "^3.1.1" - } - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "escope": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz", - "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=", - "dev": true, - "requires": { - "es6-map": "^0.1.3", - "es6-weak-map": "^2.0.1", - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - }, - "eslint": { - "version": "3.19.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-3.19.0.tgz", - "integrity": "sha1-yPxiAcf0DdCJQbh8CFdnOGpnmsw=", - "dev": true, - "requires": { - "babel-code-frame": "^6.16.0", - "chalk": "^1.1.3", - "concat-stream": "^1.5.2", - "debug": "^2.1.1", - "doctrine": "^2.0.0", - "escope": "^3.6.0", - "espree": "^3.4.0", - "esquery": "^1.0.0", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "file-entry-cache": "^2.0.0", - "glob": "^7.0.3", - "globals": "^9.14.0", - "ignore": "^3.2.0", - "imurmurhash": "^0.1.4", - "inquirer": "^0.12.0", - "is-my-json-valid": "^2.10.0", - "is-resolvable": "^1.0.0", - "js-yaml": "^3.5.1", - "json-stable-stringify": "^1.0.0", - "levn": "^0.3.0", - "lodash": "^4.0.0", - "mkdirp": "^0.5.0", - "natural-compare": "^1.4.0", - "optionator": "^0.8.2", - "path-is-inside": "^1.0.1", - "pluralize": "^1.2.1", - "progress": "^1.1.8", - "require-uncached": "^1.0.2", - "shelljs": "^0.7.5", - "strip-bom": "^3.0.0", - "strip-json-comments": "~2.0.1", - "table": "^3.7.8", - "text-table": "~0.2.0", - "user-home": "^2.0.0" - }, - "dependencies": { - "lodash": { - "version": "4.17.5", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz", - "integrity": "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==", - "dev": true - } - } - }, - "espree": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz", - "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==", - "dev": true, - "requires": { - "acorn": "^5.5.0", - "acorn-jsx": "^3.0.0" - } - }, - "esprima": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", - "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", - "dev": true - }, - "esquery": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.0.tgz", - "integrity": "sha1-z7qLV9f7qT8XKYqKAGoEzaE9gPo=", - "dev": true, - "requires": { - "estraverse": "^4.0.0" - } - }, - "esrecurse": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", - "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", - "dev": true, - "requires": { - "estraverse": "^4.1.0" - } - }, - "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", - "dev": true - }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true - }, - "event-emitter": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", - "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "~0.10.14" - } - }, - "exit-hook": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", - "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", - "dev": true - }, - "extend": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", - "dev": true - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" - }, - "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" - }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "figures": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", - "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5", - "object-assign": "^4.1.0" - } - }, - "file-entry-cache": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", - "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", - "dev": true, - "requires": { - "flat-cache": "^1.2.1", - "object-assign": "^4.0.1" - } - }, - "flat-cache": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.0.tgz", - "integrity": "sha1-0wMLMrOBVPTjt+nHCfSQ9++XxIE=", - "dev": true, - "requires": { - "circular-json": "^0.3.1", - "del": "^2.0.2", - "graceful-fs": "^4.1.2", - "write": "^0.2.1" - } - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "formatio": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/formatio/-/formatio-1.1.1.tgz", - "integrity": "sha1-XtPM1jZVEJc4NGXZlhmRAOhhYek=", - "dev": true, - "requires": { - "samsam": "~1.1" - } - }, - "formidable": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz", - "integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg==", - "dev": true - }, - "forwarded-parse": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.0.tgz", - "integrity": "sha512-as9a7Xelt0CvdUy7/qxrY73dZq2vMx49F556fwjjFrUyzq5uHHfeLgD2cCq/6P4ZvusGZzjD6aL2NdgGdS5Cew==" - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "generate-function": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", - "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=", - "dev": true - }, - "generate-object-property": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", - "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", - "dev": true, - "requires": { - "is-property": "^1.0.0" - } - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "requires": { - "assert-plus": "^1.0.0" - } - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "globals": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", - "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", - "dev": true - }, - "globby": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", - "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", - "dev": true, - "requires": { - "array-union": "^1.0.1", - "arrify": "^1.0.0", - "glob": "^7.0.3", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", - "dev": true - }, - "growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", - "dev": true - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" - }, - "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", - "requires": { - "ajv": "^6.5.5", - "har-schema": "^2.0.0" - } - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "hdt": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/hdt/-/hdt-2.1.3.tgz", - "integrity": "sha512-DIkMZs9tsgYuLgil8y7hvq1k42yAwGygb5BLUgcahPBsPkMhnYoXo4Pd8ruKk39ApPzGR8KBL943k3EUlcbuaQ==", - "optional": true, - "requires": { - "minimist": "^1.1.0", - "n3": "^0.11.2", - "nan": "^2.10.0" - }, - "dependencies": { - "n3": { - "version": "0.11.3", - "resolved": "https://registry.npmjs.org/n3/-/n3-0.11.3.tgz", - "integrity": "sha512-Hk5GSXBeAZrYoqi+NeS/U0H47Hx0Lzj7K6nLWCZpC9E04iUwEwBcrlMb/5foAli7QF4newPNQQQGgM6IAxTxGg==", - "optional": true - } - } - }, - "he": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", - "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", - "dev": true - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "ignore": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.7.tgz", - "integrity": "sha512-YGG3ejvBNHRqu0559EOxxNFihD0AjpvHlC/pdGKd3X3ofe+CoJkYazwNJYTNebqpPKN+VVQbh4ZFn1DivMNuHA==", - "dev": true - }, - "immediate": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.2.3.tgz", - "integrity": "sha1-0UD6j2FGWb1lQSMwl92qwlzdmRw=" - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "inquirer": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-0.12.0.tgz", - "integrity": "sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34=", - "dev": true, - "requires": { - "ansi-escapes": "^1.1.0", - "ansi-regex": "^2.0.0", - "chalk": "^1.0.0", - "cli-cursor": "^1.0.1", - "cli-width": "^2.0.0", - "figures": "^1.3.5", - "lodash": "^4.3.0", - "readline2": "^1.0.1", - "run-async": "^0.1.0", - "rx-lite": "^3.1.2", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.0", - "through": "^2.3.6" - }, - "dependencies": { - "lodash": { - "version": "4.17.5", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz", - "integrity": "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==", - "dev": true - } - } - }, - "interpret": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", - "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "is-my-ip-valid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz", - "integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==", - "dev": true - }, - "is-my-json-valid": { - "version": "2.17.2", - "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.17.2.tgz", - "integrity": "sha512-IBhBslgngMQN8DDSppmgDv7RNrlFotuuDsKcrCP3+HbFaVivIBU7u9oiiErw8sH4ynx3+gOGQ3q2otkgiSi6kg==", - "dev": true, - "requires": { - "generate-function": "^2.0.0", - "generate-object-property": "^1.1.0", - "is-my-ip-valid": "^1.0.0", - "jsonpointer": "^4.0.0", - "xtend": "^4.0.0" - } - }, - "is-path-cwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", - "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", - "dev": true - }, - "is-path-in-cwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz", - "integrity": "sha1-ZHdYK4IU1gI0YJRWcAO+ip6sBNw=", - "dev": true, - "requires": { - "is-path-inside": "^1.0.0" - } - }, - "is-path-inside": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", - "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", - "dev": true, - "requires": { - "path-is-inside": "^1.0.1" - } - }, - "is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", - "dev": true - }, - "is-resolvable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", - "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", - "dev": true - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "js-tokens": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", - "dev": true - }, - "js-yaml": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.11.0.tgz", - "integrity": "sha512-saJstZWv7oNeOyBh3+Dx1qWzhW0+e6/8eDzo7p5rDFqxntSztloLtuKu+Ejhtq82jsilwOIZYsCz+lIjthg1Hw==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "json-stable-stringify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", - "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", - "dev": true, - "requires": { - "jsonify": "~0.0.0" - } - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "jsonify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", - "dev": true - }, - "jsonld": { - "version": "0.4.12", - "resolved": "https://registry.npmjs.org/jsonld/-/jsonld-0.4.12.tgz", - "integrity": "sha1-oC8gXVNBQU3xtthBTxuWenEgc+g=", - "requires": { - "es6-promise": "^2.0.0", - "pkginfo": "~0.4.0", - "request": "^2.61.0", - "xmldom": "0.1.19" - } - }, - "jsonpointer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", - "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", - "dev": true - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "lodash": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", - "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=" - }, - "lolex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-1.3.2.tgz", - "integrity": "sha1-fD2mL/yzDw9agKJWbKJORdigHzE=", - "dev": true - }, - "lru-cache": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.2.tgz", - "integrity": "sha512-wgeVXhrDwAWnIF/yZARsFnMBtdFXOg1b8RIrhilp+0iDYN4mdQcNZElDZ0e4B64BhaxeQ5zN7PMyvu7we1kPeQ==", - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", - "dev": true - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" - }, - "mime-db": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", - "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" - }, - "mime-types": { - "version": "2.1.18", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", - "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", - "requires": { - "mime-db": "~1.33.0" - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "optional": true - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - }, - "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - } - } - }, - "mocha": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", - "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", - "dev": true, - "requires": { - "browser-stdout": "1.3.1", - "commander": "2.15.1", - "debug": "3.1.0", - "diff": "3.5.0", - "escape-string-regexp": "1.0.5", - "glob": "7.1.2", - "growl": "1.10.5", - "he": "1.1.1", - "minimatch": "3.0.4", - "mkdirp": "0.5.1", - "supports-color": "5.4.0" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "mute-stream": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz", - "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=", - "dev": true - }, - "n3": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/n3/-/n3-0.7.0.tgz", - "integrity": "sha1-li0YYrVELI0LlAV1x3Av+aP/GcU=" - }, - "nan": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.11.0.tgz", - "integrity": "sha512-F4miItu2rGnV2ySkXOQoA8FKz/SR2Q2sWP0sbTxNxz/tuokeC8WxOhPMcwi0qIyGtVn/rrSeLbvVkznqCdwYnw==", - "optional": true - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true - }, - "negotiate": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/negotiate/-/negotiate-1.0.1.tgz", - "integrity": "sha1-NayLVnL3sF+qEL8CYTQusRIDcP0=" - }, - "next-tick": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", - "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", - "dev": true - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "onetime": { - "version": "1.1.0", - "resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", - "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", - "dev": true - }, - "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", - "dev": true, - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.4", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "wordwrap": "~1.0.0" - } - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true - }, - "os-shim": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/os-shim/-/os-shim-0.1.3.tgz", - "integrity": "sha1-a2LDeRz3kJ6jXtRuF2WLtBfLORc=", - "dev": true - }, - "parse-cache-control": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", - "integrity": "sha1-juqz5U+laSD+Fro493+iGqzC104=", - "optional": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", - "dev": true - }, - "path-parse": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", - "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", - "dev": true - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", - "dev": true - }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "dev": true, - "requires": { - "pinkie": "^2.0.0" - } - }, - "pkginfo": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/pkginfo/-/pkginfo-0.4.1.tgz", - "integrity": "sha1-tUGO8EOd5UJfxJlQQtztFPsqhP8=" - }, - "pluralize": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz", - "integrity": "sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU=", - "dev": true - }, - "pre-commit": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/pre-commit/-/pre-commit-1.2.2.tgz", - "integrity": "sha1-287g7p3nI15X95xW186UZBpp7sY=", - "dev": true, - "requires": { - "cross-spawn": "^5.0.1", - "spawn-sync": "^1.0.15", - "which": "1.2.x" - } - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true - }, - "progress": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", - "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", - "dev": true - }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" - }, - "psl": { - "version": "1.1.31", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", - "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==" - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" - }, - "q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" - }, - "qejs": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/qejs/-/qejs-3.0.5.tgz", - "integrity": "sha1-x46rpc7wX0jeM1PJ0HrWkv+otJI=", - "requires": { - "q": "0.8.x" - }, - "dependencies": { - "q": { - "version": "0.8.12", - "resolved": "https://registry.npmjs.org/q/-/q-0.8.12.tgz", - "integrity": "sha1-kWKpHhGBnEvNp9oVz1/vqtB3iCM=" - } - } - }, - "qs": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==", - "dev": true - }, - "readable-stream": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.5.tgz", - "integrity": "sha512-tK0yDhrkygt/knjowCUiWP9YdV7c5R+8cR0r/kt9ZhBU906Fs6RpQJCEilamRJj1Nx2rWI6LkW9gKqjTkshhEw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.0.3", - "util-deprecate": "~1.0.1" - } - }, - "readline2": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz", - "integrity": "sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "mute-stream": "0.0.5" - } - }, - "rechoir": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", - "dev": true, - "requires": { - "resolve": "^1.1.6" - } - }, - "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.0", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "dependencies": { - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "mime-db": { - "version": "1.37.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", - "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==" - }, - "mime-types": { - "version": "2.1.21", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", - "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", - "requires": { - "mime-db": "~1.37.0" - } - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - } - } - }, - "require-uncached": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", - "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", - "dev": true, - "requires": { - "caller-path": "^0.1.0", - "resolve-from": "^1.0.0" - } - }, - "resolve": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.6.0.tgz", - "integrity": "sha512-mw7JQNu5ExIkcw4LPih0owX/TZXjD/ZUF/ZQ/pDnkw3ZKhDcZZw5klmBlj6gVMwjQ3Pz5Jgu7F3d0jcDVuEWdw==", - "dev": true, - "requires": { - "path-parse": "^1.0.5" - } - }, - "resolve-from": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", - "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", - "dev": true - }, - "restore-cursor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", - "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", - "dev": true, - "requires": { - "exit-hook": "^1.0.0", - "onetime": "^1.0.0" - } - }, - "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", - "dev": true, - "requires": { - "glob": "^7.0.5" - } - }, - "run-async": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz", - "integrity": "sha1-yK1KXhEGYeQCp9IbUw4AnyX444k=", - "dev": true, - "requires": { - "once": "^1.3.0" - } - }, - "rx-lite": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz", - "integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=", - "dev": true - }, - "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "samsam": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.1.2.tgz", - "integrity": "sha1-vsEf3IOp/aBjQBIQ5AF2wwJNFWc=", - "dev": true - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "shelljs": { - "version": "0.7.8", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.8.tgz", - "integrity": "sha1-3svPh0sNHl+3LhSxZKloMEjprLM=", - "dev": true, - "requires": { - "glob": "^7.0.0", - "interpret": "^1.0.0", - "rechoir": "^0.6.2" - } - }, - "sinon": { - "version": "1.17.7", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-1.17.7.tgz", - "integrity": "sha1-RUKk9JugxFwF6y6d2dID4rjv4L8=", - "dev": true, - "requires": { - "formatio": "1.1.1", - "lolex": "1.3.2", - "samsam": "1.1.2", - "util": ">=0.10.3 <1" - } - }, - "sinon-chai": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-2.14.0.tgz", - "integrity": "sha512-9stIF1utB0ywNHNT7RgiXbdmen8QDCRsrTjw+G9TgKt1Yexjiv8TOWZ6WHsTPz57Yky3DIswZvEqX8fpuHNDtQ==", - "dev": true - }, - "slice-ansi": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", - "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", - "dev": true - }, - "spawn-sync": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/spawn-sync/-/spawn-sync-1.0.15.tgz", - "integrity": "sha1-sAeZVX63+wyDdsKdROih6mfldHY=", - "dev": true, - "requires": { - "concat-stream": "^1.4.7", - "os-shim": "^0.1.2" - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - }, - "sshpk": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.0.tgz", - "integrity": "sha512-Zhev35/y7hRMcID/upReIvRse+I9SVhyVre/KTJSJQWMz3C3+G+HpO7m1wK/yckEtujKZ7dS4hkVxAnmHaIGVQ==", - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "strftime": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/strftime/-/strftime-0.6.2.tgz", - "integrity": "sha1-2kwSBzzr7jzWD0ghlAzCexnTSKE=", - "optional": true - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true - }, - "superagent": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-2.3.0.tgz", - "integrity": "sha1-cDUpoHFOV+EjlZ3e+84ZOy5Q0RU=", - "dev": true, - "requires": { - "component-emitter": "^1.2.0", - "cookiejar": "^2.0.6", - "debug": "^2.2.0", - "extend": "^3.0.0", - "form-data": "1.0.0-rc4", - "formidable": "^1.0.17", - "methods": "^1.1.1", - "mime": "^1.3.4", - "qs": "^6.1.0", - "readable-stream": "^2.0.5" - }, - "dependencies": { - "form-data": { - "version": "1.0.0-rc4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.0-rc4.tgz", - "integrity": "sha1-BaxrwiIntD5EYfSIFhVUaZ1Pi14=", - "dev": true, - "requires": { - "async": "^1.5.2", - "combined-stream": "^1.0.5", - "mime-types": "^2.1.10" - } - } - } - }, - "supertest": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-2.0.1.tgz", - "integrity": "sha1-oFgIHXiPFRXUcA11Aogea3WeRM0=", - "dev": true, - "requires": { - "methods": "1.x", - "superagent": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - }, - "table": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/table/-/table-3.8.3.tgz", - "integrity": "sha1-K7xULw/amGGnVdOUf+/Ys/UThV8=", - "dev": true, - "requires": { - "ajv": "^4.7.0", - "ajv-keywords": "^1.0.0", - "chalk": "^1.1.1", - "lodash": "^4.0.0", - "slice-ansi": "0.0.4", - "string-width": "^2.0.0" - }, - "dependencies": { - "ajv": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", - "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", - "dev": true, - "requires": { - "co": "^4.6.0", - "json-stable-stringify": "^1.0.1" - } - }, - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "lodash": { - "version": "4.17.5", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz", - "integrity": "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true - }, - "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", - "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - } - } - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2" - } - }, - "type-detect": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-1.0.0.tgz", - "integrity": "sha1-diIXzAbbJY7EiQihKY6LlRIejqI=", - "dev": true - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", - "dev": true - }, - "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "requires": { - "punycode": "^2.1.0" - } - }, - "uritemplate": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/uritemplate/-/uritemplate-0.3.4.tgz", - "integrity": "sha1-BdCoU/+8iw9Jqj1NKtd3sNHuBww=" - }, - "user-home": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", - "integrity": "sha1-nHC/2Babwdy/SGBODwS4tJzenp8=", - "dev": true, - "requires": { - "os-homedir": "^1.0.0" - } - }, - "util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", - "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", - "dev": true, - "requires": { - "inherits": "2.0.1" - }, - "dependencies": { - "inherits": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", - "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", - "dev": true - } - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "which": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/which/-/which-1.2.14.tgz", - "integrity": "sha1-mofEN48D6CfOyvGs31bHNsAcFOU=", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "write": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", - "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", - "dev": true, - "requires": { - "mkdirp": "^0.5.1" - } - }, - "xmldom": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.19.tgz", - "integrity": "sha1-Yx/Ad3bv2EEYvyUXGzftTQdaCrw=" - }, - "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", - "dev": true - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" - } - } -} diff --git a/package.json b/package.json index cc795bb5..f20fa389 100644 --- a/package.json +++ b/package.json @@ -1,56 +1,41 @@ { - "name": "ldf-server", - "description": "Linked Data Fragments Server", - "version": "2.2.5", - "license": "MIT", + "private": true, + "repository": "https://github.com/LinkedDataFragments/Server.js/", + "workspaces": [ + "packages/*" + ], "engines": { - "node": ">=6.0" - }, - "bin": { - "ldf-server": "./bin/ldf-server" - }, - "main": "module.js", - "repository": { - "type": "git", - "url": "git@github.com:LinkedDataFragments/Server.js.git" - }, - "bugs": { - "url": "https://github.com/LinkedDataFragments/Server.js/issues" - }, - "scripts": { - "test": "mocha", - "lint": "eslint bin/* lib test" - }, - "dependencies": { - "asynciterator": "^1.1.0", - "forwarded-parse": "^2.0.0", - "jsonld": "^0.4.11", - "lodash": "^2.4.2", - "lru-cache": "^4.0.1", - "mime": "^1.3.4", - "n3": "^0.7.0", - "negotiate": "^1.0.1", - "q": "^1.4.1", - "qejs": "^3.0.5", - "request": "^2.88.0", - "uritemplate": "^0.3.4" - }, - "optionalDependencies": { - "access-log": "^0.3.9", - "hdt": "^2.1.3", - "parse-cache-control": "^1.0.1" + "node": ">=10.0" }, "devDependencies": { - "chai": "^3.5.0", - "eslint": "^3.4.0", - "mocha": "^5.2.0", + "chai": "^4.0.0", + "coveralls": "^3.0.9", + "eslint": "^7.0.0", + "eslint-plugin-import": "^2.22.0", + "lerna": "^4.0.0", + "manual-git-changelog": "^1.0.1", + "mocha": "^8.0.0", + "nyc": "^15.0.0", "pre-commit": "^1.1.3", "sinon": "^1.17.4", - "sinon-chai": "^2.8.0", - "supertest": "^2.0.0" + "sinon-chai": "^3.0.0", + "supertest": "^6.0.0" }, "pre-commit": [ "lint", "test" - ] + ], + "scripts": { + "test-changed": "lerna run test --since HEAD", + "lint-changed": "lerna run lint --since HEAD", + "mocha": "mocha \"packages/*/test/**/*-test.js\" --recursive --require ./test/test-setup --timeout 500", + "test": "nyc npm run mocha", + "test-ci": "nyc --reporter=lcov npm run mocha", + "lint": "eslint packages/*/bin/* packages/*/lib packages/*/test", + "clean": "rm -rf ./node_modules && rm -rf ./packages/*/node_modules", + "publish": "lerna publish", + "publish-bare": "lerna exec -- npm publish --silent", + "postinstall": "lerna run prepare", + "version": "manual-git-changelog onversion" + } } diff --git a/packages/core/README.md b/packages/core/README.md new file mode 100644 index 00000000..216bb064 --- /dev/null +++ b/packages/core/README.md @@ -0,0 +1,276 @@ +# Linked Data Fragments Server - Core + + +[![npm version](https://badge.fury.io/js/%40ldf%2Fcore.svg)](https://www.npmjs.com/package/@ldf/core) + +This package provides core classes with shared functionality for Linked Data Fragments servers. + +This package should be used if you want to create your own LDF server configuration or LDF server module. +If you just want to run a QPF server, you can make use of [`@ldf/server`](https://github.com/LinkedDataFragments/Server.js/tree/master/packages/server) instead. + +_This package is a [Linked Data Fragments Server module](https://github.com/LinkedDataFragments/Server.js/)._ + +## Usage in `@ldf/server` + +This package exposes the the following context entries: + +**Controllers:** +* `AssetsController`: Responds to requests for assets. This is enabled by default in `@ldf/server`. _Should be used as `@type` controller value._ +* `DereferenceController`: Responds to dereferencing requests. This is enabled by default in `@ldf/server`. _Should be used as `@type` controller value._ +* `NotFoundController`: Responds to requests that cannot be resolved. This is enabled by default in `@ldf/server`. _Should be used as `@type` controller value._ +* `Controller`: An abstract controller. _Should be used as `extends` value when creating new controllers._ +* `ControllerExtension`: An abstract controller extension. _Should be used as `extends` value when creating new controller extensions._ +* `assetsDir`: Path to a directory where assets can be found. _Should be used as key in a `Server` config._ +* `assetsPath`: URL matching for assets. _Should be used as key in a `Server` config._ +* `dereference`: A dereferencing entry for a datasource to a path. _Should be used as key in a `Server` config._ +* `dereferenceDatasource`: The datasource of a dereferencing entry. _Should be used as key in a dereferencing entry._ +* `dereferencePath`: The path of a dereferencing entry. _Should be used as key in a dereferencing entry._ + +**Datasources:** +* `EmptyDatasource`: An empty data source doesn't contain any quads. _Should be used as `@type` datasource value._ +* `IndexDatasource`: A datasource that lists other data sources. This is enabled by default in `@ldf/server`. _Should be used as `@type` datasource value._ +* `MemoryDatasource`: An abstract in-memory datasource. _Should be used as `extends` value when creating new in-memory datasources._ +* `Datasource`: An abstract datasource. _Should be used as `extends` value when creating new datasources._ +* `datasourceTitle`: The title of a datasource. _Should be used as key in a datasource._ +* `description`: The description of a datasource. _Should be used as key in a datasource._ +* `datasourcePath`: The relative path to the datasource from the baseURL. _Should be used as key in a datasource._ +* `enabled`: If the datasource is enabled, by default true. _Should be used as key in a datasource._ +* `hide`: If the datasource must be hide from the index, by default false. _Should be used as key in a datasource._ +* `graph`: The default graph of the datasource. _Should be used as key in a datasource._ +* `license`: The license of the datasource. _Should be used as key in a datasource._ +* `licenseUrl`: A link to the license of the datasource. _Should be used as key in a datasource._ +* `copyright`: The copyright statement of the datasource. _Should be used as key in a datasource._ +* `homepage`: The homepage url of the datasource. _Should be used as key in a datasource._ +* `quads`: If quad patterns are supported, otherwise only triple patterns are supported. Defaults to `true`. _Should be used as key in a datasource._ +* `file`: The dataset file path. _Should be used as key in a memory datasource._ +* `datasourceUrl`: The dataset file URL from the baseURL. _Should be used as key in a memory datasource._ + +**Routers:** +* `DatasourceRouter`: Routes URLs to data sources. This is enabled by default in `@ldf/server`. _Should be used as `@type` router value._ +* `PageRouter`: Routes page numbers to offsets. This is enabled by default in `@ldf/server`. _Should be used as `@type` router value._ +* `Router`: An abstract router. _Should be used as `extends` value when creating new routers._ +* `pageSize`: The triple page size, which defaults to 100. _Should be used as key in a page router._ + +**Views:** +* `ErrorHtmlView`: Represents a 500 response in HTML. This is enabled by default in `@ldf/server`. _Should be used as `@type` view value._ +* `ForbiddenHtmlView`: Represents a 401 response in HTML. This is enabled by default in `@ldf/server`. _Should be used as `@type` view value._ +* `NotFoundHtmlView`: Represents a 404 response in HTML. This is enabled by default in `@ldf/server`. _Should be used as `@type` view value._ +* `ErrorRdfView`: Represents a 500 response in RDF. This is enabled by default in `@ldf/server`. _Should be used as `@type` view value._ +* `NotFoundRdfView`: Represents a 404 response in RDF. This is enabled by default in `@ldf/server`. _Should be used as `@type` view value._ +* `ViewCollection`: Provides access to content-negotiated views by name. This is enabled by default in `@ldf/server`. _Should be used as `@type` view value._ +* `HtmlView`: An abstract HTML view. _Should be used as `extends` value when creating new HTML views._ +* `RdfView`: An abstract RDF view. _Should be used as `extends` value when creating new RDF views._ +* `View`: An abstract view. _Should be used as `extends` value when creating new views._ +* `viewExtensions`: A view extension. _Should be used as key in a view._ +* `viewCache`: If views should be cached. _Should be used as key in an HTML view._ +* `viewHeader`: The view header title. _Should be used as key in an HTML view._ + +**Other:** +* `Server`: An HTTP server that provides access to Linked Data Fragments. This is enabled by default in `@ldf/server`. _Should be used as `@type` value._ +* `title`: The server name. _Should be used as key in a `Server` config._ +* `baseURL`: The base URL path for the server. _Should be used as key in a `Server` config._ +* `port`: The port the server will bind with. _Should be used as key in a `Server` config._ +* `workers`: The number of server instances that will be started. _Should be used as key in a `Server` config._ +* `protocol`: Explicitly set the protocol, default will be the protocol derived from the baseURL. _Should be used as key in a `Server` config._ +* `datasource` or `datasources`: One or more datasources for the server. _Should be used as key in a `Server` config._ +* `prefixes`: A collection of default URI prefixes. _Should be used as key in a `Server` config._ +* `prefix`: The prefix label of a prefix entry. _Should be used as key in a prefix entry._ +* `uri`: The prefix URI of a prefix entry. _Should be used as key in a prefix entry._ +* `responseHeaders`: Default headers that should be set in responses. _Should be used as key in a prefix entry._ +* `headerName`: The header name in a response header entry. _Should be used as key in a response header entry._ +* `headerValue`: The header value in a response header entry. _Should be used as key in a response header entry._ +* `sslKey`: Path to an SSL key. _Should be used as key in a `Server` config._ +* `sslCert`: Path to an SSL certificate. _Should be used as key in a `Server` config._ +* `sslCa`: Path to an SSL certificate authority. _Should be used as key in a `Server` config._ +* `logging`: If the server should perform logging, defaults to `false`. _Should be used as key in a `Server` config._ +* `loggingFile`: Path to a log file. _Should be used as key in a `Server` config._ +* `routers`: Routers for the server. This is configured by default in `@ldf/server`. _Should be used as key in a `Server` config._ +* `controllers`: Controllers for the server. This is configured by default in `@ldf/server`. _Should be used as key in a `Server` config._ +* `viewCollection`: Override the default view collection. This is configured by default in `@ldf/server`. _Should be used as key in a `Server` config._ +* `views`: Views for the server. This is configured by default in `@ldf/server`. _Should be used as key in a `Server` config._ +* `UrlData`: A data object class for preset URL information. This is enabled by default in `@ldf/server`. _Should be used as `@type` value._ +* `urlData`: The UrlData helper object. This is enabled by default in `@ldf/server`. _Should be used as key in a `Server` config._ +* `dataFactory`: A [factory object to construct RDFJS terms](http://rdf.js.org/data-model-spec/#datafactory-interface). `@ldf/server` uses the [N3](https://github.com/rdfjs/N3.js) `DataFactory` by default. _Should be used as key in a `Server` config._ + +`@ldf/server` and `@ldf/preset-qpf` provide default instantiations of all core classes, +which means that you don't have to define them in your config file yourself. +The only thing you still need to do is defining different optional parameters, as shown below. + +Example: +```json +{ + "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@ldf/server/^3.0.0/components/context.jsonld", + "@id": "urn:ldf-server:my", + "import": "preset-qpf:config-defaults.json", + + "title": "My Linked Data Fragments server", + "baseURL": "https://example.org/", + "port": 3000, + "workers": 2, + "protocol": "http", + + "datasources": [ + { + "@id": "urn:ldf-server:myDatasourceVersion1", + "@type": "SparqlDatasource", + "datasourceTitle": "My SPARQL source", + "description": "My datasource with a SPARQL-endpoint back-end", + "datasourcePath": "mysparql", + "sparqlEndpoint": "https://dbpedia.org/sparql", + "enabled": true, + "hide": false, + "license": "MIT", + "licenseUrl": "http://example.org/my-license", + "copyright": "This datasource is owned by Alice", + "homepage": "http://example.org/alice" + }, + { + "@id": "urn:ldf-server:myDatasourceVersion2", + "@type": "TurtleDatasource", + "datasourceTitle": "My Turtle file", + "description": "My dataset with a Turtle back-end", + "datasourcePath": "myttl", + "file": "path/to/file.ttl", + "graph": "http://example.org/default-graph" + } + ], + + "prefixes": [ + { "prefix": "rdf", "uri": "http://www.w3.org/1999/02/22-rdf-syntax-ns#" }, + { "prefix": "rdfs", "uri": "http://www.w3.org/2000/01/rdf-schema#" }, + { "prefix": "owl", "uri": "http://www.w3.org/2002/07/owl#" }, + { "prefix": "xsd", "uri": "http://www.w3.org/2001/XMLSchema#" }, + { "prefix": "hydra", "uri": "http://www.w3.org/ns/hydra/core#" }, + { "prefix": "void", "uri": "http://rdfs.org/ns/void#" }, + { "prefix": "skos", "uri": "http://www.w3.org/2004/02/skos/core#" }, + { "prefix": "dcterms", "uri": "http://purl.org/dc/terms/" }, + { "prefix": "dc11", "uri": "http://purl.org/dc/elements/1.1/" }, + { "prefix": "foaf", "uri": "http://xmlns.com/foaf/0.1/" }, + { "prefix": "geo", "uri": "http://www.w3.org/2003/01/geo/wgs84_pos#" }, + { "prefix": "dbpedia", "uri": "http://dbpedia.org/resource/" }, + { "prefix": "dbpedia-owl", "uri": "http://dbpedia.org/ontology/" }, + { "prefix": "dbpprop", "uri": "http://dbpedia.org/property/" } + ], + + "logging": true, + "loggingFile": "access.log", + + "dereference": [ + { + "dereferenceDatasource": "urn:ldf-server:myDatasourceVersion2", + "dereferencePath": "/resource/" + } + ], + + "responseHeaders": [ + { "headerName": "Access-Control-Allow-Origin", "headerValue": "*" }, + { "headerName": "Access-Control-Allow-Headers", "headerValue": "Accept-Datetime" }, + { "headerName": "Access-Control-Expose-Headers", "headerValue": "Content-Location,Link,Memento-Datetime" } + ], + + "sslKey": "../core/config/certs/localhost-server.key", + "sslCert": "../core/config/certs/localhost-server.crt", + "sslCa": "../core/config/certs/localhost-ca.crt", + + "router": [ + { + "@id": "preset-qpf:sets/routers.json#myPageRouter", + "pageSize": 50 + } + ] +} + +``` + +## Usage in other packages + +When this module is used in a package other than `@ldf/server`, +then the JSON-LD context `https://linkedsoftwaredependencies.org/contexts/@ldf/core.jsonld` must be imported. + +For example: +``` +{ + "@context": [ + "https://linkedsoftwaredependencies.org/bundles/npm/@ldf/core/^3.0.0/components/context.jsonld", + "https://linkedsoftwaredependencies.org/bundles/npm/@ldf/preset-qpf/^3.0.0/components/context.jsonld", + "https://linkedsoftwaredependencies.org/bundles/npm/@ldf/core/^3.0.0/components/context.jsonld" + ], + "@id": "urn:ldf-server:my", + + "controllers": [ + { + "@id": "urn:ldf-server:myAssetsController", + "@type": "AssetsController" + }, + { + "@id": "urn:ldf-server:myDereferenceController", + "@type": "DereferenceController" + }, + { + "@id": "urn:ldf-server:myNotFoundController", + "@type": "NotFoundController" + } + ], + + "datasources": [ + { + "@id": "urn:ldf-server:myIndexDatasource", + "@type": "IndexDatasource", + "datasourceTitle": "dataset index", + "datasourcePath": "/", + "hide": true + } + ], + + "prefixes": [ + { "prefix": "rdf", "uri": "http://www.w3.org/1999/02/22-rdf-syntax-ns#" }, + { "prefix": "rdfs", "uri": "http://www.w3.org/2000/01/rdf-schema#" }, + { "prefix": "owl", "uri": "http://www.w3.org/2002/07/owl#" }, + { "prefix": "xsd", "uri": "http://www.w3.org/2001/XMLSchema#" }, + { "prefix": "hydra", "uri": "http://www.w3.org/ns/hydra/core#" }, + { "prefix": "void", "uri": "http://rdfs.org/ns/void#" } + ], + + "routers": [ + { + "@id": "urn:ldf-server:myDatasourceRouter", + "@type": "DatasourceRouter" + }, + { + "@id": "urn:ldf-server:myPageRouter", + "@type": "PageRouter" + } + ], + + "views": [ + { + "@id": "urn:ldf-server:myErrorHtmlView", + "@type": "ErrorHtmlView" + }, + { + "@id": "urn:ldf-server:myErrorRdfView", + "@type": "ErrorRdfView" + }, + { + "@id": "urn:ldf-server:myForbiddenHtmlView", + "@type": "ForbiddenHtmlView" + }, + { + "@id": "urn:ldf-server:myNotFoundHtmlView", + "@type": "NotFoundHtmlView" + }, + { + "@id": "urn:ldf-server:myNotFoundRdfView", + "@type": "NotFoundRdfView" + } + ] + + // Same as above... +} +``` + +## License +The Linked Data Fragments server is written by [Ruben Verborgh](https://ruben.verborgh.org/), Miel Vander Sande, [Ruben Taelman](https://www.rubensworks.net/) and colleagues. + +This code is copyrighted by [Ghent University – imec](http://idlab.ugent.be/) +and released under the [MIT license](http://opensource.org/licenses/MIT). diff --git a/assets/favicon.ico b/packages/core/assets/favicon.ico similarity index 100% rename from assets/favicon.ico rename to packages/core/assets/favicon.ico diff --git a/assets/images/logo.svg b/packages/core/assets/images/logo.svg similarity index 100% rename from assets/images/logo.svg rename to packages/core/assets/images/logo.svg diff --git a/assets/styles/ldf-server.css b/packages/core/assets/styles/ldf-server.css similarity index 87% rename from assets/styles/ldf-server.css rename to packages/core/assets/styles/ldf-server.css index ea52043b..8496b3e4 100644 --- a/assets/styles/ldf-server.css +++ b/packages/core/assets/styles/ldf-server.css @@ -64,11 +64,12 @@ pre { form { margin: 0 0 1.5em; + padding: .5em 0 0 20px; } fieldset { border: none; - padding: .5em 0 0 20px; + padding: 0; } fieldset ul { margin-left: 0; @@ -85,7 +86,7 @@ legend { } label { - width: 100px; + width: 90px; display: block; float: left; clear: both; @@ -99,7 +100,7 @@ input { outline: none; font-size: .95em; } -fieldset input { +fieldset input:not([type=radio]) { width: 500px; color: #be1622; background-color: transparent; @@ -112,7 +113,7 @@ input[type=submit] { color: #be1622; background-color: #f6f6f6; border-radius: 3px; - padding: 5px 8px; + padding: 5px 16px; border: 1px solid #999999; cursor: pointer; } @@ -120,7 +121,7 @@ input[type=submit]:hover { border-color: #666666; } input[type=submit]:active { - padding: 6px 7px 4px 9px; + margin: 0 0 1px 1px; } .uri { font-family: "Droid Sans Mono", monospace; @@ -145,12 +146,12 @@ footer { font-size: small; } footer * { - color: gray; + color: #999999; margin-right: 5px; } .counts { - color: gray; + color: #999999; } ul.links { margin: 0; @@ -163,14 +164,14 @@ ul.links li { font-weight: bold; } -ul.triples { +ul.quads { margin: .3em 0 1em 20px; font-size: .95em; line-height: 1.5; font-family: "Droid Sans Mono", monospace; overflow-x: hidden; } -ul.triples li { +ul.quads li { text-indent: -20px; padding-left: 20px; max-width: 100%; @@ -179,17 +180,23 @@ ul.triples li { white-space: nowrap; text-overflow: ellipsis; } -ul.triples li:hover { +ul.quads li:hover { max-height: 100em; white-space: normal; transition: max-height .5s ease-in; transition-delay: .5s; } -ul.triples li:not(:hover) a { +ul.quads li:not(:hover) a { color: inherit; } -ul.triples a:nth-child(2) { - margin: 0 1em; +ul.quads a { + margin-right: 1em; +} +ul.quads a:last-child { + margin-right: 0; +} +ul.quads li:not(:hover) a.graph { + color: #999999; } abbr { border: none; @@ -209,7 +216,7 @@ dt { clear: left; } dd { - color: gray; + color: #999999; margin: .1em 0 0 12em; font-size: .95em; } @@ -232,7 +239,7 @@ dd { h1, legend { margin: 0; } - fieldset, ul.triples { + fieldset, ul.quads { padding: .5em 0; margin: 0; } @@ -242,7 +249,11 @@ dd { label { width: 80px; } - ul.triples li { + ul.quads li { margin: 1em 0; } } + +.searchForm { + display: inline-block; +} diff --git a/packages/core/components/Controller.jsonld b/packages/core/components/Controller.jsonld new file mode 100644 index 00000000..a774821a --- /dev/null +++ b/packages/core/components/Controller.jsonld @@ -0,0 +1,63 @@ +{ + "@context": [ + "https://linkedsoftwaredependencies.org/bundles/npm/@ldf/core/^3.0.0/components/context.jsonld" + ], + "@id": "npmd:@ldf/server", + "components": [ + { + "@id": "ldfc:Controller", + "@type": "AbstractClass", + "parameters": [ + { + "@id": "ldfc:Controller#prefix", + "inheritValues": { + "@type": "InheritanceValue", + "onParameter": "ldfc:Server#prefix", + "from": "ldfc:Server" + } + }, + { + "@id": "ldfc:Controller#datasource", + "inheritValues": { + "@type": "InheritanceValue", + "onParameter": "ldfc:Server#datasource", + "from": "ldfc:Server" + } + }, + { + "@id": "ldfc:Controller#viewCollection", + "inheritValues": { + "@type": "InheritanceValue", + "onParameter": "ldfc:Server#viewCollection", + "from": "ldfc:Server" + } + }, + { + "@id": "ldfc:Controller#urlData", + "inheritValues": { + "@type": "InheritanceValue", + "onParameter": "ldfc:Server#urlData", + "from": "ldfc:Server" + } + } + ], + "constructorArguments": { + "@id": "ldfc:Controller#constructorArgumentsObject", + "fields": [ + { + "@id": "ldfc:Server#prefixField" + }, + { + "@id": "ldfc:Server#datasourceField" + }, + { + "@id": "ldfc:Server#viewField" + }, + { + "@id": "ldfc:Server#urlDataField" + } + ] + } + } + ] +} diff --git a/packages/core/components/Controller/Assets.jsonld b/packages/core/components/Controller/Assets.jsonld new file mode 100644 index 00000000..5188e1dd --- /dev/null +++ b/packages/core/components/Controller/Assets.jsonld @@ -0,0 +1,53 @@ +{ + "@context": [ + "https://linkedsoftwaredependencies.org/bundles/npm/@ldf/core/^3.0.0/components/context.jsonld" + ], + "@id": "npmd:@ldf/server", + "components": [ + { + "@id": "ldfc:Controller/Assets", + "@type": "Class", + "extends": "ldfc:Controller", + "requireElement": "controllers.AssetsController", + "comment": "An AssetsController responds to requests for assets", + "parameters": [ + { + "@id": "ldfc:Controller/Assets#dir", + "inheritValues": { + "@type": "InheritanceValue", + "onParameter": "ldfc:Server#assetsDir", + "from": "ldfc:Server" + } + }, + { + "@id": "ldfc:Controller/Assets#path", + "inheritValues": { + "@type": "InheritanceValue", + "onParameter": "ldfc:Server#assetsPath", + "from": "ldfc:Server" + } + } + ], + "constructorArguments": { + "extends": "ldfc:Controller#constructorArgumentsObject", + "fields": [ + { + "keyRaw": "summaries", + "value": { + "fields": [ + { + "keyRaw": "assetsFolder", + "value": "ldfc:Server#assetsDir" + }, + { + "keyRaw": "path", + "value": "ldfc:Server#assetsPath" + } + ] + } + } + ] + } + } + ] +} diff --git a/packages/core/components/Controller/Dereference.jsonld b/packages/core/components/Controller/Dereference.jsonld new file mode 100644 index 00000000..4bd8ceaa --- /dev/null +++ b/packages/core/components/Controller/Dereference.jsonld @@ -0,0 +1,42 @@ +{ + "@context": [ + "https://linkedsoftwaredependencies.org/bundles/npm/@ldf/core/^3.0.0/components/context.jsonld" + ], + "@id": "npmd:@ldf/server", + "components": [ + { + "@id": "ldfc:Controller/Dereference", + "@type": "Class", + "extends": "ldfc:Controller", + "requireElement": "controllers.DereferenceController", + "comment": "A DeferenceController responds to dereferencing requests", + "parameters": [ + { + "@id": "ldfc:Controller/Dereference#dereference", + "inheritValues": { + "@type": "InheritanceValue", + "onParameter": "ldfc:Server#dereference", + "from": "ldfc:Server" + } + } + ], + "constructorArguments": { + "extends": "ldfc:Controller#constructorArgumentsObject", + "fields": [ + { + "keyRaw": "dereference", + "value": { + "fields": [ + { + "collectEntries": "ldfc:Server#dereference", + "key": "ldfc:Server#dereferencePath", + "value": "ldfc:Server#dereferenceDatasource" + } + ] + } + } + ] + } + } + ] +} diff --git a/packages/core/components/Controller/NotFound.jsonld b/packages/core/components/Controller/NotFound.jsonld new file mode 100644 index 00000000..2422f833 --- /dev/null +++ b/packages/core/components/Controller/NotFound.jsonld @@ -0,0 +1,18 @@ +{ + "@context": [ + "https://linkedsoftwaredependencies.org/bundles/npm/@ldf/core/^3.0.0/components/context.jsonld" + ], + "@id": "npmd:@ldf/server", + "components": [ + { + "@id": "ldfc:Controller/NotFound", + "@type": "Class", + "extends": "ldfc:Controller", + "requireElement": "controllers.NotFoundController", + "comment": "A NotFoundController responds to requests that cannot be resolved", + "constructorArguments": { + "extends": "ldfc:Controller#constructorArgumentsObject" + } + } + ] +} diff --git a/packages/core/components/ControllerExtension.jsonld b/packages/core/components/ControllerExtension.jsonld new file mode 100644 index 00000000..3ce50cd8 --- /dev/null +++ b/packages/core/components/ControllerExtension.jsonld @@ -0,0 +1,12 @@ +{ + "@context": [ + "https://linkedsoftwaredependencies.org/bundles/npm/@ldf/core/^3.0.0/components/context.jsonld" + ], + "@id": "npmd:@ldf/server", + "components": [ + { + "@id": "ldfc:ControllerExtension", + "@type": "AbstractClass" + } + ] +} diff --git a/packages/core/components/Datasource.jsonld b/packages/core/components/Datasource.jsonld new file mode 100644 index 00000000..ce9030b3 --- /dev/null +++ b/packages/core/components/Datasource.jsonld @@ -0,0 +1,161 @@ +{ + "@context": [ + "https://linkedsoftwaredependencies.org/bundles/npm/@ldf/core/^3.0.0/components/context.jsonld" + ], + "@id": "npmd:@ldf/server", + "components": [ + { + "@id": "ldfc:Datasource", + "@type": "AbstractClass", + "comment": "An Datasource loads and queries data", + "parameters": [ + { + "@id": "ldfc:Datasource#title", + "comment": "The datasource name", + "range": "xsd:string", + "unique": true + }, + { + "@id": "ldfc:Datasource#description", + "comment": "The datasource description", + "range": "xsd:string", + "unique": true + }, + { + "@id": "ldfc:Datasource#path", + "comment": "The relative path to the datasource from the baseURL", + "range": "xsd:string", + "unique": true, + "required": true + }, + { + "@id": "ldfc:Datasource#enabled", + "comment": "If the datasource is enabled, by default true", + "range": "xsd:boolean", + "unique": true, + "default": true + }, + { + "@id": "ldfc:Datasource#hide", + "comment": "If the datasource must be hide from the index, by default false", + "range": "xsd:boolean", + "unique": true, + "default": false + }, + { + "@id": "ldfc:Datasource#graph", + "comment": "The default graph of the datasource", + "range": "xsd:string", + "unique": true + }, + { + "@id": "ldfc:Datasource#urlData", + "inheritValues": { + "@type": "InheritanceValue", + "onParameter": "ldfc:Server#urlData", + "from": "ldfc:Server" + } + }, + { + "@id": "ldfc:Datasource#license", + "comment": "The license of the datasource", + "range": "xsd:string", + "unique": true + }, + { + "@id": "ldfc:Datasource#licenseUrl", + "comment": "A link to the license of the datasource", + "range": "xsd:string", + "unique": true + }, + { + "@id": "ldfc:Datasource#copyright", + "comment": "The copyright statement of the datasource", + "range": "xsd:string", + "unique": true + }, + { + "@id": "ldfc:Datasource#homepage", + "comment": "The homepage url of the datasource", + "range": "xsd:string", + "unique": true + }, + { + "@id": "ldfc:Datasource#quads", + "comment": "If quad patterns are supported, otherwise only triple patterns are supported", + "range": "xsd:boolean", + "unique": true, + "default": true + }, + { + "@id": "ldfc:Datasource#dataFactory", + "inheritValues": { + "@type": "InheritanceValue", + "onParameter": "ldfc:Server#dataFactory", + "from": "ldfc:Server" + } + } + ], + "constructorArguments": { + "@id": "ldfc:Datasource#constructorArgumentsObject", + "fields": [ + { + "keyRaw": "id", + "value": "rdf:subject" + }, + { + "keyRaw": "title", + "value": "ldfc:Datasource#title" + }, + { + "keyRaw": "description", + "value": "ldfc:Datasource#description" + }, + { + "keyRaw": "path", + "value": "ldfc:Datasource#path" + }, + { + "keyRaw": "enabled", + "value": "ldfc:Datasource#enabled" + }, + { + "keyRaw": "hide", + "value": "ldfc:Datasource#hide" + }, + { + "keyRaw": "graph", + "value": "ldfc:Datasource#graph" + }, + { + "keyRaw": "urlData", + "value": "ldfc:Server#urlData" + }, + { + "keyRaw": "license", + "value": "ldfc:Datasource#license" + }, + { + "keyRaw": "licenseUrl", + "value": "ldfc:Datasource#licenseUrl" + }, + { + "keyRaw": "copyright", + "value": "ldfc:Datasource#copyright" + }, + { + "keyRaw": "homepage", + "value": "ldfc:Datasource#homepage" + }, + { + "keyRaw": "quads", + "value": "ldfc:Datasource#quads" + }, + { + "@id": "ldfc:Server#dataFactoryField" + } + ] + } + } + ] +} diff --git a/packages/core/components/Datasource/Empty.jsonld b/packages/core/components/Datasource/Empty.jsonld new file mode 100644 index 00000000..8896c252 --- /dev/null +++ b/packages/core/components/Datasource/Empty.jsonld @@ -0,0 +1,18 @@ +{ + "@context": [ + "https://linkedsoftwaredependencies.org/bundles/npm/@ldf/core/^3.0.0/components/context.jsonld" + ], + "@id": "npmd:@ldf/server", + "components": [ + { + "@id": "ldfc:Datasource/Empty", + "@type": "Class", + "extends": "ldfc:Datasource", + "requireElement": "datasources.EmptyDatasource", + "comment": "An empty data source doesn't contain any quads", + "constructorArguments": { + "extends": "ldfc:Datasource#constructorArgumentsObject" + } + } + ] +} diff --git a/packages/core/components/Datasource/Index.jsonld b/packages/core/components/Datasource/Index.jsonld new file mode 100644 index 00000000..13612824 --- /dev/null +++ b/packages/core/components/Datasource/Index.jsonld @@ -0,0 +1,33 @@ +{ + "@context": [ + "https://linkedsoftwaredependencies.org/bundles/npm/@ldf/core/^3.0.0/components/context.jsonld" + ], + "@id": "npmd:@ldf/server", + "components": [ + { + "@id": "ldfc:Datasource/Index", + "@type": "Class", + "extends": "ldfc:Datasource", + "requireElement": "datasources.IndexDatasource", + "comment": "An IndexDatasource is a datasource that lists other data sources", + "parameters": [ + { + "@id": "ldfc:Datasource/Index#datasource", + "inheritValues": { + "@type": "InheritanceValue", + "onParameter": "ldfc:Server#datasource", + "from": "ldfc:Server" + } + } + ], + "constructorArguments": { + "extends": "ldfc:Datasource#constructorArgumentsObject", + "fields": [ + { + "@id": "ldfc:Server#datasourceField" + } + ] + } + } + ] +} diff --git a/packages/core/components/Datasource/Memory.jsonld b/packages/core/components/Datasource/Memory.jsonld new file mode 100644 index 00000000..4e8b78df --- /dev/null +++ b/packages/core/components/Datasource/Memory.jsonld @@ -0,0 +1,41 @@ +{ + "@context": [ + "https://linkedsoftwaredependencies.org/bundles/npm/@ldf/core/^3.0.0/components/context.jsonld" + ], + "@id": "npmd:@ldf/server", + "components": [ + { + "@id": "ldfc:Datasource/Memory", + "@type": "AbstractClass", + "extends": "ldfc:Datasource", + "parameters": [ + { + "@id": "ldfc:Datasource/Memory#file", + "comment": "The dataset file path", + "range": "xsd:string", + "unique": true + }, + { + "@id": "ldfc:Datasource/Memory#url", + "comment": "The dataset file URL", + "range": "xsd:string", + "unique": true + } + ], + "constructorArguments": { + "@id": "ldfc:Datasource/Memory#constructorArgumentsObject", + "extends": "ldfc:Datasource#constructorArgumentsObject", + "fields": [ + { + "keyRaw": "file", + "value": "ldfc:Datasource/Memory#file" + }, + { + "keyRaw": "url", + "value": "ldfc:Datasource/Memory#url" + } + ] + } + } + ] +} diff --git a/packages/core/components/Router.jsonld b/packages/core/components/Router.jsonld new file mode 100644 index 00000000..53762768 --- /dev/null +++ b/packages/core/components/Router.jsonld @@ -0,0 +1,13 @@ +{ + "@context": [ + "https://linkedsoftwaredependencies.org/bundles/npm/@ldf/core/^3.0.0/components/context.jsonld" + ], + "@id": "npmd:@ldf/server", + "components": [ + { + "@id": "ldfc:Router", + "@type": "AbstractClass", + "comment": "Router is a base class for routing requests" + } + ] +} diff --git a/packages/core/components/Router/Datasource.jsonld b/packages/core/components/Router/Datasource.jsonld new file mode 100644 index 00000000..edecc481 --- /dev/null +++ b/packages/core/components/Router/Datasource.jsonld @@ -0,0 +1,34 @@ +{ + "@context": [ + "https://linkedsoftwaredependencies.org/bundles/npm/@ldf/core/^3.0.0/components/context.jsonld" + ], + "@id": "npmd:@ldf/server", + "components": [ + { + "@id": "ldfc:Router/Datasource", + "@type": "Class", + "extends": "ldfc:Router", + "requireElement": "routers.DatasourceRouter", + "comment": "A DatasourceRouter routes URLs to data sources", + "parameters": [ + { + "@id": "ldfc:Router/Datasource#urlData", + "inheritValues": { + "@type": "InheritanceValue", + "onParameter": "ldfc:Server#urlData", + "from": "ldfc:Server" + } + } + ], + "constructorArguments": [ + { + "fields": [ + { + "@id": "ldfc:Server#urlDataField" + } + ] + } + ] + } + ] +} diff --git a/packages/core/components/Router/Page.jsonld b/packages/core/components/Router/Page.jsonld new file mode 100644 index 00000000..2e7f51dd --- /dev/null +++ b/packages/core/components/Router/Page.jsonld @@ -0,0 +1,32 @@ +{ + "@context": [ + "https://linkedsoftwaredependencies.org/bundles/npm/@ldf/core/^3.0.0/components/context.jsonld" + ], + "@id": "npmd:@ldf/server", + "components": [ + { + "@id": "ldfc:Router/Page", + "@type": "Class", + "extends": "ldfc:Router", + "requireElement": "routers.PageRouter", + "comment": "A PageRouter routes page numbers to offsets", + "parameters": [ + { + "@id": "ldfc:Router/Page#pageSize", + "comment": "The triple page size, which defaults to 100", + "range": "xsd:integer", + "unique": true, + "default": 100 + } + ], + "constructorArguments": { + "fields": [ + { + "keyRaw": "pageSize", + "value": "ldfc:Router/Page#pageSize" + } + ] + } + } + ] +} diff --git a/packages/core/components/Server.jsonld b/packages/core/components/Server.jsonld new file mode 100644 index 00000000..a2014e85 --- /dev/null +++ b/packages/core/components/Server.jsonld @@ -0,0 +1,326 @@ +{ + "@context": [ + "https://linkedsoftwaredependencies.org/bundles/npm/@ldf/core/^3.0.0/components/context.jsonld" + ], + "@id": "npmd:@ldf/server", + "components": [ + { + "@id": "ldfc:Server", + "@type": "Class", + "requireElement": "LinkedDataFragmentsServerWorker", + "comment": "LinkedDataFragmentsServer is an HTTP server that provides access to Linked Data Fragments", + "parameters": [ + { + "@id": "ldfc:Server#name", + "comment": "The server name", + "range": "xsd:string", + "unique": true, + "default": "Linked Data Fragments server" + }, + { + "@id": "ldfc:Server#baseURL", + "comment": "The base URL path for the server", + "range": "xsd:string", + "unique": true, + "default": "/" + }, + { + "@id": "ldfc:Server#urlData", + "comment": "The UrlData helper object", + "range": "ldfc:UrlData", + "unique": true, + "default": { + "@id": "ldfc:Server#defaultUrlData", + "@type": "ldfc:UrlData" + } + }, + { + "@id": "ldfc:Server#port", + "comment": "The port the server will bind with", + "range": "xsd:integer", + "unique": true, + "default": 3000 + }, + { + "@id": "ldfc:Server#workers", + "comment": "The number of server instances that will be started", + "range": "xsd:integer", + "unique": true, + "default": 1 + }, + { + "@id": "ldfc:Server#protocol", + "comment": "Explicitly set the protocol, default will be the protocol derived from the baseURL", + "range": "xsd:string", + "unique": true + }, + { + "@id": "ldfc:Server#datasource", + "comment": "A datasource for the server", + "range": "ldfc:Datasource" + }, + { + "@id": "ldfc:Server#prefix", + "comment": "The URI prefixes that will be used", + "default": [ + { "prefix": "rdf", "uri": "http://www.w3.org/1999/02/22-rdf-syntax-ns#" }, + { "prefix": "rdfs", "uri": "http://www.w3.org/2000/01/rdf-schema#" }, + { "prefix": "owl", "uri": "http://www.w3.org/2002/07/owl#" }, + { "prefix": "xsd", "uri": "http://www.w3.org/2001/XMLSchema#" }, + { "prefix": "hydra", "uri": "http://www.w3.org/ns/hydra/core#" }, + { "prefix": "void", "uri": "http://rdfs.org/ns/void#" }, + { "prefix": "sd", "uri": "http://www.w3.org/TR/sparql11-service-description/#" } + ] + }, + { + "@id": "ldfc:Server#router", + "comment": "A router for the server", + "range": "ldfc:Router" + }, + { + "@id": "ldfc:Server#controller", + "comment": "A controller for the server", + "range": "ldfc:Controller" + }, + { + "@id": "ldfc:Server#viewCollection", + "comment": "A ViewCollection provides access to content-negotiated views by name", + "range": "ldfc:View/Collection", + "unique": true, + "default": [ + { + "@id": "ldfc:Server#defaultViewCollection", + "@type": "ViewCollection" + } + ] + }, + { + "@id": "ldfc:Server#view", + "comment": "A view for the server", + "range": "ldfc:View" + }, + { + "@id": "ldfc:Server#responseHeader", + "comment": "Configuration for the server response data", + "range": { + "@id": "ldfc:Server#Response" + }, + "default": [ + { "headerName": "Access-Control-Allow-Origin", "headerValue": "*" }, + { "headerName": "Access-Control-Allow-Headers", "headerValue": "Accept-Datetime" }, + { "headerName": "Access-Control-Expose-Headers", "headerValue": "Content-Location,Link,Memento-Datetime" } + ] + }, + { + "@id": "ldfc:Server#sslKey", + "comment": "Path to an SSL key", + "range": "xsd:string", + "unique": true, + "default": "config/certs/localhost-server.key" + }, + { + "@id": "ldfc:Server#sslCert", + "comment": "Path to an SSL certificate", + "range": "xsd:string", + "unique": true, + "default": "config/certs/localhost-server.crt" + }, + { + "@id": "ldfc:Server#sslCa", + "comment": "Path to an SSL certificate authority", + "range": "xsd:string", + "unique": true, + "default": "config/certs/localhost-ca.crt" + }, + { + "@id": "ldfc:Server#logging", + "comment": "If the server should perform logging", + "range": "xsd:boolean", + "unique": true, + "default": false + }, + { + "@id": "ldfc:Server#loggingFile", + "comment": "Path to a log file", + "range": "xsd:string", + "unique": true, + "default": "access.log" + }, + { + "@id": "ldfc:Server#assetsPath", + "comment": "URL matching for assets", + "range": "xsd:string", + "unique": true + }, + { + "@id": "ldfc:Server#assetsDir", + "comment": "Path to a directory where assets can be found", + "range": "xsd:string", + "unique": true, + "default": "file://../assets/" + }, + { + "@id": "ldfc:Server#dereference", + "comment": "A dereferencing for a datasource to a path", + "range": { + "@id": "ldfc:Server#Dereferencement", + "rdfs:hasProperty": [ + { "@id": "ldfc:Server#dereferenceDatasource" }, + { "@id": "ldfc:Server#dereferencePath" } + ] + } + }, + { + "@id": "ldfc:Server#dataFactory", + "comment": "A factory object to construct RDFJS terms", + "unique": true, + "default": { + "requireName": "n3", + "requireElement": "DataFactory", + "requireNoConstructor": true + } + } + ], + "constructorArguments": { + "fields": [ + { + "@id": "ldfc:Server#titleField", + "keyRaw": "title", + "value": "ldfc:Server#name" + }, + { + "keyRaw": "baseURL", + "value": "ldfc:Server#baseURL" + }, + { + "@id": "ldfc:Server#urlDataField", + "keyRaw": "urlData", + "value": "ldfc:Server#urlData" + }, + { + "keyRaw": "port", + "value": "ldfc:Server#port" + }, + { + "keyRaw": "protocol", + "value": "ldfc:Server#protocol" + }, + { + "keyRaw": "workers", + "value": "ldfc:Server#workers" + }, + { + "@id": "ldfc:Server#datasourceField", + "keyRaw": "datasources", + "value": { + "fields": [ + { + "collectEntries": "ldfc:Server#datasource", + "key": "ldfc:Datasource#path", + "value": "rdf:object" + } + ] + } + }, + { + "@id": "ldfc:Server#prefixField", + "keyRaw": "prefixes", + "value": { + "fields": [ + { + "collectEntries": "ldfc:Server#prefix", + "key": "rdfa:prefix", + "value": "rdfa:uri" + } + ] + } + }, + { + "@id": "ldfc:Server#routerField", + "keyRaw": "routers", + "value": "ldfc:Server#router" + }, + { + "keyRaw": "controllers", + "value": "ldfc:Server#controller" + }, + { + "@id": "ldfc:Server#viewField", + "keyRaw": "views", + "value": "ldfc:Server#viewCollection" + }, + { + "@id": "ldfc:Server#viewsRawField", + "keyRaw": "viewsRaw", + "value": "ldfc:Server#view" + }, + { + "@id": "ldfc:Server#dataFactoryField", + "keyRaw": "dataFactory", + "value": "ldfc:Server#dataFactory" + }, + { + "keyRaw": "response", + "value": { + "fields": [ + { + "keyRaw": "headers", + "value": { + "fields": [ + { + "collectEntries": "ldfc:Server#responseHeader", + "key": "ldfc:Server#headerName", + "value": "ldfc:Server#headerValue" + } + ] + } + } + ] + } + }, + { + "keyRaw": "ssl", + "value": { + "fields": [ + { + "keyRaw": "keys", + "value": { + "fields": [ + { + "keyRaw": "key", + "value": "ldfc:Server#sslKey" + }, + { + "keyRaw": "cert", + "value": "ldfc:Server#sslCert" + }, + { + "keyRaw": "ca", + "value": "ldfc:Server#sslCa" + } + ] + } + } + ] + } + }, + { + "keyRaw": "logging", + "value": { + "fields": [ + { + "keyRaw": "enabled", + "value": "ldfc:Server#logging" + }, + { + "keyRaw": "file", + "value": "ldfc:Server#loggingFile" + } + ] + } + } + ] + } + } + ] +} diff --git a/packages/core/components/UrlData.jsonld b/packages/core/components/UrlData.jsonld new file mode 100644 index 00000000..a90e4047 --- /dev/null +++ b/packages/core/components/UrlData.jsonld @@ -0,0 +1,68 @@ +{ + "@context": [ + "https://linkedsoftwaredependencies.org/bundles/npm/@ldf/core/^3.0.0/components/context.jsonld" + ], + "@id": "npmd:@ldf/server", + "components": [ + { + "@id": "ldfc:UrlData", + "@type": "Class", + "requireElement": "UrlData", + "comment": "A data object class for preset URL information", + "parameters": [ + { + "@id": "ldfc:UrlData#baseURL", + "inheritValues": { + "@type": "InheritanceValue", + "onParameter": "ldfc:Server#baseURL", + "from": "ldfc:Server" + } + }, + { + "@id": "ldfc:UrlData#assetsPath", + "inheritValues": { + "@type": "InheritanceValue", + "onParameter": "ldfc:Server#assetsPath", + "from": "ldfc:Server" + } + }, + { + "@id": "ldfc:UrlData#assetsDir", + "inheritValues": { + "@type": "InheritanceValue", + "onParameter": "ldfc:Server#assetsDir", + "from": "ldfc:Server" + } + }, + { + "@id": "ldfc:UrlData#protocol", + "inheritValues": { + "@type": "InheritanceValue", + "onParameter": "ldfc:Server#protocol", + "from": "ldfc:Server" + } + } + ], + "constructorArguments": { + "fields": [ + { + "keyRaw": "baseURL", + "value": "ldfc:Server#baseURL" + }, + { + "keyRaw": "assetsPath", + "value": "ldfc:Server#assetsPath" + }, + { + "keyRaw": "assetsDir", + "value": "ldfc:Server#assetsDir" + }, + { + "keyRaw": "protocol", + "value": "ldfc:Server#protocol" + } + ] + } + } + ] +} diff --git a/packages/core/components/View.jsonld b/packages/core/components/View.jsonld new file mode 100644 index 00000000..4aa450da --- /dev/null +++ b/packages/core/components/View.jsonld @@ -0,0 +1,51 @@ +{ + "@context": [ + "https://linkedsoftwaredependencies.org/bundles/npm/@ldf/core/^3.0.0/components/context.jsonld" + ], + "@id": "npmd:@ldf/server", + "components": [ + { + "@id": "ldfc:View", + "@type": "AbstractClass", + "comment": "View is a base class for objects that generate server responses", + "parameters": [ + { + "@id": "ldfc:View#extension", + "range": "ldfc:View" + }, + { + "@id": "ldfc:View#urlData", + "inheritValues": { + "@type": "InheritanceValue", + "onParameter": "ldfc:Server#urlData", + "from": "ldfc:Server" + } + }, + { + "@id": "ldfc:View#dataFactory", + "inheritValues": { + "@type": "InheritanceValue", + "onParameter": "ldfc:Server#dataFactory", + "from": "ldfc:Server" + } + } + ], + "constructorArguments": { + "@id": "ldfc:View#constructorArgumentsObject", + "fields": [ + { + "keyRaw": "views", + "value": "ldfc:View#extension" + }, + { + "keyRaw": "urlData", + "value": "ldfc:Server#urlData" + }, + { + "@id": "ldfc:Server#dataFactoryField" + } + ] + } + } + ] +} diff --git a/packages/core/components/View/Collection.jsonld b/packages/core/components/View/Collection.jsonld new file mode 100644 index 00000000..db145704 --- /dev/null +++ b/packages/core/components/View/Collection.jsonld @@ -0,0 +1,31 @@ +{ + "@context": [ + "https://linkedsoftwaredependencies.org/bundles/npm/@ldf/core/^3.0.0/components/context.jsonld" + ], + "@id": "npmd:@ldf/server", + "components": [ + { + "@id": "ldfc:View/Collection", + "@type": "Class", + "requireElement": "views.ViewCollection", + "parameters": [ + { + "@id": "ldfc:View/Collection#view", + "inheritValues": { + "@type": "InheritanceValue", + "onParameter": "ldfc:Server#view", + "from": "ldfc:Server" + }, + "unique": true + } + ], + "constructorArguments": [ + { + "elements": [ + "ldfc:Server#view" + ] + } + ] + } + ] +} diff --git a/packages/core/components/View/Html.jsonld b/packages/core/components/View/Html.jsonld new file mode 100644 index 00000000..5285ba8d --- /dev/null +++ b/packages/core/components/View/Html.jsonld @@ -0,0 +1,67 @@ +{ + "@context": [ + "https://linkedsoftwaredependencies.org/bundles/npm/@ldf/core/^3.0.0/components/context.jsonld" + ], + "@id": "npmd:@ldf/server", + "components": [ + { + "@id": "ldfc:View/Html", + "@type": "AbstractClass", + "extends": "ldfc:View", + "comment": "HtmlView is a base class for views that generate HTML responses", + "parameters": [ + { + "@id": "ldfc:View/Html#cache", + "comment": "If views should be cached", + "range": "xsd:boolean", + "unique": true + }, + { + "@id": "ldfc:View/Html#urlData", + "inheritValues": { + "@type": "InheritanceValue", + "onParameter": "ldfc:Server#urlData", + "from": "ldfc:Server" + }, + "unique": true + }, + { + "@id": "ldfc:View/Html#title", + "inheritValues": { + "@type": "InheritanceValue", + "onParameter": "ldfc:Server#name", + "from": "ldfc:Server" + }, + "unique": true + }, + { + "@id": "ldfc:View/Html#header", + "comment": "The view header title", + "unique": true + } + ], + "constructorArguments": [ + { + "@id": "ldfc:View/Html#constructorArgumentsObject", + "extends": "ldfc:View#constructorArgumentsObject", + "fields": [ + { + "keyRaw": "cache", + "value": "ldfc:View/Html#cache" + }, + { + "@id": "ldfc:Server#urlDataField" + }, + { + "@id": "ldfc:Server#titleField" + }, + { + "keyRaw": "header", + "value": "ldfc:View/Html#header" + } + ] + } + ] + } + ] +} diff --git a/packages/core/components/View/Html/Error.jsonld b/packages/core/components/View/Html/Error.jsonld new file mode 100644 index 00000000..042ce310 --- /dev/null +++ b/packages/core/components/View/Html/Error.jsonld @@ -0,0 +1,20 @@ +{ + "@context": [ + "https://linkedsoftwaredependencies.org/bundles/npm/@ldf/core/^3.0.0/components/context.jsonld" + ], + "@id": "npmd:@ldf/server", + "components": [ + { + "@id": "ldfc:View/Html/Error", + "@type": "Class", + "extends": "ldfc:View/Html", + "requireElement": "views.error.ErrorHtmlView", + "comment": "An ErrorHtmlView represents a 500 response in HTML", + "constructorArguments": [ + { + "extends": "ldfc:View/Html#constructorArgumentsObject" + } + ] + } + ] +} diff --git a/packages/core/components/View/Html/Forbidden.jsonld b/packages/core/components/View/Html/Forbidden.jsonld new file mode 100644 index 00000000..66fd7f79 --- /dev/null +++ b/packages/core/components/View/Html/Forbidden.jsonld @@ -0,0 +1,20 @@ +{ + "@context": [ + "https://linkedsoftwaredependencies.org/bundles/npm/@ldf/core/^3.0.0/components/context.jsonld" + ], + "@id": "npmd:@ldf/server", + "components": [ + { + "@id": "ldfc:View/Html/Forbidden", + "@type": "Class", + "extends": "ldfc:View/Html", + "requireElement": "views.forbidden.ForbiddenHtmlView", + "comment": "A ForbiddenHtmlView represents a 401 response in HTML", + "constructorArguments": [ + { + "extends": "ldfc:View/Html#constructorArgumentsObject" + } + ] + } + ] +} diff --git a/packages/core/components/View/Html/NotFound.jsonld b/packages/core/components/View/Html/NotFound.jsonld new file mode 100644 index 00000000..e910937f --- /dev/null +++ b/packages/core/components/View/Html/NotFound.jsonld @@ -0,0 +1,20 @@ +{ + "@context": [ + "https://linkedsoftwaredependencies.org/bundles/npm/@ldf/core/^3.0.0/components/context.jsonld" + ], + "@id": "npmd:@ldf/server", + "components": [ + { + "@id": "ldfc:View/Html/NotFound", + "@type": "Class", + "extends": "ldfc:View/Html", + "requireElement": "views.notfound.NotFoundHtmlView", + "comment": "A NotFoundHtmlView represents a 404 response in HTML", + "constructorArguments": [ + { + "extends": "ldfc:View/Html#constructorArgumentsObject" + } + ] + } + ] +} diff --git a/packages/core/components/View/Rdf.jsonld b/packages/core/components/View/Rdf.jsonld new file mode 100644 index 00000000..206e1da4 --- /dev/null +++ b/packages/core/components/View/Rdf.jsonld @@ -0,0 +1,21 @@ +{ + "@context": [ + "https://linkedsoftwaredependencies.org/bundles/npm/@ldf/core/^3.0.0/components/context.jsonld" + ], + "@id": "npmd:@ldf/server", + "components": [ + { + "@id": "ldfc:View/Rdf", + "@type": "AbstractClass", + "extends": "ldfc:View", + "comment": "RdfView is a base class for views that generate RDF responses", + "constructorArguments": [ + { + "@id": "ldfc:View/Rdf#constructorArgumentsObject", + "@type": "ObjectMapping", + "extends": "ldfc:View#constructorArgumentsObject" + } + ] + } + ] +} diff --git a/packages/core/components/View/Rdf/Error.jsonld b/packages/core/components/View/Rdf/Error.jsonld new file mode 100644 index 00000000..166ef4f5 --- /dev/null +++ b/packages/core/components/View/Rdf/Error.jsonld @@ -0,0 +1,20 @@ +{ + "@context": [ + "https://linkedsoftwaredependencies.org/bundles/npm/@ldf/core/^3.0.0/components/context.jsonld" + ], + "@id": "npmd:@ldf/server", + "components": [ + { + "@id": "ldfc:View/Rdf/Error", + "@type": "Class", + "extends": "ldfc:View/Rdf", + "requireElement": "views.error.ErrorRdfView", + "comment": "An ErrorRdfView represents a 500 response in RDF", + "constructorArguments": [ + { + "extends": "ldfc:View/Rdf#constructorArgumentsObject" + } + ] + } + ] +} diff --git a/packages/core/components/View/Rdf/NotFound.jsonld b/packages/core/components/View/Rdf/NotFound.jsonld new file mode 100644 index 00000000..5c7db8b5 --- /dev/null +++ b/packages/core/components/View/Rdf/NotFound.jsonld @@ -0,0 +1,20 @@ +{ + "@context": [ + "https://linkedsoftwaredependencies.org/bundles/npm/@ldf/core/^3.0.0/components/context.jsonld" + ], + "@id": "npmd:@ldf/server", + "components": [ + { + "@id": "ldfc:View/Rdf/NotFound", + "@type": "Class", + "extends": "ldfc:View/Rdf", + "requireElement": "views.notfound.NotFoundRdfView", + "comment": "A NotFoundRdfView represents a 404 response in RDF", + "constructorArguments": [ + { + "extends": "ldfc:View/Rdf#constructorArgumentsObject" + } + ] + } + ] +} diff --git a/packages/core/components/components.jsonld b/packages/core/components/components.jsonld new file mode 100644 index 00000000..a0baeeae --- /dev/null +++ b/packages/core/components/components.jsonld @@ -0,0 +1,35 @@ +{ + "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@ldf/core/^3.0.0/components/context.jsonld", + "@id": "npmd:@ldf/server", + "@type": "Module", + "requireName": "@ldf/core", + "import": [ + "files-ldfc:components/Controller/Assets.jsonld", + "files-ldfc:components/Controller/Dereference.jsonld", + "files-ldfc:components/Controller/NotFound.jsonld", + + "files-ldfc:components/Datasource/Empty.jsonld", + "files-ldfc:components/Datasource/Index.jsonld", + "files-ldfc:components/Datasource/Memory.jsonld", + + "files-ldfc:components/Router/Datasource.jsonld", + "files-ldfc:components/Router/Page.jsonld", + + "files-ldfc:components/View/Html/Error.jsonld", + "files-ldfc:components/View/Html/Forbidden.jsonld", + "files-ldfc:components/View/Html/NotFound.jsonld", + "files-ldfc:components/View/Rdf/Error.jsonld", + "files-ldfc:components/View/Rdf/NotFound.jsonld", + "files-ldfc:components/View/Collection.jsonld", + "files-ldfc:components/View/Html.jsonld", + "files-ldfc:components/View/Rdf.jsonld", + + "files-ldfc:components/Controller.jsonld", + "files-ldfc:components/ControllerExtension.jsonld", + "files-ldfc:components/Datasource.jsonld", + "files-ldfc:components/Router.jsonld", + "files-ldfc:components/Server.jsonld", + "files-ldfc:components/UrlData.jsonld", + "files-ldfc:components/View.jsonld" + ] +} diff --git a/packages/core/components/context.jsonld b/packages/core/components/context.jsonld new file mode 100644 index 00000000..efac61b9 --- /dev/null +++ b/packages/core/components/context.jsonld @@ -0,0 +1,96 @@ +{ + "@context": [ + "https://linkedsoftwaredependencies.org/bundles/npm/componentsjs/^4.0.0/components/context.jsonld", + { + "npmd": "https://linkedsoftwaredependencies.org/bundles/npm/", + "ldfc": "npmd:@ldf/core/", + "files-ldfc": "ldfc:^3.0.0/", + + "rdfa": "http://www.w3.org/ns/rdfa#", + + "AssetsController": "ldfc:Controller/Assets", + "DereferenceController": "ldfc:Controller/Dereference", + "NotFoundController": "ldfc:Controller/NotFound", + "assetsDir": "ldfc:Controller/Assets#dir", + "assetsPath": "ldfc:Controller/Assets#path", + "dereference": "ldfc:Server#dereference", + "dereferenceDatasource": { + "@id": "ldfc:Server#dereferenceDatasource", + "@type": "@id" + }, + "dereferencePath": "ldfc:Server#dereferencePath", + "dataFactory": "ldfc:Server#dataFactory", + + "EmptyDatasource": "ldfc:Datasource/Empty", + "IndexDatasource": "ldfc:Datasource/Index", + "MemoryDatasource": "ldfc:Datasource/Memory", + "datasourceTitle": "ldfc:Datasource#title", + "description": "ldfc:Datasource#description", + "datasourcePath": "ldfc:Datasource#path", + "enabled": "ldfc:Datasource#enabled", + "hide": "ldfc:Datasource#hide", + "graph": "ldfc:Datasource#graph", + "license": "ldfc:Datasource#license", + "licenseUrl": "ldfc:Datasource#licenseUrl", + "copyright": "ldfc:Datasource#copyright", + "homepage": "ldfc:Datasource#homepage", + "quads": "ldfc:Datasource#quads", + "file": "ldfc:Datasource/Memory#file", + "datasourceUrl": "ldfc:Datasource/Memory#url", + + "DatasourceRouter": "ldfc:Router/Datasource", + "PageRouter": "ldfc:Router/Page", + "pageSize": "ldfc:Router/Page#pageSize", + + "ErrorHtmlView": "ldfc:View/Html/Error", + "ForbiddenHtmlView": "ldfc:View/Html/Forbidden", + "NotFoundHtmlView": "ldfc:View/Html/NotFound", + "ErrorRdfView": "ldfc:View/Rdf/Error", + "NotFoundRdfView": "ldfc:View/Rdf/NotFound", + "ViewCollection": "ldfc:View/Collection", + "HtmlView": "ldfc:View/Html", + "RdfView": "ldfc:View/Rdf", + "viewExtension": "ldfc:View#extension", + "viewExtensions": "ldfc:View#extension", + "viewCache": "ldfc:View/Html#cache", + "viewTitle": "ldfc:View/Html#title", + "viewHeader": "ldfc:View/Html#header", + + "Controller": "ldfc:Controller", + "ControllerExtension": "ldfc:ControllerExtension", + "Datasource": "ldfc:Datasource", + "Router": "ldfc:Router", + "Server": "ldfc:Server", + "UrlData": "ldfc:UrlData", + "View": "ldfc:View", + + "title": "ldfc:Server#name", + "baseURL": "ldfc:Server#baseURL", + "urlData": "ldfc:Server#urlData", + "port": "ldfc:Server#port", + "workers": "ldfc:Server#workers", + "protocol": "ldfc:Server#protocol", + "datasource": "ldfc:Server#datasource", + "datasources": "ldfc:Server#datasource", + "prefixes": "ldfc:Server#prefix", + "prefix": "rdfa:prefix", + "uri": "rdfa:uri", + "router": "ldfc:Server#router", + "routers": "ldfc:Server#router", + "controller": "ldfc:Server#controller", + "controllers": "ldfc:Server#controller", + "viewCollection": "ldfc:Server#viewCollection", + "view": "ldfc:Server#view", + "views": "ldfc:Server#view", + "responseHeader": "ldfc:Server#responseHeader", + "responseHeaders": "ldfc:Server#responseHeader", + "headerName": "ldfc:Server#headerName", + "headerValue": "ldfc:Server#headerValue", + "sslKey": "ldfc:Server#sslKey", + "sslCert": "ldfc:Server#sslCert", + "sslCa": "ldfc:Server#sslCa", + "logging": "ldfc:Server#logging", + "loggingFile": "ldfc:Server#loggingFile" + } + ] +} diff --git a/config/certs/localhost-ca.crt b/packages/core/config/certs/localhost-ca.crt similarity index 100% rename from config/certs/localhost-ca.crt rename to packages/core/config/certs/localhost-ca.crt diff --git a/config/certs/localhost-ca.key b/packages/core/config/certs/localhost-ca.key similarity index 100% rename from config/certs/localhost-ca.key rename to packages/core/config/certs/localhost-ca.key diff --git a/config/certs/localhost-client.crt b/packages/core/config/certs/localhost-client.crt similarity index 100% rename from config/certs/localhost-client.crt rename to packages/core/config/certs/localhost-client.crt diff --git a/config/certs/localhost-client.key b/packages/core/config/certs/localhost-client.key similarity index 100% rename from config/certs/localhost-client.key rename to packages/core/config/certs/localhost-client.key diff --git a/config/certs/localhost-client.p12 b/packages/core/config/certs/localhost-client.p12 similarity index 100% rename from config/certs/localhost-client.p12 rename to packages/core/config/certs/localhost-client.p12 diff --git a/config/certs/localhost-server.crt b/packages/core/config/certs/localhost-server.crt similarity index 100% rename from config/certs/localhost-server.crt rename to packages/core/config/certs/localhost-server.crt diff --git a/config/certs/localhost-server.key b/packages/core/config/certs/localhost-server.key similarity index 100% rename from config/certs/localhost-server.key rename to packages/core/config/certs/localhost-server.key diff --git a/config/certs/make-client-certificates.sh b/packages/core/config/certs/make-client-certificates.sh similarity index 100% rename from config/certs/make-client-certificates.sh rename to packages/core/config/certs/make-client-certificates.sh diff --git a/config/certs/make-server-certificates.sh b/packages/core/config/certs/make-server-certificates.sh similarity index 100% rename from config/certs/make-server-certificates.sh rename to packages/core/config/certs/make-server-certificates.sh diff --git a/config/certs/webid.cnf b/packages/core/config/certs/webid.cnf similarity index 100% rename from config/certs/webid.cnf rename to packages/core/config/certs/webid.cnf diff --git a/config/certs/webid.ttl b/packages/core/config/certs/webid.ttl similarity index 100% rename from config/certs/webid.ttl rename to packages/core/config/certs/webid.ttl diff --git a/packages/core/config/config-example.json b/packages/core/config/config-example.json new file mode 100644 index 00000000..81e7c7fa --- /dev/null +++ b/packages/core/config/config-example.json @@ -0,0 +1,81 @@ +{ + "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@ldf/server/^3.0.0/components/context.jsonld", + "@id": "urn:ldf-server:my", + "import": "preset-qpf:config-defaults.json", + + "title": "My Linked Data Fragments server", + "baseURL": "https://example.org/", + "port": 3000, + "workers": 2, + "protocol": "http", + + "datasources": [ + { + "@id": "urn:ldf-server:myDatasourceVersion1", + "@type": "SparqlDatasource", + "datasourceTitle": "My SPARQL source", + "description": "My datasource with a SPARQL-endpoint back-end", + "datasourcePath": "mysparql", + "sparqlEndpoint": "https://dbpedia.org/sparql", + "enabled": true, + "hide": false, + "license": "MIT", + "licenseUrl": "http://example.org/my-license", + "copyright": "This datasource is owned by Alice", + "homepage": "http://example.org/alice" + }, + { + "@id": "urn:ldf-server:myDatasourceVersion2", + "@type": "TurtleDatasource", + "datasourceTitle": "My Turtle file", + "description": "My dataset with a Turtle back-end", + "datasourcePath": "myttl", + "file": "path/to/file.ttl", + "graph": "http://example.org/default-graph" + } + ], + + "prefixes": [ + { "prefix": "rdf", "uri": "http://www.w3.org/1999/02/22-rdf-syntax-ns#" }, + { "prefix": "rdfs", "uri": "http://www.w3.org/2000/01/rdf-schema#" }, + { "prefix": "owl", "uri": "http://www.w3.org/2002/07/owl#" }, + { "prefix": "xsd", "uri": "http://www.w3.org/2001/XMLSchema#" }, + { "prefix": "hydra", "uri": "http://www.w3.org/ns/hydra/core#" }, + { "prefix": "void", "uri": "http://rdfs.org/ns/void#" }, + { "prefix": "skos", "uri": "http://www.w3.org/2004/02/skos/core#" }, + { "prefix": "dcterms", "uri": "http://purl.org/dc/terms/" }, + { "prefix": "dc11", "uri": "http://purl.org/dc/elements/1.1/" }, + { "prefix": "foaf", "uri": "http://xmlns.com/foaf/0.1/" }, + { "prefix": "geo", "uri": "http://www.w3.org/2003/01/geo/wgs84_pos#" }, + { "prefix": "dbpedia", "uri": "http://dbpedia.org/resource/" }, + { "prefix": "dbpedia-owl", "uri": "http://dbpedia.org/ontology/" }, + { "prefix": "dbpprop", "uri": "http://dbpedia.org/property/" } + ], + + "logging": true, + "loggingFile": "access.log", + + "dereference": [ + { + "dereferenceDatasource": "urn:ldf-server:myDatasourceVersion2", + "dereferencePath": "/resource/" + } + ], + + "responseHeaders": [ + { "headerName": "Access-Control-Allow-Origin", "headerValue": "*" }, + { "headerName": "Access-Control-Allow-Headers", "headerValue": "Accept-Datetime" }, + { "headerName": "Access-Control-Expose-Headers", "headerValue": "Content-Location,Link,Memento-Datetime" } + ], + + "sslKey": "../core/config/certs/localhost-server.key", + "sslCert": "../core/config/certs/localhost-server.crt", + "sslCa": "../core/config/certs/localhost-ca.crt", + + "router": [ + { + "@id": "preset-qpf:sets/routers.json#myPageRouter", + "pageSize": 50 + } + ] +} diff --git a/packages/core/index.js b/packages/core/index.js new file mode 100644 index 00000000..0987c197 --- /dev/null +++ b/packages/core/index.js @@ -0,0 +1,45 @@ +/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ +/* Exports of the components of this package */ + +module.exports = { + controllers: { + AssetsController: require('./lib/controllers/AssetsController'), + Controller: require('./lib/controllers/Controller'), + DereferenceController: require('./lib/controllers/DereferenceController'), + ErrorController: require('./lib/controllers/ErrorController'), + NotFoundController: require('./lib/controllers/NotFoundController'), + }, + datasources: { + Datasource: require('./lib/datasources/Datasource'), + EmptyDatasource: require('./lib/datasources/EmptyDatasource'), + IndexDatasource: require('./lib/datasources/IndexDatasource'), + MemoryDatasource: require('./lib/datasources/MemoryDatasource'), + }, + routers: { + DatasourceRouter: require('./lib/routers/DatasourceRouter'), + PageRouter: require('./lib/routers/PageRouter'), + }, + views: { + error: { + ErrorHtmlView: require('./lib/views/error/ErrorHtmlView'), + ErrorRdfView: require('./lib/views/error/ErrorRdfView'), + }, + forbidden: { + ForbiddenHtmlView: require('./lib/views/forbidden/ForbiddenHtmlView'), + }, + notfound: { + NotFoundHtmlView: require('./lib/views/notfound/NotFoundHtmlView'), + NotFoundRdfView: require('./lib/views/notfound/NotFoundRdfView'), + }, + HtmlView: require('./lib/views/HtmlView'), + RdfView: require('./lib/views/RdfView'), + View: require('./lib/views/View'), + ViewCollection: require('./lib/views/ViewCollection'), + }, + runCli: require('./lib/CliRunner').runCli, + runCustom: require('./lib/CliRunner').runCustom, + LinkedDataFragmentsServer: require('./lib/LinkedDataFragmentsServer'), + LinkedDataFragmentsServerWorker: require('./lib/LinkedDataFragmentsServerWorker'), + UrlData: require('./lib/UrlData'), + Util: require('./lib/Util'), +}; diff --git a/packages/core/lib/CliRunner.js b/packages/core/lib/CliRunner.js new file mode 100644 index 00000000..655f2377 --- /dev/null +++ b/packages/core/lib/CliRunner.js @@ -0,0 +1,118 @@ +/*! @license MIT ©2013-2017 Ruben Verborgh and Ruben Taelman, Ghent University - imec */ +/* Logic for starting an LDF server with a given config from the command line. */ + +let cluster = require('cluster'), + ComponentsManager = require('componentsjs').ComponentsManager; + +// Run function for starting the server from the command line +function runCli(moduleRootPath) { + let argv = process.argv.slice(2); + runCustom(argv, process.stdin, process.stdout, process.stderr, null, { mainModulePath: moduleRootPath }); +} + +// Generic run function for starting the server from a given config +function runCustom(args, stdin, stdout, stderr, componentConfigUri, properties) { + if (args.length < 1 || args.length > 4 || /^--?h(elp)?$/.test(args[0])) { + stdout.write('usage: server config.json [port [workers [componentConfigUri]]]\n'); + return process.exit(1); + } + + let cliPort = parseInt(args[1], 10), + cliWorkers = parseInt(args[2], 10), + configUri = args[3] || componentConfigUri || 'urn:ldf-server:my'; + + ComponentsManager.build({ + ...properties, + configLoader: (registry) => registry.register(args[0]), + }) + .then((manager) => { + return manager.instantiate(configUri) + .then((worker) => { + if (cluster.isMaster) + startClusterMaster(worker._config); + else + worker.run(cliPort); + }) + .catch((e) => { + stderr.write('Instantiation error:\n'); + stderr.write(e.stack + '\n'); + process.exit(1); + }); + }) + .catch((e) => { + stderr.write('Component definition error:\n'); + stderr.write(e.stack + '\n'); + process.exit(1); + }); + + function startClusterMaster(config) { + let workers = cliWorkers || config.workers || 1; + + // Create workers + stdout.write('Master ' + process.pid + ' running.\n'); + for (let i = 0; i < workers; i++) + cluster.fork(); + + // Respawn crashed workers + cluster.on('listening', (worker) => { + worker.once('exit', (code, signal) => { + if (!worker.exitedAfterDisconnect) { + stdout.write('Worker ' + worker.process.pid + 'died with ' + (code || signal) + '. Starting new worker.\n'); + cluster.fork(); + } + }); + }); + + // Disconnect from cluster on SIGINT, so that the process can cleanly terminate + process.once('SIGINT', () => { + cluster.disconnect(); + }); + + // Respawn workers one by one when receiving a SIGHUP signal + process.on('SIGHUP', function respawn() { + stdout.write('Respawning workers of master ' + process.pid + '.\n'); + process.addListener('SIGHUP', respawnPending); + process.removeListener('SIGHUP', respawn); + + // Retrieve a list of old workers that will be replaced by new ones + let workers = Object.keys(cluster.workers).map((id) => { return cluster.workers[id]; }); + (function respawnNext() { + // If there are still old workers, respawn a new one + if (workers.length) { + // Wait until the new worker starts listening to kill the old one + let newWorker = cluster.fork(); + newWorker.once('listening', () => { + let worker = workers.pop(); + if (!worker) + return newWorker.kill(), respawnNext(); // Dead workers are replaced automatically + worker.once('exit', () => { + stdout.write('Worker ' + newWorker.process.pid + ' replaces killed worker ' + worker.process.pid + '.\n'); + respawnNext(); + }); + worker.kill(); + newWorker.removeListener('exit', abort); + }); + // Abort the respawning process if creating a new worker fails + newWorker.on('exit', abort); + function abort(code, signal) { + if (!newWorker.suicide) { + stdout.write('Respawning aborted because worker ' + newWorker.process.pid + ' died with ' + + (code || signal) + '.\n'); + process.addListener('SIGHUP', respawn); + process.removeListener('SIGHUP', respawnPending); + } + } + } + // No old workers left, so respawning has finished + else { + process.addListener('SIGHUP', respawn); + process.removeListener('SIGHUP', respawnPending); + stdout.write('Respawned all workers of master ' + process.pid + '.\n'); + } + })(); + function respawnPending() { stdout.write('Respawning already in progress\n'); } + }); + } +} + +module.exports = { runCli: runCli, runCustom: runCustom }; diff --git a/lib/LinkedDataFragmentsServer.js b/packages/core/lib/LinkedDataFragmentsServer.js similarity index 57% rename from lib/LinkedDataFragmentsServer.js rename to packages/core/lib/LinkedDataFragmentsServer.js index a6a7e1e4..d67ff3cb 100644 --- a/lib/LinkedDataFragmentsServer.js +++ b/packages/core/lib/LinkedDataFragmentsServer.js @@ -1,60 +1,65 @@ /*! @license MIT ©2014-2016 Ruben Verborgh, Ghent University - imec */ /* LinkedDataFragmentsServer is an HTTP server that provides access to Linked Data Fragments */ -var _ = require('lodash'), +let _ = require('lodash'), fs = require('fs'), Util = require('./Util'), - ErrorController = require('./controllers/ErrorController'); + ErrorController = require('./controllers/ErrorController'), + UrlData = require('./UrlData'); // Creates a new LinkedDataFragmentsServer -function LinkedDataFragmentsServer(options) { - // Create the HTTP(S) server - var server, sockets = 0; - switch (options.protocol) { - case 'http': - server = require('http').createServer(); - break; - case 'https': - var ssl = options.ssl || {}, authentication = options.authentication || {}; - // WebID authentication requires a client certificate - if (authentication.webid) - ssl.requestCert = ssl.rejectUnauthorized = true; - server = require('https').createServer(_.assign(ssl, _.mapValues(ssl.keys, readHttpsOption))); - break; - default: - throw new Error('The configured protocol ' + options.protocol + ' is invalid.'); - } - // Copy over members - for (var member in LinkedDataFragmentsServer.prototype) - server[member] = LinkedDataFragmentsServer.prototype[member]; +class LinkedDataFragmentsServer { + constructor(options) { + // Create the HTTP(S) server + let server, sockets = 0; + let urlData = options && options.urlData ? options.urlData : new UrlData(); + switch (urlData.protocol) { + case 'http': + server = require('http').createServer(); + break; + case 'https': + const ssl = options.ssl || {}, authentication = options.authentication || {}; + // WebID authentication requires a client certificate + if (authentication.webid) + ssl.requestCert = ssl.rejectUnauthorized = true; + server = require('https').createServer({ ...ssl, ..._.mapValues(ssl.keys, readHttpsOption) }); + break; + default: + throw new Error('The configured protocol ' + urlData.protocol + ' is invalid.'); + } + + // Copy over members + for (let member in LinkedDataFragmentsServer.prototype) + server[member] = LinkedDataFragmentsServer.prototype[member]; - // Assign settings - server._sockets = {}; - server._log = options.log || _.noop; - server._accesslogger = options.accesslogger || _.noop; - server._controllers = options.controllers || []; - server._errorController = new ErrorController(options); - server._defaultHeaders = options.response && options.response.headers || {}; + // Assign settings + server._sockets = {}; + server._log = options.log || _.noop; + server._accesslogger = options.accesslogger || _.noop; + server._controllers = options.controllers || []; + server._errorController = new ErrorController(options); + server._defaultHeaders = options.response && options.response.headers || {}; - // Attach event listeners - server.on('error', function (error) { server._reportError(error); }); - server.on('request', function (request, response) { - server._accesslogger(request, response); - try { server._processRequest(request, response); } - catch (error) { server._reportError(request, response, error); } - }); - server.on('connection', function (socket) { - var socketId = sockets++; - server._sockets[socketId] = socket; - socket.on('close', function () { delete server._sockets[socketId]; }); - }); - return server; + // Attach event listeners + server.on('error', (error) => { server._reportError(error); }); + server.on('request', (request, response) => { + server._accesslogger(request, response); + try { server._processRequest(request, response); } + catch (error) { server._reportError(request, response, error); } + }); + server.on('connection', (socket) => { + let socketId = sockets++; + server._sockets[socketId] = socket; + socket.on('close', () => { delete server._sockets[socketId]; }); + }); + return server; + } } // Handles an incoming HTTP request LinkedDataFragmentsServer.prototype._processRequest = function (request, response) { // Add default response headers - for (var header in this._defaultHeaders) + for (let header in this._defaultHeaders) response.setHeader(header, this._defaultHeaders[header]); // Verify an allowed HTTP method was used @@ -76,7 +81,7 @@ LinkedDataFragmentsServer.prototype._processRequest = function (request, respons } // Try each of the controllers in order - var self = this, controllerId = 0; + let self = this, controllerId = 0; function nextController(error) { // Error if the previous controller failed if (error) @@ -86,12 +91,12 @@ LinkedDataFragmentsServer.prototype._processRequest = function (request, respons response.emit('error', new Error('No controller for ' + request.url)); // Otherwise, try the next controller else { - var controller = self._controllers[controllerId++], next = _.once(nextController); + let controller = self._controllers[controllerId++], next = _.once(nextController); try { controller.handleRequest(request, response, next); } catch (error) { next(error); } } } - response.on('error', function (error) { self._reportError(request, response, error); }); + response.on('error', (error) => { self._reportError(request, response, error); }); nextController(); }; @@ -122,7 +127,7 @@ LinkedDataFragmentsServer.prototype._reportError = function (request, response, LinkedDataFragmentsServer.prototype.stop = function () { // Don't accept new connections, and close existing ones this.close(); - for (var id in this._sockets) + for (let id in this._sockets) this._sockets[id].destroy(); // Close all controllers @@ -135,10 +140,10 @@ LinkedDataFragmentsServer.prototype.stop = function () { // Reads the value of an option for the https module function readHttpsOption(value) { // Read each value of an array - if (_.isArray(value)) + if (Array.isArray(value)) return value.map(readHttpsOption); // Certificates and keys can be strings or files - else if (_.isString(value) && fs.existsSync(value)) + else if (typeof value === 'string' && fs.existsSync(value)) return fs.readFileSync(value); // Other strings and regular objects are also allowed else diff --git a/packages/core/lib/LinkedDataFragmentsServerWorker.js b/packages/core/lib/LinkedDataFragmentsServerWorker.js new file mode 100644 index 00000000..3d5d8ea3 --- /dev/null +++ b/packages/core/lib/LinkedDataFragmentsServerWorker.js @@ -0,0 +1,93 @@ +/*! @license MIT ©2014-2017 Ruben Verborgh and Ruben Taelman, Ghent University - imec */ +/* LinkedDataFragmentsServerRunner is able to run a Linked Data Fragments server */ + +let _ = require('lodash'), + fs = require('fs'), + LinkedDataFragmentsServer = require('./LinkedDataFragmentsServer'); + +// Creates a new LinkedDataFragmentsServerWorker +class LinkedDataFragmentsServerWorker { + constructor(config) { + if (!config.datasources) + throw new Error('At least one datasource must be defined.'); + if (!config.controllers) + throw new Error('At least one controller must be defined.'); + if (!config.routers) + throw new Error('At least one router must be defined.'); + + // Create all data sources + Object.keys(config.datasources).forEach((datasourceId) => { + let datasource = config.datasources[datasourceId]; + datasource.on('error', datasourceError); + function datasourceError(error) { + config.datasources[datasourceId].hide = true; + process.stderr.write('WARNING: skipped datasource ' + datasourceId + '. ' + error.message + '\n'); + } + }); + + // Set up logging + let loggingSettings = config.logging; + // eslint-disable-next-line no-console + config.log = console.log; + if (loggingSettings.enabled) { + let accesslog = require('access-log'); + config.accesslogger = function (request, response) { + accesslog(request, response, null, (logEntry) => { + fs.appendFile(loggingSettings.file, logEntry + '\n', (error) => { + error && process.stderr.write('Error when writing to access log file: ' + error); + }); + }); + }; + } + + // Make sure the 'last' controllers are last in the array and the 'first' are first. + let lastControllers = _.remove(config.controllers, (controller) => { + return controller._last; + }); + let firstControllers = _.remove(config.controllers, (controller) => { + return controller._first; + }); + config.controllers = firstControllers.concat(config.controllers.concat(lastControllers)); + + this._config = config; + } + + // Start the worker + run(port) { + let config = this._config; + if (port) + config.port = port; + let server = new LinkedDataFragmentsServer(config); + + // Start the server when all data sources are ready + let pending = Object.keys(config.datasources).length; + for (const datasourceId in config.datasources) { + const datasource = config.datasources[datasourceId]; + // Add datasource ready-listener + let ready = _.once(startWhenReady); + datasource.once('initialized', ready); + datasource.once('error', ready); + + // Init datasource asynchronously + datasource.initialize(); + } + function startWhenReady() { + if (!--pending) { + server.listen(config.port); + // eslint-disable-next-line no-console + console.log('Worker %d running on %s://localhost:%d/ (URL: %s).', + process.pid, config.urlData.protocol, config.port, config.urlData.baseURL); + } + } + + // Terminate gracefully if possible + process.once('SIGINT', () => { + // eslint-disable-next-line no-console + console.log('Stopping worker', process.pid); + server.stop(); + process.on('SIGINT', () => { process.exit(1); }); + }); + } +} + +module.exports = LinkedDataFragmentsServerWorker; diff --git a/packages/core/lib/UrlData.js b/packages/core/lib/UrlData.js new file mode 100644 index 00000000..07ced698 --- /dev/null +++ b/packages/core/lib/UrlData.js @@ -0,0 +1,24 @@ +/*! @license MIT ©2015-2017 Ruben Verborgh and Ruben Taelman, Ghent University - imec */ +/* A data object class for preset URL information */ + +// Creates a new UrlData +class UrlData { + constructor(options) { + // Configure preset URLs + options = options || {}; + this.baseURL = (options.baseURL || '/').replace(/\/?$/, '/'); + this.baseURLRoot = this.baseURL.match(/^(?:https?:\/\/[^\/]+)?/)[0]; + this.baseURLPath = this.baseURL.substr(this.baseURLRoot.length); + this.blankNodePath = this.baseURLRoot ? '/.well-known/genid/' : ''; + this.blankNodePrefix = this.blankNodePath ? this.baseURLRoot + this.blankNodePath : 'genid:'; + this.blankNodePrefixLength = this.blankNodePrefix.length; + this.assetsPath = this.baseURLPath + 'assets/' || options.assetsPath; + this.protocol = options.protocol; + if (!this.protocol) { + let protocolMatch = (this.baseURL || '').match(/^(\w+):/); + this.protocol = protocolMatch ? protocolMatch[1] : 'http'; + } + } +} + +module.exports = UrlData; diff --git a/lib/Util.js b/packages/core/lib/Util.js similarity index 93% rename from lib/Util.js rename to packages/core/lib/Util.js index 87ae231d..4fed774a 100644 --- a/lib/Util.js +++ b/packages/core/lib/Util.js @@ -13,7 +13,7 @@ module.exports.createErrorType = function (BaseError, name, init) { if (typeof BaseError !== 'function') init = name, name = BaseError, BaseError = Error; function ErrorType(message) { - var error = this instanceof ErrorType ? this : new ErrorType(message); + let error = this instanceof ErrorType ? this : new ErrorType(message); error.name = name; error.message = message || ''; Error.captureStackTrace(error, error.constructor); diff --git a/packages/core/lib/controllers/AssetsController.js b/packages/core/lib/controllers/AssetsController.js new file mode 100644 index 00000000..c40629d6 --- /dev/null +++ b/packages/core/lib/controllers/AssetsController.js @@ -0,0 +1,63 @@ +/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ +/* An AssetsController responds to requests for assets */ + +let Controller = require('./Controller'), + fs = require('fs'), + path = require('path'), + mime = require('mime'), + Util = require('../Util'), + UrlData = require('../UrlData'); + +// Creates a new AssetsController +class AssetsController extends Controller { + constructor(options) { + options = options || {}; + super(options); + + // Set up path matching + let assetsPath = (options.urlData || new UrlData()).assetsPath || '/assets/'; + this._matcher = new RegExp('^' + Util.toRegExp(assetsPath) + '(.+)|^/(\\w*)\\.ico$'); + + // Read all assets + let assetsFolders = options.assetsFolders || ['file:///' + path.join(__dirname, '../../assets/')]; + this._assets = {}; + for (let i = 0; i < assetsFolders.length; i++) + this._readAssetsFolder(assetsFolders[i], ''); + } + + // Recursively reads assets in the folder, assigning them to the URL path + _readAssetsFolder(assetsFolder, assetsPath) { + if (assetsFolder.indexOf('file:///') === 0) + assetsFolder = assetsFolder.replace('file:///', ''); + fs.readdirSync(assetsFolder).forEach(function (name) { + let filename = path.join(assetsFolder, name), stats = fs.statSync(filename); + // Read an asset file into memory + if (stats.isFile()) { + let assetType = mime.getType(filename); + this._assets[assetsPath + name.replace(/[.][^.]+$/, '')] = { + type: assetType.indexOf('text/') ? assetType : assetType + ';charset=utf-8', + contents: fs.readFileSync(filename), + }; + } + // Read all asset files in a folder + else if (stats.isDirectory()) + this._readAssetsFolder(filename, assetsPath + name + '/'); + }, this); + } + + // Try to serve the requested asset + _handleRequest(request, response, next) { + let assetMatch = request.url.match(this._matcher), asset; + if (asset = assetMatch && this._assets[assetMatch[1] || assetMatch[2]]) { + response.writeHead(200, { + 'Content-Type': asset.type, + 'Cache-Control': 'public,max-age=1209600', // 14 days + }); + response.end(asset.contents); + } + else + next(); + } +} + +module.exports = AssetsController; diff --git a/packages/core/lib/controllers/Controller.js b/packages/core/lib/controllers/Controller.js new file mode 100644 index 00000000..6d43c24f --- /dev/null +++ b/packages/core/lib/controllers/Controller.js @@ -0,0 +1,106 @@ +/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ +/* Controller is a base class for HTTP request handlers */ + +let url = require('url'), + _ = require('lodash'), + ViewCollection = require('../views/ViewCollection'), + UrlData = require('../UrlData'), + Util = require('../Util'), + parseForwarded = require('forwarded-parse'); + +// Creates a new Controller +class Controller { + constructor(options) { + options = options || {}; + this._prefixes = options.prefixes || {}; + this._datasources = _.reduce(options.datasources || {}, (datasources, value, key) => { + // If the path does not start with a slash, add one. + datasources[key.replace(/^(?!\/)/, '/')] = value; + return datasources; + }, {}); + this._views = options.views && options.views.matchView ? + options.views : new ViewCollection(options.views); + + // Set up base URL (if we're behind a proxy, this allows reconstructing the actual request URL) + this._baseUrl = _.mapValues(url.parse((options.urlData || new UrlData()).baseURL), (value, key) => { + return value && !/^(?:href|path|search|hash)$/.test(key) ? value : undefined; + }); + } + + // Tries to process the HTTP request + handleRequest(request, response, next, settings) { + // Add a `parsedUrl` field to `request`, + // containing the parsed request URL, resolved against the base URL + if (!request.parsedUrl) { + // Keep the request's path and query, but take over all other defined baseURL properties + request.parsedUrl = _.defaults(_.pick(url.parse(request.url, true), 'path', 'pathname', 'query'), + this._getForwarded(request), + this._getXForwardHeaders(request), + this._baseUrl, + { protocol: 'http:', host: request.headers.host }); + } + + // Try to handle the request + let self = this; + try { this._handleRequest(request, response, done, settings); } + catch (error) { done(error); } + function done(error) { + if (self) { + // Send a 406 response if no suitable view was found + if (error instanceof ViewCollection.ViewCollectionError) + return self._handleNotAcceptable(request, response, next); + self = null; + next(error); + } + } + } + + // Get host and protocol from HTTP's Forwarded header + _getForwarded(request) { + if (!request.headers.forwarded) + return {}; + try { + let forwarded = _.defaults.apply(this, parseForwarded(request.headers.forwarded)); + return { + protocol: forwarded.proto ? forwarded.proto + ':' : undefined, + host: forwarded.host, + }; + } + catch (error) { return {}; } + } + + // Get host and protocol from HTTP's X-Forwarded-* headers + _getXForwardHeaders(request) { + return { + protocol: request.headers['x-forwarded-proto'] ? request.headers['x-forwarded-proto'] + ':' : undefined, + host: request.headers['x-forwarded-host'], + }; + } + + // Tries to process the HTTP request in an implementation-specific way + _handleRequest(request, response, next, settings) { + next(); + } + + // Serves an error indicating content negotiation failure + _handleNotAcceptable(request, response, next) { + response.writeHead(406, { 'Content-Type': Util.MIME_PLAINTEXT }); + response.end('No suitable content type found.\n'); + } + + // Finds an appropriate view using content negotiation + _negotiateView(viewName, request, response) { + // Indicate that the response is content-negotiated + let vary = response.getHeader('Vary'); + response.setHeader('Vary', 'Accept' + (vary ? ', ' + vary : '')); + // Negotiate a view + let viewMatch = this._views.matchView(viewName, request); + response.setHeader('Content-Type', viewMatch.responseType || viewMatch.type); + return viewMatch.view; + } + + // Cleans resources used by the controller + close() { } +} + +module.exports = Controller; diff --git a/packages/core/lib/controllers/DereferenceController.js b/packages/core/lib/controllers/DereferenceController.js new file mode 100644 index 00000000..086935f1 --- /dev/null +++ b/packages/core/lib/controllers/DereferenceController.js @@ -0,0 +1,36 @@ +/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ +/* A DeferenceController responds to dereferencing requests */ + +let Controller = require('./Controller'), + url = require('url'), + _ = require('lodash'), + Util = require('../Util'); + +// Creates a new DeferenceController +class DeferenceController extends Controller { + constructor(options) { + options = options || {}; + super(options); + let paths = this._paths = options.dereference || {}; + this._matcher = /$0^/; + if (!_.isEmpty(paths)) + this._matcher = new RegExp('^(' + Object.keys(paths).map(Util.toRegExp).join('|') + ')'); + } + + // Dereferences a URL by redirecting to its subject fragment of a certain data source + _handleRequest(request, response, next) { + let match = this._matcher.exec(request.url), datasource; + if (datasource = match && this._paths[match[1]]) { + let entity = url.format(_.defaults({ + pathname: datasource.path, + query: { subject: url.format(request.parsedUrl) }, + }, request.parsedUrl)); + response.writeHead(303, { 'Location': entity, 'Content-Type': Util.MIME_PLAINTEXT }); + response.end(entity); + } + else + next(); + } +} + +module.exports = DeferenceController; diff --git a/packages/core/lib/controllers/ErrorController.js b/packages/core/lib/controllers/ErrorController.js new file mode 100644 index 00000000..4a5e049c --- /dev/null +++ b/packages/core/lib/controllers/ErrorController.js @@ -0,0 +1,30 @@ +/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ +/* An ErrorController responds to requests that caused an error */ + +let Controller = require('./Controller'), + Util = require('../Util'); + +// Creates a new ErrorController +class ErrorController extends Controller { + constructor(options) { + super(options); + } + + // Serves an error response + _handleRequest(request, response, next) { + // Try to write an error response through an appropriate view + let error = response.error || (response.error = new Error('Unknown error')), + view = this._negotiateView('Error', request, response), + metadata = { prefixes: this._prefixes, datasources: this._datasources, error: error }; + response.writeHead(500); + view.render(metadata, request, response); + } + + // Writes the error in plaintext if no view was found + _handleNotAcceptable(request, response, next) { + response.writeHead(500, { 'Content-Type': Util.MIME_PLAINTEXT }); + response.end('Application error: ' + response.error.message + '\n'); + } +} + +module.exports = ErrorController; diff --git a/packages/core/lib/controllers/NotFoundController.js b/packages/core/lib/controllers/NotFoundController.js new file mode 100644 index 00000000..3f3f83ae --- /dev/null +++ b/packages/core/lib/controllers/NotFoundController.js @@ -0,0 +1,33 @@ +/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ +/* A NotFoundController responds to requests that cannot be resolved */ + +let Controller = require('./Controller'), + Util = require('../Util'); + +// Creates a new NotFoundController +class NotFoundController extends Controller { + constructor(options) { + super(options); + this._last = true; + } + + // Serves a 404 response + _handleRequest(request, response, next) { + // Cache 404 responses + response.setHeader('Cache-Control', 'public,max-age=3600'); + + // Render the 404 message using the appropriate view + let view = this._negotiateView('NotFound', request, response), + metadata = { url: request.url, prefixes: this._prefixes, datasources: this._datasources }; + response.writeHead(404); + view.render(metadata, request, response); + } + + // Writes the 404 in plaintext if no view was found + _handleNotAcceptable(request, response, next) { + response.writeHead(404, { 'Content-Type': Util.MIME_PLAINTEXT }); + response.end(request.url + ' not found\n'); + } +} + +module.exports = NotFoundController; diff --git a/packages/core/lib/datasources/Datasource.js b/packages/core/lib/datasources/Datasource.js new file mode 100644 index 00000000..05324766 --- /dev/null +++ b/packages/core/lib/datasources/Datasource.js @@ -0,0 +1,211 @@ +/*! @license MIT ©2014-2016 Ruben Verborgh, Ghent University - imec */ +/* A Datasource provides base functionality for queryable access to a source of quads. */ + +let fs = require('fs'), + UrlData = require('../UrlData'), + BufferedIterator = require('asynciterator').BufferedIterator, + EventEmitter = require('events'), + stringToTerm = require('rdf-string').stringToTerm; + +// Creates a new Datasource +class Datasource extends EventEmitter { + constructor(options, supportedFeatureList) { + super(); + + // Set the options + options = options || {}; + this.urlData = options.urlData || new UrlData(); + let path = (options.path || '').replace(/^\//, ''); + this._datasourcePath = this.urlData.baseURLPath + encodeURI(path); + this._skolemizeBlacklist = options.skolemizeBlacklist || {}; + this.title = options.title; + this.id = options.id; + this.hide = options.hide; + this.enabled = options.enabled !== false; + if (this.enabled === false) + this.hide = true; + this.description = options.description; + this.path = this._datasourcePath; + this.url = this.urlData.baseURLRoot + this._datasourcePath + '#dataset'; + this.license = options.license; + this.licenseUrl = options.licenseUrl; + this.copyright = options.copyright; + this.homepage = options.homepage; + this._request = options.request || require('request'); + this.dataFactory = options.dataFactory; + if (options.graph) { + this._graph = this.dataFactory.namedNode(options.graph); + this._queryGraphReplacements = Object.create(null); + this._queryGraphReplacements[''] = 'urn:ldf:emptyGraph'; + this._queryGraphReplacements[options.graph] = ''; + } + this._supportsQuads = 'quads' in options ? options.quads : true; + + // Whether the datasource can be queried + this.initialized = false; + + // Expose the supported query features + if (supportedFeatureList && supportedFeatureList.length) { + let objectSupportedFeatures = {}; + for (let i = 0; i < supportedFeatureList.length; i++) + objectSupportedFeatures[supportedFeatureList[i]] = true; + this.supportedFeatures = objectSupportedFeatures; + } + else + this.supportedFeatures = {}; + if (!this._supportsQuads) + delete this.supportedFeatures.quadPattern; + Object.freeze(this.supportedFeatures); + } + + + // Initialize the datasource asynchronously + initialize() { + if (!this.enabled) { + this.initialized = true; + return this.emit('initialized'); + } + + try { + this._initialize() + .then(() => { + this.initialized = true; + this.emit('initialized'); + }) + .catch((error) => this.emit('error', error)); + } + catch (error) { + this.emit('error', error); + } + } + + // Prepares the datasource for querying + async _initialize() { + + } + + // Checks whether the data source can evaluate the given query + supportsQuery(query) { + // An uninitialized datasource does not support any query + if (!this.initialized) + return false; + + // A query is supported if the data source supports all of its features + let features = query.features, supportedFeatures = this.supportedFeatures, feature; + if (features) { + for (feature in features) { + if (features[feature] && !supportedFeatures[feature]) + return false; + } + return true; + } + // A query without features is supported if this data source has at least one feature + else { + for (feature in supportedFeatures) { + if (supportedFeatures[feature]) + return true; + } + return false; + } + } + + // Selects the quads that match the given query, returning a quad stream + select(query, onError) { + if (!this.initialized) + return onError && onError(new Error('The datasource is not initialized yet')); + if (!this.supportsQuery(query)) + return onError && onError(new Error('The datasource does not support the given query')); + query = { ...query }; + + // Translate blank nodes IRIs in the query to blank nodes + let blankNodePrefix = this.urlData.blankNodePrefix, blankNodePrefixLength = this.urlData.blankNodePrefixLength; + if (query.subject && query.subject.termType === 'NamedNode' && query.subject.value.indexOf(blankNodePrefix) === 0) + query.subject = this.dataFactory.blankNode(query.subject.value.substr(blankNodePrefixLength)); + if (query.object && query.object.termType === 'NamedNode' && query.object.value.indexOf(blankNodePrefix) === 0) + query.object = this.dataFactory.blankNode(query.object.value.substr(blankNodePrefixLength)); + if (query.graph && query.graph.termType === 'NamedNode' && query.graph.value.indexOf(blankNodePrefix) === 0) + query.graph = this.dataFactory.blankNode(query.graph.value.substr(blankNodePrefixLength)); + + // Force the default graph if QPF support is disable + if (!this._supportsQuads) + query.graph = this.dataFactory.defaultGraph(); + + // If a custom default graph was set, query it as the default graph + if (this._graph && query.graph && query.graph.value in this._queryGraphReplacements) + query.graph = stringToTerm(this._queryGraphReplacements[query.graph.value], this.dataFactory); + + // Transform the received quads + let destination = new BufferedIterator(), outputQuads, defaultGraph = this._graph; + outputQuads = destination.map((quad) => { + let { subject, predicate, object, graph } = quad; + // Translate blank nodes in the result to blank node IRIs. + if (quad.subject && quad.subject.termType === 'BlankNode' && !this._skolemizeBlacklist[quad.subject.value]) + subject = this.dataFactory.namedNode(blankNodePrefix + quad.subject.value); + if (quad.object && quad.object.termType === 'BlankNode' && !this._skolemizeBlacklist[quad.object.value]) + object = this.dataFactory.namedNode(blankNodePrefix + quad.object.value); + if (quad.graph && quad.graph.termType === 'BlankNode' && !this._skolemizeBlacklist[quad.graph.value]) + graph = this.dataFactory.namedNode(blankNodePrefix + quad.graph.value); + // If a custom default graph was set, move default graph triples there. + else if (defaultGraph && (!quad.graph || quad.graph.termType === 'DefaultGraph')) + graph = defaultGraph; + return this.dataFactory.quad(subject, predicate, object, graph); + }); + outputQuads.copyProperties(destination, ['metadata']); + onError && outputQuads.on('error', onError); + + // Execute the query + try { this._executeQuery(query, destination); } + catch (error) { outputQuads.emit('error', error); } + return outputQuads; + } + + // Writes the results of the query to the given destination + _executeQuery(query, destination) { + throw new Error('_executeQuery has not been implemented'); + } + + // Retrieves a stream through HTTP or the local file system + _fetch(options) { + let stream, + url = options.url, protocolMatch = /^(?:([a-z]+):)?/.exec(url); + switch (protocolMatch[1] || 'file') { + // Fetch a representation through HTTP(S) + case 'http': + case 'https': + stream = this._request(options); + stream.on('response', (response) => { + if (response.statusCode >= 300) { + setImmediate(() => { + stream.emit('error', new Error(url + ' returned ' + response.statusCode)); + }); + } + }); + break; + // Read a file from the local filesystem + case 'file': + stream = fs.createReadStream(url.substr(protocolMatch[0].length), { encoding: 'utf8' }); + break; + default: + stream = new EventEmitter(); + setImmediate(() => { + stream.emit('error', new Error('Unknown protocol: ' + protocolMatch[1])); + }); + } + + // If the stream has no other error handlers attached (besides this one), + // emit the stream error as a datasource error + stream.on('error', (error) => { + if (stream.listenerCount('error') === 1) + this.emit('error', error); + }); + return stream; + } + + // Closes the data source, freeing possible resources used + close(callback) { + callback && callback(); + } +} + + +module.exports = Datasource; diff --git a/packages/core/lib/datasources/EmptyDatasource.js b/packages/core/lib/datasources/EmptyDatasource.js new file mode 100644 index 00000000..841b4257 --- /dev/null +++ b/packages/core/lib/datasources/EmptyDatasource.js @@ -0,0 +1,16 @@ +/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ +/* An empty data source doesn't contain any quads. */ + +let MemoryDatasource = require('./MemoryDatasource'); + +// Creates a new EmptyDatasource +class EmptyDatasource extends MemoryDatasource { + constructor(options) { + super(options); + } + + // Retrieves all quads in the datasource + _getAllQuads(addQuad, done) { done(); } +} + +module.exports = EmptyDatasource; diff --git a/packages/core/lib/datasources/IndexDatasource.js b/packages/core/lib/datasources/IndexDatasource.js new file mode 100644 index 00000000..2e58e36c --- /dev/null +++ b/packages/core/lib/datasources/IndexDatasource.js @@ -0,0 +1,37 @@ +/*! @license MIT ©2014-2016 Ruben Verborgh, Ghent University - imec */ +/* An IndexDatasource is a datasource that lists other data sources. */ + +let MemoryDatasource = require('./MemoryDatasource'); + +let rdf = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + rdfs = 'http://www.w3.org/2000/01/rdf-schema#', + dc = 'http://purl.org/dc/terms/', + voID = 'http://rdfs.org/ns/void#'; + +// Creates a new IndexDatasource +class IndexDatasource extends MemoryDatasource { + constructor(options) { + super(options); + this._datasources = options ? options.datasources : {}; + this.role = 'index'; + delete this._datasources['/']; + } + + // Creates quads for each data source + _getAllQuads(addQuad, done) { + const quad = this.dataFactory.quad, namedNode = this.dataFactory.namedNode, literal = this.dataFactory.literal; + for (let name in this._datasources) { + let datasource = this._datasources[name], datasourceUrl = datasource.url; + if (!datasource.hide && datasourceUrl) { + addQuad(quad(namedNode(datasourceUrl), namedNode(rdf + 'type'), namedNode(voID + 'Dataset'))); + datasource.title && addQuad(quad(namedNode(datasourceUrl), namedNode(rdfs + 'label'), literal(datasource.title))); + datasource.title && addQuad(quad(namedNode(datasourceUrl), namedNode(dc + 'title'), literal(datasource.title))); + datasource.description && addQuad(quad(namedNode(datasourceUrl), namedNode(dc + 'description'), literal(datasource.description))); + } + } + delete this._datasources; + done(); + } +} + +module.exports = IndexDatasource; diff --git a/packages/core/lib/datasources/MemoryDatasource.js b/packages/core/lib/datasources/MemoryDatasource.js new file mode 100644 index 00000000..da540e72 --- /dev/null +++ b/packages/core/lib/datasources/MemoryDatasource.js @@ -0,0 +1,50 @@ +/*! @license MIT ©2014-2015 Ruben Verborgh and Ruben Taelman, Ghent University - imec */ +/* A MemoryDatasource queries a set of in-memory quads. */ + +let Datasource = require('./Datasource'), + N3Store = require('n3').Store; + +// Creates a new MemoryDatasource +class MemoryDatasource extends Datasource { + constructor(options) { + let supportedFeatureList = ['quadPattern', 'triplePattern', 'limit', 'offset', 'totalCount']; + super(options, supportedFeatureList); + if (options.file) { + if (!options.file.startsWith('file://') && !options.file.startsWith('http://') && !options.file.startsWith('https://')) + options.file = `file://${options.file}`; + } + + this._url = options && (options.url || options.file); + } + + // Prepares the datasource for querying + _initialize(done) { + return new Promise((resolve, reject) => { + let quadStore = this._quadStore = new N3Store(); + this._getAllQuads((quad) => { quadStore.addQuad(quad); }, (error) => { + if (error) + return reject(error); + return resolve(); + }); + }); + } + + // Retrieves all quads in the datasource + _getAllQuads(addQuad, done) { + throw new Error('_getAllQuads is not implemented'); + } + + // Writes the results of the query to the given quad stream + _executeQuery(query, destination) { + let offset = query.offset || 0, limit = query.limit || Infinity, + quads = this._quadStore.getQuads(query.subject, query.predicate, query.object, query.graph); + // Send the metadata + destination.setProperty('metadata', { totalCount: quads.length, hasExactCount: true }); + // Send the requested subset of quads + for (let i = offset, l = Math.min(offset + limit, quads.length); i < l; i++) + destination._push(quads[i]); + destination.close(); + } +} + +module.exports = MemoryDatasource; diff --git a/packages/core/lib/routers/DatasourceRouter.js b/packages/core/lib/routers/DatasourceRouter.js new file mode 100644 index 00000000..4796dfe8 --- /dev/null +++ b/packages/core/lib/routers/DatasourceRouter.js @@ -0,0 +1,21 @@ +/*! @license MIT ©2014-2016 Ruben Verborgh, Ghent University - imec */ +/* A DatasourceRouter routes URLs to data sources. */ + +let UrlData = require('../UrlData'); + +// Creates a new DatasourceRouter +class DatasourceRouter { + constructor(options) { + let urlData = options && options.urlData || new UrlData(); + this._baseLength = urlData.baseURLPath.length - 1; + } + + // Extracts the data source parameter from the request and adds it to the query + extractQueryParams(request, query) { + (query.features || (query.features = {})).datasource = true; + let path = request.url && request.url.pathname || '/'; + query.datasource = path.substr(this._baseLength); + } +} + +module.exports = DatasourceRouter; diff --git a/packages/core/lib/routers/PageRouter.js b/packages/core/lib/routers/PageRouter.js new file mode 100644 index 00000000..15e56017 --- /dev/null +++ b/packages/core/lib/routers/PageRouter.js @@ -0,0 +1,25 @@ +/*! @license MIT ©2014-2016 Ruben Verborgh, Ghent University - imec */ +/* A PageRouter routes page numbers to offsets */ + +// Creates a new PageRouter with the given page size, which defaults to 100. +class PageRouter { + constructor(config) { + config = config || {}; + this.pageSize = isFinite(config.pageSize) && config.pageSize > 1 ? ~~config.pageSize : 100; + } + + // Extracts a page parameter from the request and adds it to the query + extractQueryParams(request, query) { + let page = request.url && request.url.query && request.url.query.page, + features = query.features || (query.features = {}); + + // Set the limit to the page size + features.limit = true, query.limit = this.pageSize; + + // If a page is given, adjust the offset + if (page && /^\d+$/.test(page) && (page = parseInt(page, 10)) > 1) + features.offset = true, query.offset = this.pageSize * (page - 1); + } +} + +module.exports = PageRouter; diff --git a/packages/core/lib/views/HtmlView.js b/packages/core/lib/views/HtmlView.js new file mode 100644 index 00000000..ab2b6f53 --- /dev/null +++ b/packages/core/lib/views/HtmlView.js @@ -0,0 +1,60 @@ +/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ +/* HtmlView is a base class for views that generate HTML responses. */ + +let View = require('./View'), + qejs = require('qejs'), + q = require('q'), + path = require('path'), + _ = require('lodash'), + RdfString = require('rdf-string'), + UrlData = require('../UrlData'); + +// Creates a new HTML view with the given name and settings +class HtmlView extends View { + constructor(viewName, settings) { + settings = settings || {}; + settings.urlData = settings.urlData || new UrlData(); + let defaults = { + cache: true, RdfString: RdfString, + assetsPath: settings.urlData.assetsPath || '/', baseURL: settings.urlData.baseURL || '/', + title: '', header: settings && settings.title, + }; + super(viewName, 'text/html', { ...settings, ...defaults }); + } + + // Renders the template with the given name to the response + _renderTemplate(templateName, options, request, response, done) { + // Initialize all view extensions + let extensions = options.extensions || (options.extensions = {}), self = this; + for (let extension in extensions) { + if (!extensions[extension]) + extensions[extension] = this._renderViewExtensionContents(extension, options, request, response); + else if (extensions[extension] === 'function') + extensions[extension] = newExtensionViewConstructor(extension, options, request, response); + } + + // Render the template with its options + let fileName = (templateName[0] === '/' ? templateName : path.join(__dirname, templateName)) + '.html'; + qejs.renderFile(fileName, options) + .then((html) => { response.write(html); done(); }) + .fail((error) => { done(error); }); + + function newExtensionViewConstructor(extension, options, request, response) { + return function (data) { + let subOptions = { ...options }; + for (let key in data) + subOptions[key] = data[key]; + return self._renderViewExtensionContents(extension, subOptions, request, response); + }; + } + } + + // Renders the view extensions to a string, returned through a promise + _renderViewExtensionContents(name, options, request, response) { + let buffer = '', writer = { write: function (data) { buffer += data; }, end: _.noop }; + return q.ninvoke(this, '_renderViewExtensions', name, options, request, writer) + .then(() => { return buffer; }); + } +} + +module.exports = HtmlView; diff --git a/packages/core/lib/views/RdfView.js b/packages/core/lib/views/RdfView.js new file mode 100644 index 00000000..57252c47 --- /dev/null +++ b/packages/core/lib/views/RdfView.js @@ -0,0 +1,132 @@ +/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ +/* HtmlView is a base class for views that generate RDF responses. */ + +let View = require('./View'), + N3 = require('n3'), + JsonLdSerializer = require('jsonld-streaming-serializer').JsonLdSerializer, + _ = require('lodash'); + +let dcTerms = 'http://purl.org/dc/terms/', + rdf = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + hydra = 'http://www.w3.org/ns/hydra/core#', + voID = 'http://rdfs.org/ns/void#'; + +let primaryTopic = 'http://xmlns.com/foaf/0.1/primaryTopic'; + +let contentTypes = 'application/trig;q=0.9,application/n-quads;q=0.7,' + + 'application/ld+json;q=0.8,application/json;q=0.8,' + + 'text/turtle;q=0.6,application/n-triples;q=0.5,text/n3;q=0.6'; + +// Creates a new RDF view with the given name and settings +class RdfView extends View { + constructor(viewName, settings) { + super(viewName, contentTypes, settings); + } + + // Renders the view with the given settings to the response + _render(settings, request, response, done) { + // Add generic writer settings + settings.fragmentUrl = settings.fragment && settings.fragment.url || ''; + settings.metadataGraph = settings.fragmentUrl + '#metadata'; + settings.contentType = response.getHeader('Content-Type'); + + // Write the triples with a content-type-specific writer + let self = this, + writer = /json/.test(settings.contentType) ? this._createJsonLdWriter(settings, response, done) + : this._createN3Writer(settings, response, done); + settings.writer = writer; + function main() { self._generateRdf(settings, writer.data, writer.meta, after); } + function after() { self._renderViewExtensions('After', settings, request, response, writer.end); } + function before() { self._renderViewExtensions('Before', settings, request, response, main); } + before(); + } + + // Generates triples and quads by sending them to the data and/or metadata callbacks + _generateRdf(settings, data, metadata, done) { + throw new Error('The _generateRdf method is not yet implemented.'); + } + + // Renders the specified view extension + _renderViewExtension(extension, options, request, response, done) { + // only view extensions that generate triples are supported + if (extension._generateRdf) + extension._generateRdf(options, options.writer.data, options.writer.meta, done); + } + + // Adds details about the datasources + _addDatasources(settings, data, metadata) { + let datasources = settings.datasources; + for (let datasourceName in datasources) { + let datasource = datasources[datasourceName]; + if (datasource.url) { + const quad = this.dataFactory.quad, namedNode = this.dataFactory.namedNode, literal = this.dataFactory.literal; + metadata(quad(namedNode(datasource.url), namedNode(rdf + 'type'), namedNode(voID + 'Dataset'))); + metadata(quad(namedNode(datasource.url), namedNode(rdf + 'type'), namedNode(hydra + 'Collection'))); + metadata(quad(namedNode(datasource.url), namedNode(dcTerms + 'title'), literal('"' + datasource.title + '"', 'en'))); + } + } + } + + // Creates a writer for Turtle/N-Triples/TriG/N-Quads + _createN3Writer(settings, response, done) { + let writer = new N3.Writer({ format: settings.contentType, prefixes: settings.prefixes }), + supportsGraphs = /trig|quad/.test(settings.contentType), metadataGraph; + + const dataFactory = this.dataFactory; + return { + // Adds the data quad to the output + // NOTE: The first parameter can also be a quad object + data: function (quad) { + writer.addQuad(quad); + }, + // Adds the metadata triple to the output + meta: function (quad) { + // Relate the metadata graph to the data. + if (supportsGraphs && !metadataGraph) { + metadataGraph = settings.metadataGraph; + writer.addQuad(dataFactory.namedNode(metadataGraph), dataFactory.namedNode(primaryTopic), dataFactory.namedNode(settings.fragmentUrl), dataFactory.namedNode(metadataGraph)); + } + const graph = quad.graph.termType === 'DefaultGraph' ? (metadataGraph ? dataFactory.namedNode(metadataGraph) : dataFactory.defaultGraph()) : quad.graph; + writer.addQuad(dataFactory.quad(quad.subject, quad.predicate, quad.object, graph)); + }, + // Ends the output and flushes the stream + end: function () { + writer.end((error, output) => { + response.write(error ? '' : output); + done(); + }); + }, + }; + } + + // Creates a writer for JSON-LD + _createJsonLdWriter(settings, response, done) { + let prefixes = settings.prefixes || {}, context = _.omit(prefixes, ''), base = prefixes['']; + base && (context['@base'] = base); + const mySerializer = new JsonLdSerializer({ space: ' ', context: context, baseIRI: prefixes[''], useNativeTypes: true }) + .on('error', done); + mySerializer.pipe(response); + mySerializer.on('error', (e => done(e))); + mySerializer.on('end', (e => done(null))); + + const dataFactory = this.dataFactory; + return { + // Adds the data triple to the output + data: function (quad) { + mySerializer.write(quad); + }, + // Adds the metadata triple to the output + meta: function (quad) { + const graph = quad.graph.termType === 'DefaultGraph' ? (settings.metadataGraph ? dataFactory.namedNode(settings.metadataGraph) : dataFactory.defaultGraph()) : quad.graph; + mySerializer.write(dataFactory.quad(quad.subject, quad.predicate, quad.object, graph)); + }, + // Ends the output and flushes the stream + end: function () { + // We need to wait for the serializer stream to end before calling done() + mySerializer.end(); + }, + }; + } +} + +module.exports = RdfView; diff --git a/packages/core/lib/views/View.js b/packages/core/lib/views/View.js new file mode 100644 index 00000000..97faf3bb --- /dev/null +++ b/packages/core/lib/views/View.js @@ -0,0 +1,100 @@ +/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ +/* View is a base class for objects that generate server responses. */ + +let join = require('path').join, + ViewCollection = require('./ViewCollection'); + +// Creates a view with the given name +class View { + constructor(viewName, contentTypes, defaults) { + this.name = viewName || ''; + this._parseContentTypes(contentTypes); + this._defaults = defaults || {}; + this.dataFactory = this._defaults.dataFactory; + if (this._defaults.views) + this._defaults.views = new ViewCollection(defaults.views); + } + + // Parses a string of content types into an array of objects + // i.e., 'a/b,q=0.7' => [{ type: 'a/b', responseType: 'a/b;charset=utf-8', quality: 0.7 }] + // The "type" represents the MIME type, + // whereas "responseType" contains the value of the Content-Type header with encoding. + _parseContentTypes(contentTypes) { + let matcher = this._supportedContentTypeMatcher = Object.create(null); + if (typeof contentTypes === 'string') { + contentTypes = contentTypes.split(',').map((typeString) => { + let contentType = typeString.match(/[^;,]*/)[0], + responseType = contentType + ';charset=utf-8', + quality = typeString.match(/;q=([0-9.]+)/); + matcher[contentType] = matcher[responseType] = true; + return { + type: contentType, + responseType: responseType, + quality: quality ? Math.min(Math.max(parseFloat(quality[1]), 0.0), 1.0) : 1.0, + }; + }); + } + this.supportedContentTypes = contentTypes || []; + } + + // Indicates whether the view supports the given content type + supportsContentType(contentType) { + return this._supportedContentTypeMatcher[contentType]; + } + + // Renders the view with the given options to the response + render(options, request, response, done) { + // Initialize view-specific settings + let settings = { ...options, ...this._defaults }; + if (!settings.contentType) + settings.contentType = response.getHeader('Content-Type'); + + // Export our base view, so it can be reused by other modules + settings.viewPathBase = join(__dirname, 'base.html'); + + // Render the view and end the response when done + this._render(settings, request, response, (error) => { + if (error) + response.emit('error', error); + response.end(); + done && done(); + }); + } + + // Gets extensions with the given name for this view + _getViewExtensions(name, contentType) { + let extensions = this._defaults.views ? this._defaults.views.getViews(this.name + ':' + name) : []; + if (extensions.length) { + extensions = extensions.filter((extension) => { + return extension.supportsContentType(contentType); + }); + } + return extensions; + } + + // Renders the extensions with the given name for this view + _renderViewExtensions(name, options, request, response, done) { + let self = this, extensions = this._getViewExtensions(name, options.contentType), i = 0; + (function next() { + if (i < extensions.length) + self._renderViewExtension(extensions[i++], options, request, response, next); + else + done(); + })(); + } + + // Renders the specified view extension + _renderViewExtension(extension, options, request, response, done) { + extension.render(options, request, response, done); + } + + // Renders the view with the given settings to the response + // (settings combines the view defaults with instance-specific options) + _render(settings, request, response, done) { + throw new Error('The _render method is not yet implemented.'); + } +} + + + +module.exports = View; diff --git a/packages/core/lib/views/ViewCollection.js b/packages/core/lib/views/ViewCollection.js new file mode 100644 index 00000000..3d8a71db --- /dev/null +++ b/packages/core/lib/views/ViewCollection.js @@ -0,0 +1,60 @@ +/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ +/* + A ViewCollection provides access to content-negotiated views by name. + + It can hold multiple views with the same name. + `matchViews` performs content negotiation to find the most appropriate view for a response. + `getViews` returns all views with a given name. +*/ + +let negotiate = require('negotiate'), + Util = require('../Util'); + +let ViewCollectionError = Util.createErrorType('ViewCollectionError'); + +// Creates a new ViewCollection +class ViewCollection { + constructor(views) { + this._views = {}; // Views keyed by name + this._viewMatchers = {}; // Views matchers keyed by name; each one matches one content type + views && this.addViews(views); + } + + // Adds the given view to the collection + addView(view) { + // Add the view to the list per type + (this._views[view.name] || (this._views[view.name] = [])).push(view); + // Add a match entry for each content type supported by the view + let matchers = this._viewMatchers[view.name] || (this._viewMatchers[view.name] = []); + view.supportedContentTypes.forEach((contentType) => { + matchers.push({ ...contentType, view: view }); + }); + } + + // Adds the given views to the collection + addViews(views) { + for (let i = 0; i < views.length; i++) + this.addView(views[i]); + } + + // Gets all views with the given name + getViews(name) { + return this._views[name] || []; + } + + // Gets the best match for views with the given name that accommodate the request + matchView(name, request) { + // Retrieve the views with the given name + let viewList = this._viewMatchers[name]; + if (!viewList || !viewList.length) + throw new ViewCollectionError('No view named ' + name + ' found.'); + // Negotiate the view best matching the request's requirements + let viewDetails = negotiate.choose(viewList, request)[0]; + if (!viewDetails) + throw new ViewCollectionError('No matching view named ' + name + ' found.'); + return viewDetails; + } +} +ViewCollection.ViewCollectionError = ViewCollectionError; + +module.exports = ViewCollection; diff --git a/lib/views/base.html b/packages/core/lib/views/base.html similarity index 100% rename from lib/views/base.html rename to packages/core/lib/views/base.html diff --git a/packages/core/lib/views/error/ErrorHtmlView.js b/packages/core/lib/views/error/ErrorHtmlView.js new file mode 100644 index 00000000..265c0010 --- /dev/null +++ b/packages/core/lib/views/error/ErrorHtmlView.js @@ -0,0 +1,18 @@ +/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ +/* An ErrorRdfView represents a 500 response in HTML. */ + +let HtmlView = require('../HtmlView'); + +// Creates a new ErrorHtmlView +class ErrorHtmlView extends HtmlView { + constructor(settings) { + super('Error', settings); + } + + // Renders the view with the given settings to the response + _render(settings, request, response, done) { + this._renderTemplate('error/error', settings, request, response, done); + } +} + +module.exports = ErrorHtmlView; diff --git a/packages/core/lib/views/error/ErrorRdfView.js b/packages/core/lib/views/error/ErrorRdfView.js new file mode 100644 index 00000000..fe983740 --- /dev/null +++ b/packages/core/lib/views/error/ErrorRdfView.js @@ -0,0 +1,20 @@ +/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ +/* An ErrorRdfView represents a 500 response in RDF. */ + +let RdfView = require('../RdfView'); + +// Creates a new ErrorRdfView +class ErrorRdfView extends RdfView { + constructor(settings) { + super('Error', settings); + } + + // Generates triples and quads by sending them to the data and/or metadata callbacks + _generateRdf(settings, data, metadata, done) { + this._addDatasources(settings, data, metadata); + done(); + } +} + + +module.exports = ErrorRdfView; diff --git a/lib/views/error/error.html b/packages/core/lib/views/error/error.html similarity index 100% rename from lib/views/error/error.html rename to packages/core/lib/views/error/error.html diff --git a/packages/core/lib/views/forbidden/ForbiddenHtmlView.js b/packages/core/lib/views/forbidden/ForbiddenHtmlView.js new file mode 100644 index 00000000..4ebff277 --- /dev/null +++ b/packages/core/lib/views/forbidden/ForbiddenHtmlView.js @@ -0,0 +1,18 @@ +/*! @license MIT ©2015-2016 Miel Vander Sande, Ghent University - imec */ +/* A ForbiddenHtmlView represents a 401 response in HTML. */ + +let HtmlView = require('../HtmlView'); + +// Creates a new ForbiddenHtmlView +class ForbiddenHtmlView extends HtmlView { + constructor(settings) { + super('Forbidden', settings); + } + + // Renders the view with the given settings to the response + _render(settings, request, response, done) { + this._renderTemplate('forbidden/forbidden', settings, request, response, done); + } +} + +module.exports = ForbiddenHtmlView; diff --git a/lib/views/forbidden/forbidden.html b/packages/core/lib/views/forbidden/forbidden.html similarity index 78% rename from lib/views/forbidden/forbidden.html rename to packages/core/lib/views/forbidden/forbidden.html index a2f15ecd..7fe34f88 100644 --- a/lib/views/forbidden/forbidden.html +++ b/packages/core/lib/views/forbidden/forbidden.html @@ -7,8 +7,8 @@

Not authorized to access resource

Available datasets

    -<% for (var datasourceName in datasources) { - var datasource = datasources[datasourceName]; %> +<% for (const datasourceName in datasources) { + const datasource = datasources[datasourceName]; %>
  • <%= datasource.title %>
  • <% } %>
diff --git a/packages/core/lib/views/notfound/NotFoundHtmlView.js b/packages/core/lib/views/notfound/NotFoundHtmlView.js new file mode 100644 index 00000000..48e9c003 --- /dev/null +++ b/packages/core/lib/views/notfound/NotFoundHtmlView.js @@ -0,0 +1,18 @@ +/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ +/* A NotFoundRdfView represents a 404 response in HTML. */ + +let HtmlView = require('../HtmlView'); + +// Creates a new NotFoundHtmlView +class NotFoundHtmlView extends HtmlView { + constructor(settings) { + super('NotFound', settings); + } + + // Renders the view with the given settings to the response + _render(settings, request, response, done) { + this._renderTemplate('notfound/notfound', settings, request, response, done); + } +} + +module.exports = NotFoundHtmlView; diff --git a/packages/core/lib/views/notfound/NotFoundRdfView.js b/packages/core/lib/views/notfound/NotFoundRdfView.js new file mode 100644 index 00000000..3a6877bb --- /dev/null +++ b/packages/core/lib/views/notfound/NotFoundRdfView.js @@ -0,0 +1,20 @@ +/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ +/* A NotFoundRdfView represents a 404 response in RDF. */ + +let RdfView = require('../RdfView'); + +// Creates a new NotFoundRdfView +class NotFoundRdfView extends RdfView { + constructor(settings) { + super('NotFound', settings); + } + + // Generates triples and quads by sending them to the data and/or metadata callbacks + _generateRdf(settings, data, metadata, done) { + this._addDatasources(settings, data, metadata); + done(); + } +} + + +module.exports = NotFoundRdfView; diff --git a/lib/views/notfound/notfound.html b/packages/core/lib/views/notfound/notfound.html similarity index 78% rename from lib/views/notfound/notfound.html rename to packages/core/lib/views/notfound/notfound.html index 81ee110d..f579505b 100644 --- a/lib/views/notfound/notfound.html +++ b/packages/core/lib/views/notfound/notfound.html @@ -7,8 +7,8 @@

Resource not found

Available datasets