diff --git a/package-lock.json b/package-lock.json index 44313fee70d8443fbad41850d1e5c817c64dc95e..47c4cfdd0fd1a3a5c2b88cc9384d96bc42dc219e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3,6 +3,16 @@ "requires": true, "lockfileVersion": 1, "dependencies": { + "@apidevtools/json-schema-ref-parser": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-8.0.0.tgz", + "integrity": "sha512-n4YBtwQhdpLto1BaUCyAeflizmIbaloGShsPyRtFf5qdFJxfssj+GgLavczgKJFa3Bq+3St2CKcpRJdjtB4EBw==", + "requires": { + "@jsdevtools/ono": "^7.1.0", + "call-me-maybe": "^1.0.1", + "js-yaml": "^3.13.1" + } + }, "@babel/cli": { "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.8.4.tgz", @@ -2104,6 +2114,11 @@ } } }, + "@jsdevtools/ono": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.2.tgz", + "integrity": "sha512-qS/a24RA5FEoiJS9wiv6Pwg2c/kiUo3IVUQcfeM9JvsR6pM8Yx+yl/6xWYLckZCT5jpLNhslgjiA8p/XcGyMRQ==" + }, "@loaders.gl/3d-tiles": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@loaders.gl/3d-tiles/-/3d-tiles-2.0.4.tgz", @@ -2980,6 +2995,11 @@ "svg.select.js": "^3.0.1" } }, + "append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY=" + }, "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", @@ -2998,7 +3018,6 @@ "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" } @@ -3483,7 +3502,6 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dev": true, "optional": true, "requires": { "file-uri-to-path": "1.0.0" @@ -3756,6 +3774,38 @@ "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", "dev": true }, + "busboy": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", + "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=", + "requires": { + "dicer": "0.2.5", + "readable-stream": "1.1.x" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, "bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", @@ -3824,6 +3874,11 @@ "unset-value": "^1.0.0" } }, + "call-me-maybe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", + "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=" + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -4876,6 +4931,16 @@ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.10.0.tgz", "integrity": "sha512-EhfEKevYGWhWlZbNeplfhIU/+N+x0iCIx7VzKlXma2EdQyznVlZhCptXUY+BegNpPW2kjdx15Rvq503YcXXrcA==" }, + "deasync": { + "version": "0.1.20", + "resolved": "https://registry.npmjs.org/deasync/-/deasync-0.1.20.tgz", + "integrity": "sha512-E1GI7jMI57hL30OX6Ht/hfQU8DO4AuB9m72WFm4c38GNbUD4Q03//XZaOIHZiY+H1xUaomcot5yk2q/qIZQkGQ==", + "optional": true, + "requires": { + "bindings": "^1.5.0", + "node-addon-api": "^1.7.1" + } + }, "debounce": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.0.tgz", @@ -5123,6 +5188,38 @@ "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==", "dev": true }, + "dicer": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", + "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=", + "requires": { + "readable-stream": "1.1.x", + "streamsearch": "0.1.2" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, "diff-sequences": { "version": "25.2.1", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.2.1.tgz", @@ -6404,6 +6501,53 @@ } } }, + "express-openapi-validator": { + "version": "3.12.7", + "resolved": "https://registry.npmjs.org/express-openapi-validator/-/express-openapi-validator-3.12.7.tgz", + "integrity": "sha512-6r0vnuzv2sUOZZSj3fMSgWFN8CfiWZV6KXrBfm0AGsLKsyW41zUHTwASlnwDiF5z/TeLpH5w4rgisneG1G1fBw==", + "requires": { + "ajv": "^6.12.2", + "content-type": "^1.0.4", + "deasync": "^0.1.19", + "js-yaml": "^3.13.1", + "json-schema-ref-parser": "^8.0.0", + "lodash.merge": "^4.6.2", + "lodash.uniq": "^4.5.0", + "lodash.zipobject": "^4.1.3", + "media-typer": "^1.1.0", + "multer": "^1.4.2", + "ono": "^7.1.2", + "path-to-regexp": "^6.1.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz", + "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "fast-deep-equal": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==" + }, + "media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==" + }, + "path-to-regexp": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.1.0.tgz", + "integrity": "sha512-h9DqehX3zZZDCEm+xbfU0ZmwCGFCAAraPJWMXJ4+v32NjZJilVg3k1TcKsRgIb8IQ/izZSaydDc1OhJCZvs2Dw==" + } + } + }, "expression-eval": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/expression-eval/-/expression-eval-2.1.0.tgz", @@ -6639,7 +6783,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "dev": true, "optional": true }, "filefy": { @@ -11427,7 +11570,6 @@ "version": "3.13.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "dev": true, "requires": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -11436,8 +11578,7 @@ "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" } } }, @@ -11521,6 +11662,14 @@ "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" }, + "json-schema-ref-parser": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/json-schema-ref-parser/-/json-schema-ref-parser-8.0.0.tgz", + "integrity": "sha512-2P4icmNkZLrBr6oa5gSZaDSol/oaBHYkoP/8dsw63E54NnHGRhhiFuy9yFoxPuSm+uHKmeGxAAWMDF16SCHhcQ==", + "requires": { + "@apidevtools/json-schema-ref-parser": "8.0.0" + } + }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -11825,6 +11974,16 @@ "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", "integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=" }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" + }, + "lodash.zipobject": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/lodash.zipobject/-/lodash.zipobject-4.1.3.tgz", + "integrity": "sha1-s5n1q6j/YqdG9peb8gshT5ZNvvg=" + }, "loglevel": { "version": "1.6.7", "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.7.tgz", @@ -12267,6 +12426,21 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "multer": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.2.tgz", + "integrity": "sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg==", + "requires": { + "append-field": "^1.0.0", + "busboy": "^0.2.11", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.1", + "object-assign": "^4.1.1", + "on-finished": "^2.3.0", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + } + }, "multicast-dns": { "version": "6.2.3", "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", @@ -12367,6 +12541,12 @@ "lower-case": "^1.1.1" } }, + "node-addon-api": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.1.tgz", + "integrity": "sha512-2+DuKodWvwRTrCfKOeR24KIc5unKjOh8mz17NCzVnHWfjAdDqbfbjqh7gUT+BkXBRQM52+xCHciKWonJ3CbJMQ==", + "optional": true + }, "node-environment-flags": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz", @@ -12769,6 +12949,14 @@ "mimic-fn": "^2.1.0" } }, + "ono": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/ono/-/ono-7.1.2.tgz", + "integrity": "sha512-es7Gfr+OGNFwiYpyHCLgBF+p/RA0qYbWysQKlZbLvvUBis5BygEs8TVJ4r+SgHDfagOgONhaAl6Y4JLy++0MTw==", + "requires": { + "@jsdevtools/ono": "7.1.2" + } + }, "opn": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", @@ -15243,8 +15431,7 @@ "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, "sshpk": { "version": "1.16.1", @@ -15393,6 +15580,11 @@ "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", "dev": true }, + "streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" + }, "string-length": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-length/-/string-length-3.1.0.tgz", @@ -15637,6 +15829,19 @@ "svg.js": "^2.6.5" } }, + "swagger-ui-dist": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.25.1.tgz", + "integrity": "sha512-Sw/K95j1pT9TZtLKiHDEml7YqcXC9thTTQjxrvNgd9j1KzOIxpo/5lhHuUMAN/hxVAHetzmcBcQaBjywRXog8w==" + }, + "swagger-ui-express": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.1.4.tgz", + "integrity": "sha512-Ea96ecpC+Iq9GUqkeD/LFR32xSs8gYqmTW1gXCuKg81c26WV6ZC2FsBSPVExQP6WkyUuz5HEiR0sEv/HCC343g==", + "requires": { + "swagger-ui-dist": "^3.18.1" + } + }, "symbol-observable": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", @@ -17274,8 +17479,7 @@ "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" }, "y18n": { "version": "3.2.1", diff --git a/package.json b/package.json index 29843e37cc4bcfd76b162999c7679e62f286e2b1..cbb8981645b2ad6f583910e7b3b008523fcec0a6 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,9 @@ "deck.gl": "^8.0.17", "dotenv": "^8.2.0", "express": "^4.16.4", + "express-openapi-validator": "^3.12.7", "immutable": "^4.0.0-rc.12", + "js-yaml": "^3.13.1", "leaflet": "^1.6.0", "leaflet-draw": "^1.0.4", "leaflet-fullscreen": "^1.0.2", @@ -93,6 +95,7 @@ "rxjs": "^6.3.3", "rxjs-compat": "^6.3.3", "sass-loader": "^8.0.0", + "swagger-ui-express": "^4.1.4", "victory": "^0.26.1" }, "standard": { diff --git a/src/client/epics/index.js b/src/client/epics/index.js index 5c5943ae020ad6dd40e9f12da3e508227b6b53f9..8550e1fa84245c279d48d94c9af177a8fff3cbd3 100644 --- a/src/client/epics/index.js +++ b/src/client/epics/index.js @@ -22,7 +22,7 @@ import { FETCH_PAGINATED_RESULTS, FETCH_PAGINATED_RESULTS_FAILED, FETCH_RESULTS, - FETCH_RESULTS_CLIENT_SIDE, + FETCH_FULL_TEXT_RESULTS, FETCH_RESULTS_FAILED, FETCH_BY_URI, FETCH_BY_URI_FAILED, @@ -61,8 +61,8 @@ const port = window.location.hostname === 'localhost' || window.location.hostnam : '' export const apiUrl = (process.env.NODE_ENV === 'development') - ? `http://localhost:3001${rootUrl}/api/` - : `${window.location.protocol}//${window.location.hostname}${port}${rootUrl}/api/` + ? `http://localhost:3001${rootUrl}/api/v1` + : `${window.location.protocol}//${window.location.hostname}${port}${rootUrl}/api/v1` export const availableLocales = { en: localeEN, @@ -84,16 +84,24 @@ const fetchPaginatedResultsEpic = (action$, state$) => action$.pipe( sortBy: sortBy, sortDirection: sortDirection }) - const requestUrl = `${apiUrl}${resultClass}/paginated?${params}` + const requestUrl = `${apiUrl}/faceted-search/${resultClass}/paginated` // https://rxjs-dev.firebaseapp.com/api/ajax/ajax - return ajax.getJSON(requestUrl).pipe( - map(response => updatePaginatedResults({ - resultClass: response.resultClass, - page: response.page, - pagesize: response.pagesize, - data: response.data, - sparqlQuery: response.sparqlQuery - })), + return ajax({ + url: requestUrl, + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: params + }).pipe( + map(ajaxResponse => + updatePaginatedResults({ + resultClass: ajaxResponse.response.resultClass, + page: ajaxResponse.response.page, + pagesize: ajaxResponse.response.pagesize, + data: ajaxResponse.response.data, + sparqlQuery: ajaxResponse.response.sparqlQuery + })), // https://redux-observable.js.org/docs/recipes/ErrorHandling.html catchError(error => of({ type: FETCH_PAGINATED_RESULTS_FAILED, @@ -112,18 +120,25 @@ const fetchResultsEpic = (action$, state$) => action$.pipe( ofType(FETCH_RESULTS), withLatestFrom(state$), mergeMap(([action, state]) => { - const { resultClass, facetClass, groupBy } = action + const { resultClass, facetClass } = action const params = stateToUrl({ facets: state[`${facetClass}Facets`].facets, - facetClass, - groupBy + facetClass }) - const requestUrl = `${apiUrl}${resultClass}/all?${params}` - return ajax.getJSON(requestUrl).pipe( - map(response => updateResults({ + const requestUrl = `${apiUrl}/faceted-search/${resultClass}/all` + // https://rxjs-dev.firebaseapp.com/api/ajax/ajax + return ajax({ + url: requestUrl, + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: params + }).pipe( + map(ajaxResponse => updateResults({ resultClass: resultClass, - data: response.data, - sparqlQuery: response.sparqlQuery + data: ajaxResponse.response.data, + sparqlQuery: ajaxResponse.response.sparqlQuery })), catchError(error => of({ type: FETCH_RESULTS_FAILED, @@ -138,52 +153,27 @@ const fetchResultsEpic = (action$, state$) => action$.pipe( }) ) -const clientFSFetchResultsEpic = (action$, state$) => action$.pipe( - ofType(CLIENT_FS_FETCH_RESULTS), - withLatestFrom(state$), - debounceTime(500), - switchMap(([action, state]) => { - const { jenaIndex } = action - const selectedDatasets = pickSelectedDatasets(state.clientSideFacetedSearch.datasets) - const dsParams = selectedDatasets.map(ds => `dataset=${ds}`).join('&') - let requestUrl - if (action.jenaIndex === 'text') { - requestUrl = `${apiUrl}federatedSearch?q=${action.query}&${dsParams}` - } else if (action.jenaIndex === 'spatial') { - const { latMin, longMin, latMax, longMax } = state.leafletMap - requestUrl = `${apiUrl}federatedSearch?latMin=${latMin}&longMin=${longMin}&latMax=${latMax}&longMax=${longMax}&${dsParams}` - } - return ajax.getJSON(requestUrl).pipe( - map(response => clientFSUpdateResults({ - results: response, - jenaIndex - })), - catchError(error => of({ - type: CLIENT_FS_FETCH_RESULTS_FAILED, - error: error, - message: { - text: backendErrorText, - title: 'Error' - } - })) - ) - }) -) const fetchResultCountEpic = (action$, state$) => action$.pipe( ofType(FETCH_RESULT_COUNT), withLatestFrom(state$), mergeMap(([action, state]) => { const { resultClass, facetClass } = action const params = stateToUrl({ - facets: state[`${facetClass}Facets`].facets, - facetClass: facetClass + facets: state[`${facetClass}Facets`].facets }) - const requestUrl = `${apiUrl}${resultClass}/count?${params}` - return ajax.getJSON(requestUrl).pipe( - map(response => updateResultCount({ - resultClass: response.resultClass, - data: response.data, - sparqlQuery: response.sparqlQuery + const requestUrl = `${apiUrl}/faceted-search/${resultClass}/count` + return ajax({ + url: requestUrl, + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: params + }).pipe( + map(ajaxResponse => updateResultCount({ + resultClass: ajaxResponse.response.resultClass, + data: ajaxResponse.response.data, + sparqlQuery: ajaxResponse.response.sparqlQuery })), catchError(error => of({ type: FETCH_RESULT_COUNT_FAILED, @@ -198,22 +188,15 @@ const fetchResultCountEpic = (action$, state$) => action$.pipe( }) ) -const fetchResultsClientSideEpic = (action$, state$) => action$.pipe( - ofType(FETCH_RESULTS_CLIENT_SIDE), +const fullTextSearchEpic = (action$, state$) => action$.pipe( + ofType(FETCH_FULL_TEXT_RESULTS), withLatestFrom(state$), debounceTime(500), switchMap(([action, state]) => { - const searchUrl = apiUrl + 'search' - let requestUrl = '' - if (action.jenaIndex === 'text') { - requestUrl = `${searchUrl}?q=${action.query}` - } else if (action.jenaIndex === 'spatial') { - const { latMin, longMin, latMax, longMax } = state.map - requestUrl = `${searchUrl}?latMin=${latMin}&longMin=${longMin}&latMax=${latMax}&longMax=${longMax}` - } + const requestUrl = `${apiUrl}/full-text-search?q=${action.query}` return ajax.getJSON(requestUrl).pipe( map(response => updateResults({ - resultClass: 'all', + resultClass: 'fullText', data: response.data, sparqlQuery: response.sparqlQuery, query: action.query, @@ -221,7 +204,7 @@ const fetchResultsClientSideEpic = (action$, state$) => action$.pipe( })), catchError(error => of({ type: FETCH_RESULTS_FAILED, - resultClass: 'all', + resultClass: 'fullText', error: error, message: { text: backendErrorText, @@ -239,14 +222,21 @@ const fetchByURIEpic = (action$, state$) => action$.pipe( const { resultClass, facetClass, uri } = action const params = stateToUrl({ facets: facetClass == null ? null : state[`${facetClass}Facets`].facets, - facetClass: facetClass + facetClass }) - const requestUrl = `${apiUrl}${resultClass}/instance/${encodeURIComponent(uri)}?${params}` - return ajax.getJSON(requestUrl).pipe( - map(response => updateInstance({ + const requestUrl = `${apiUrl}/${resultClass}/page/${encodeURIComponent(uri)}` + return ajax({ + url: requestUrl, + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: params + }).pipe( + map(ajaxResponse => updateInstance({ resultClass: resultClass, - data: response.data, - sparqlQuery: response.sparqlQuery + data: ajaxResponse.response.data, + sparqlQuery: ajaxResponse.response.sparqlQuery })), catchError(error => of({ type: FETCH_BY_URI_FAILED, @@ -274,14 +264,21 @@ const fetchFacetEpic = (action$, state$) => action$.pipe( sortBy: sortBy, sortDirection: sortDirection }) - const requestUrl = `${apiUrl}${action.facetClass}/facet/${facetID}?${params}` - return ajax.getJSON(requestUrl).pipe( - map(res => updateFacetValues({ + const requestUrl = `${apiUrl}/faceted-search/${action.facetClass}/facet/${facetID}` + return ajax({ + url: requestUrl, + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: params + }).pipe( + map(ajaxResponse => updateFacetValues({ facetClass: facetClass, - id: facetID, - data: res.data || [], - flatData: res.flatData || [], - sparqlQuery: res.sparqlQuery + id: ajaxResponse.response.id, + data: ajaxResponse.response.data || [], + flatData: ajaxResponse.response.flatData || [], + sparqlQuery: ajaxResponse.response.sparqlQuery })), catchError(error => of({ type: FETCH_FACET_FAILED, @@ -311,14 +308,21 @@ const fetchFacetConstrainSelfEpic = (action$, state$) => action$.pipe( sortDirection: sortDirection, constrainSelf: true }) - const requestUrl = `${apiUrl}${action.facetClass}/facet/${facetID}?${params}` - return ajax.getJSON(requestUrl).pipe( - map(res => updateFacetValuesConstrainSelf({ + const requestUrl = `${apiUrl}/faceted-search/${action.facetClass}/facet/${facetID}` + return ajax({ + url: requestUrl, + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: params + }).pipe( + map(ajaxResponse => updateFacetValuesConstrainSelf({ facetClass: facetClass, id: facetID, - data: res.data || [], - flatData: res.flatData || [], - sparqlQuery: res.sparqlQuery + data: ajaxResponse.response.data || [], + flatData: ajaxResponse.response.flatData || [], + sparqlQuery: ajaxResponse.response.sparqlQuery })), catchError(error => of({ type: FETCH_FACET_FAILED, @@ -334,6 +338,38 @@ const fetchFacetConstrainSelfEpic = (action$, state$) => action$.pipe( }) ) +const clientFSFetchResultsEpic = (action$, state$) => action$.pipe( + ofType(CLIENT_FS_FETCH_RESULTS), + withLatestFrom(state$), + debounceTime(500), + switchMap(([action, state]) => { + const { jenaIndex } = action + const selectedDatasets = pickSelectedDatasets(state.clientSideFacetedSearch.datasets) + const dsParams = selectedDatasets.map(ds => `dataset=${ds}`).join('&') + let requestUrl + if (action.jenaIndex === 'text') { + requestUrl = `${apiUrl}/federated-search?q=${action.query}&${dsParams}` + } else if (action.jenaIndex === 'spatial') { + const { latMin, longMin, latMax, longMax } = state.leafletMap + requestUrl = `${apiUrl}/federated-search?latMin=${latMin}&longMin=${longMin}&latMax=${latMax}&longMax=${longMax}&${dsParams}` + } + return ajax.getJSON(requestUrl).pipe( + map(response => clientFSUpdateResults({ + results: response, + jenaIndex + })), + catchError(error => of({ + type: CLIENT_FS_FETCH_RESULTS_FAILED, + error: error, + message: { + text: backendErrorText, + title: 'Error' + } + })) + ) + }) +) + const loadLocalesEpic = action$ => action$.pipe( ofType(LOAD_LOCALES), // https://thecodebarbarian.com/a-beginners-guide-to-redux-observable @@ -441,12 +477,12 @@ const fetchGeoJSONLayer = async (layerID, bounds) => { const rootEpic = combineEpics( fetchPaginatedResultsEpic, fetchResultsEpic, - clientFSFetchResultsEpic, fetchResultCountEpic, - fetchResultsClientSideEpic, fetchByURIEpic, fetchFacetEpic, fetchFacetConstrainSelfEpic, + fullTextSearchEpic, + clientFSFetchResultsEpic, loadLocalesEpic, fetchSimilarDocumentsEpic, fetchGeoJSONLayersEpic, diff --git a/src/client/helpers/helpers.js b/src/client/helpers/helpers.js index 2c5c0111c622bf7b46e8cbcb08c76f69f089bcf9..9671380876a38b13d173344346f712d73c5e53c5 100644 --- a/src/client/helpers/helpers.js +++ b/src/client/helpers/helpers.js @@ -22,45 +22,50 @@ export const stateToUrl = ({ if (constrainSelf !== null) { params.constrainSelf = constrainSelf } if (groupBy !== null) { params.groupBy = groupBy } if (facets !== null) { - const constraints = {} + const constraints = [] for (const [key, value] of Object.entries(facets)) { if (has(value, 'uriFilter') && value.uriFilter !== null) { - constraints[key] = { + constraints.push({ + facetID: key, filterType: value.filterType, priority: value.priority, values: Object.keys(value.uriFilter) - } + }) } else if (has(value, 'spatialFilter') && value.spatialFilter !== null) { - constraints[key] = { + constraints.push({ + facetID: key, filterType: value.filterType, priority: value.priority, values: boundsToValues(value.spatialFilter._bounds) - } + }) } else if (has(value, 'textFilter') && value.textFilter !== null) { - constraints[key] = { + constraints.push({ + facetID: key, filterType: value.filterType, priority: value.priority, values: value.textFilter - } + }) } else if (has(value, 'timespanFilter') && value.timespanFilter !== null) { - constraints[key] = { + constraints.push({ + facetID: key, filterType: value.filterType, priority: value.priority, values: value.timespanFilter - } + }) } else if (has(value, 'integerFilter') && value.integerFilter !== null) { - constraints[key] = { + constraints.push({ + facetID: key, filterType: value.filterType, priority: value.priority, values: value.integerFilter - } + }) } } - if (Object.keys(constraints).length > 0) { - params.constraints = JSON.stringify(constraints) + if (constraints.length > 0) { + params.constraints = constraints } } - return querystring.stringify(params) + return params } export const urlToState = ({ initialState, queryString }) => { diff --git a/src/server/index.js b/src/server/index.js index 5287e9328157162a44f6d63879b49e7473ba5ac6..9a98a5c3e3d3b207280c0cc52b21212852f2e945 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -1,3 +1,4 @@ +import fs from 'fs' import express from 'express' import path from 'path' import bodyParser from 'body-parser' @@ -12,6 +13,9 @@ import { getFacet } from './sparql/FacetValues' import { queryJenaIndex } from './sparql/JenaQuery' import { getFederatedResults } from './sparql/FederatedSearch' import { fetchGeoJSONLayer } from './wfs/WFSApi' +import swaggerUi from 'swagger-ui-express' +import { OpenApiValidator } from 'express-openapi-validator' +import yaml from 'js-yaml' const DEFAULT_PORT = 3001 const app = express() app.set('port', process.env.PORT || DEFAULT_PORT) @@ -20,10 +24,15 @@ app.use(bodyParser.json()) // NODE_ENV is defined in package.json when running in localhost const isDevelopment = process.env.NODE_ENV === 'development' -// allow CORS +// CORS middleware app.use(function (req, res, next) { res.header('Access-Control-Allow-Origin', '*') + res.header('Access-Control-Allow-Methods', 'GET,POST,OPTIONS') res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept') + // handle pre-flight request + if (req.method === 'OPTIONS') { + return res.status(200).end() + } next() }) @@ -37,186 +46,190 @@ if (!isDevelopment) { } // React app makes requests to these api urls -const apiPath = '/api' - -// https://medium.com/@Abazhenov/using-async-await-in-express-with-node-8-b8af872c0016 -app.get(`${apiPath}/:resultClass/paginated`, async (req, res, next) => { - try { - const data = await getPaginatedResults({ - resultClass: req.params.resultClass, - page: req.query.page == null ? null : req.query.page, - pagesize: parseInt(req.query.pagesize) || null, - sortBy: req.query.sortBy || null, - sortDirection: req.query.sortDirection || null, - constraints: req.query.constraints == null ? null : JSON.parse(req.query.constraints), - resultFormat: req.query.resultFormat == null ? 'json' : req.query.resultFormat - }) - res.json(data) - } catch (error) { - next(error) - } +const apiPath = '/api/v1' + +// Generate API docs from YAML file with Swagger UI +let swaggerDocument +try { + swaggerDocument = yaml.safeLoad(fs.readFileSync(path.join(__dirname, './openapi.yaml'), 'utf8')) +} catch (e) { + console.log(e) +} + +app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument)) + +new OpenApiValidator({ + apiSpec: swaggerDocument, + validateResponses: true }) + .install(app) + .then(() => { + // https://medium.com/@Abazhenov/using-async-await-in-express-with-node-8-b8af872c0016 + app.post(`${apiPath}/faceted-search/:resultClass/paginated`, async (req, res, next) => { + const { params, body } = req + try { + const data = await getPaginatedResults({ + resultClass: params.resultClass, + page: body.page, + pagesize: parseInt(body.pagesize), + sortBy: body.sortBy, + sortDirection: body.sortDirection, + constraints: body.constraints, + resultFormat: 'json' + }) + res.json(data) + } catch (error) { + console.log(error) + next(error) + } + }) -app.get(`${apiPath}/:resultClass/all`, async (req, res, next) => { - try { - const resultFormat = req.query.resultFormat == null ? 'json' : req.query.resultFormat - const data = await getAllResults({ - resultClass: req.params.resultClass, - facetClass: req.query.facetClass || null, - constraints: req.query.constraints == null ? null : JSON.parse(req.query.constraints), - resultFormat: resultFormat, - groupBy: req.query.groupBy === 'true' + app.post(`${apiPath}/faceted-search/:resultClass/all`, async (req, res, next) => { + const { params, body } = req + const resultFormat = 'json' + try { + const data = await getAllResults({ + resultClass: params.resultClass, + facetClass: body.facetClass, + constraints: body.constraints, + resultFormat: resultFormat + }) + if (resultFormat === 'csv') { + res.writeHead(200, { + 'Content-Type': 'text/csv', + 'Content-Disposition': 'attachment; filename=results.csv' + }) + res.end(data) + } else { + res.json(data) + } + } catch (error) { + next(error) + } }) - if (resultFormat === 'csv') { - res.writeHead(200, { - 'Content-Type': 'text/csv', - 'Content-Disposition': 'attachment; filename=results.csv' - }) - res.end(data) - } else { - res.json(data) - } - } catch (error) { - next(error) - } -}) -app.get(`${apiPath}/:resultClass/count`, async (req, res, next) => { - try { - const data = await getResultCount({ - resultClass: req.params.resultClass, - constraints: req.query.constraints == null ? null : JSON.parse(req.query.constraints), - resultFormat: req.query.resultFormat == null ? 'json' : req.query.resultFormat + app.post(`${apiPath}/faceted-search/:resultClass/count`, async (req, res, next) => { + const { params, body } = req + try { + const data = await getResultCount({ + resultClass: params.resultClass, + constraints: body.constraints, + resultFormat: 'json' + }) + res.json(data) + } catch (error) { + next(error) + } }) - res.json(data) - } catch (error) { - next(error) - } -}) -app.get(`${apiPath}/:resultClass/instance/:uri`, async (req, res, next) => { - try { - const data = await getByURI({ - resultClass: req.params.resultClass, - facetClass: req.query.facetClass || null, - constraints: req.query.constraints == null ? null : JSON.parse(req.query.constraints), - variant: req.query.variant || null, - uri: req.params.uri, - resultFormat: req.query.resultFormat == null ? 'json' : req.query.resultFormat + app.post(`${apiPath}/:resultClass/page/:uri`, async (req, res, next) => { + const { params, body } = req + try { + const data = await getByURI({ + resultClass: params.resultClass, + uri: params.uri, + facetClass: body.facetClass, + constraints: body.constraints, + resultFormat: 'json' + }) + console.log(data) + res.json(data) + } catch (error) { + next(error) + } }) - res.json(data) - } catch (error) { - next(error) - } -}) -app.get(`${apiPath}/:facetClass/facet/:id`, async (req, res, next) => { - try { - const data = await getFacet({ - facetClass: req.params.facetClass, - facetID: req.params.id, - sortBy: req.query.sortBy || null, - sortDirection: req.query.sortDirection || null, - constraints: req.query.constraints == null ? null : JSON.parse(req.query.constraints), - resultFormat: req.query.resultFormat == null ? 'json' : req.query.resultFormat, - constrainSelf: req.query.constrainSelf || false + app.post(`${apiPath}/faceted-search/:facetClass/facet/:id`, async (req, res, next) => { + const { params, body } = req + try { + const data = await getFacet({ + facetClass: params.facetClass, + facetID: params.id, + sortBy: body.sortBy, + sortDirection: body.sortDirection, + constraints: body.constraints, + resultFormat: 'json', + constrainSelf: body.constrainSelf + }) + res.json(data) + } catch (error) { + next(error) + } }) - res.json(data) - } catch (error) { - next(error) - } -}) -app.get(`${apiPath}/search`, async (req, res, next) => { - let queryTerm = '' - let latMin = 0 - let longMin = 0 - let latMax = 0 - let longMax = 0 - if (has(req.query, 'q')) { - queryTerm = req.query.q - } - if (has(req.query, 'latMin')) { - latMin = req.query.latMin - longMin = req.query.longMin - latMax = req.query.latMax - longMax = req.query.longMax - } - try { - const data = await queryJenaIndex({ - queryTerm: queryTerm, - latMin: latMin, - longMin: longMin, - latMax: latMax, - longMax: longMax, - resultFormat: req.query.resultFormat == null ? 'json' : req.query.resultFormat + app.get(`${apiPath}/full-text-search`, async (req, res, next) => { + try { + const data = await queryJenaIndex({ + queryTerm: req.query.q, + resultFormat: 'json' + }) + res.json(data) + } catch (error) { + next(error) + } }) - res.json(data) - } catch (error) { - next(error) - } -}) -app.get(`${apiPath}/federatedSearch`, async (req, res, next) => { - let queryTerm = '' - let latMin = 0 - let longMin = 0 - let latMax = 0 - let longMax = 0 - if (has(req.query, 'q')) { - queryTerm = req.query.q - } - if (has(req.query, 'latMin')) { - latMin = req.query.latMin - longMin = req.query.longMin - latMax = req.query.latMax - longMax = req.query.longMax - } - try { - const data = await getFederatedResults({ - queryTerm, - latMin, - longMin, - latMax, - longMax, - datasets: castArray(req.query.dataset), - resultFormat: req.query.resultFormat == null ? 'json' : req.query.resultFormat + app.get(`${apiPath}/federated-search`, async (req, res, next) => { + let queryTerm = '' + let latMin = 0 + let longMin = 0 + let latMax = 0 + let longMax = 0 + if (has(req.query, 'q')) { + queryTerm = req.query.q + } + if (has(req.query, 'latMin')) { + latMin = req.query.latMin + longMin = req.query.longMin + latMax = req.query.latMax + longMax = req.query.longMax + } + try { + const data = await getFederatedResults({ + queryTerm, + latMin, + longMin, + latMax, + longMax, + datasets: castArray(req.query.dataset), + resultFormat: req.query.resultFormat == null ? 'json' : req.query.resultFormat + }) + res.json(data) + } catch (error) { + next(error) + } }) - res.json(data) - } catch (error) { - next(error) - } -}) -app.get(`${apiPath}/wfs`, async (req, res, next) => { - const layerIDs = castArray(req.query.layerID) - try { - const data = await Promise.all(layerIDs.map(layerID => fetchGeoJSONLayer({ layerID }))) - res.json(data) - } catch (error) { - next(error) - } -}) + app.get(`${apiPath}/wfs`, async (req, res, next) => { + const layerIDs = castArray(req.query.layerID) + try { + const data = await Promise.all(layerIDs.map(layerID => fetchGeoJSONLayer({ layerID }))) + res.json(data) + } catch (error) { + next(error) + } + }) -// Express server is used to serve the React app only in production -if (!isDevelopment) { - /* Routes are matched to a url in order of their definition + // Express server is used to serve the React app only in production + if (!isDevelopment) { + /* Routes are matched to a url in order of their definition Redirect all the the rest for react-router to handle */ - app.get('*', function (request, response) { - response.sendFile(path.join(publicPath, 'index.html')) - }) -} + app.get('*', function (request, response) { + response.sendFile(path.join(publicPath, 'index.html')) + }) + } -const servingInfo = isDevelopment - ? 'NODE_ENV=development, so Webpack serves the React app' - : `Static files (e.g. the React app) will be served from ${publicPath}` + const servingInfo = isDevelopment + ? 'NODE_ENV=development, so Webpack serves the React app' + : `Static files (e.g. the React app) will be served from ${publicPath}` -const port = app.get('port') + const port = app.get('port') -app.listen(port, () => - console.log(` - Express server listening on port ${port} - API path is ${apiPath} - ${servingInfo} - `) -) + app.listen(port, () => + console.log(` + Express server listening on port ${port} + API path is ${apiPath} + ${servingInfo} + `) + ) + }) diff --git a/src/server/openapi.yaml b/src/server/openapi.yaml new file mode 100644 index 0000000000000000000000000000000000000000..fb5baf1ae9ab37dac0dc8b236f29b0b6d6cc24a3 --- /dev/null +++ b/src/server/openapi.yaml @@ -0,0 +1,346 @@ +openapi: 3.0.3 +info: + title: Sampo-UI API + description: Description + version: 1.0.0 +servers: + - url: /api/v1 +paths: + /faceted-search/{resultClass}/paginated: + post: + summary: Return faceted search results with pagination + parameters: + - in: path + name: resultClass + schema: + type: string + example: perspective1 + required: true + description: The class of the results + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + page: + type: integer + default: 0 + example: 0 + pagesize: + type: integer + default: 10 + example: 10 + sortBy: + type: string + nullable: true + default: null + example: null + sortDirection: + type: string + nullable: true + default: null + example: null + constraints: + type: array + items: + type: object + nullable: true + default: null + example: null + responses: + '200': + description: Paginated search results + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + type: object + description: Results as an array of objects + page: + type: integer + description: The current page + pagesize: + type: integer + description: Items per page + resultClass: + type: string + description: The class of the results + sparqlQuery: + type: string + description: The SPARQL query that was used for the results + /faceted-search/{resultClass}/all: + post: + summary: Return all search results + parameters: + - in: path + name: resultClass + schema: + type: string + example: perspective1 + required: true + description: The class of the results + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + constraints: + type: array + items: + type: object + nullable: true + default: null + example: null + responses: + '200': + description: All search results + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + type: object + description: Results as an array of objects + sparqlQuery: + type: string + description: The SPARQL query that was used for the results + /faceted-search/{resultClass}/count: + post: + summary: Return the total count of the faceted search results + parameters: + - in: path + name: resultClass + schema: + type: string + example: perspective1 + required: true + description: The class of the results + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + constraints: + type: array + items: + type: object + nullable: true + default: null + example: null + responses: + '200': + description: The total count of the faceted search results + content: + application/json: + schema: + type: object + properties: + data: + type: integer + sparqlQuery: + type: string + resultClass: + type: string + constraints: + type: array + nullable: true + items: + type: object + default: null + /faceted-search/{facetClass}/facet/{id}: + post: + summary: Return values for a single facet + parameters: + - in: path + name: facetClass + schema: + type: string + required: true + description: The class of the facet + - in: path + name: id + schema: + type: string + required: true + description: The id of the facet + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + sortBy: + type: string + sortDirection: + type: string + constraints: + type: array + items: + type: object + nullable: true + default: null + constrainSelf: + type: boolean + default: false + responses: + '200': + description: Facet values + content: + application/json: + schema: + type: object + properties: + data: + oneOf: + - type: array + items: + type: object + - type: object + description: Facet values as an array of objects (checkbox facets) + or as a single object (timespan facets). + flatData: + type: array + items: + type: object + description: Facet values as an array of objects with no hierarchy + id: + type: string + description: The id of facet + sparqlQuery: + type: string + description: The SPARQL query that was used for the values of the facet + /{resultClass}/page/{uri}: + post: + summary: Return information about a single resource + parameters: + - in: path + name: resultClass + schema: + type: string + required: true + description: The class of the resource + - in: path + name: uri + schema: + type: string + required: true + description: The URI of the resource + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + facetClass: + type: string + constraints: + type: array + items: + type: object + nullable: true + default: null + responses: + '200': + description: Information about a single resource + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + type: object + description: An array containing one object describing the resource + sparqlQuery: + type: string + description: The SPARQL query that was used for retrieving the metadata + /full-text-search: + get: + summary: Full text search + parameters: + - in: query + name: q + schema: + type: string + required: true + description: The query string + responses: + '200': + description: Full text search results + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + type: object + description: An array of objects + sparqlQuery: + type: string + description: The SPARQL query that was used for retrieving the results + /federated-search: + get: + summary: Federated search can be used for retrieving the initial result set for + client-side faceted search. + parameters: + - in: query + name: dataset + schema: + type: array + items: + type: string + explode: true + required: true + - in: query + name: q + schema: + type: string + description: The query string + - in: query + name: latMin + schema: + type: number + - in: query + name: longMin + schema: + type: number + - in: query + name: latMax + schema: + type: number + - in: query + name: longMax + schema: + type: number + responses: + '200': + description: Federated search results + content: + application/json: + schema: + type: array + items: + type: object + description: Search results from multiple SPARQL endpoints + merged into a single array + + + diff --git a/src/server/sparql/FacetResults.js b/src/server/sparql/FacetResults.js index 8594b7b3894fc10ce1e2d8e5901a0dbd54395f5e..506cc3955eda05858f2ad750cefbc596f418af50 100644 --- a/src/server/sparql/FacetResults.js +++ b/src/server/sparql/FacetResults.js @@ -1,5 +1,4 @@ import { runSelectQuery } from './SparqlApi' -import { runNetworkQuery } from './NetworkApi' import { prefixes } from './sampo/SparqlQueriesPrefixes' import { countQuery, @@ -10,11 +9,8 @@ import { manuscriptPropertiesFacetResults, manuscriptPropertiesInstancePage, productionPlacesQuery, - productionCoordinatesQuery, lastKnownLocationsQuery, - migrationsQuery, - networkLinksQuery, - networkNodesQuery + migrationsQuery } from './sampo/SparqlQueriesPerspective1' import { workProperties } from './sampo/SparqlQueriesPerspective2' import { eventProperties, eventPlacesQuery } from './sampo/SparqlQueriesPerspective3' @@ -26,7 +22,7 @@ import { allPlacesQuery } from './sampo/SparqlQueriesPlaces' import { facetConfigs, endpoint } from './sampo/FacetConfigsSampo' -import { mapCount, mapPlaces, mapCoordinates } from './Mappers' +import { mapCount, mapPlaces } from './Mappers' import { makeObjectList } from './SparqlObjectMapper' import { generateConstraintsBlock } from './Filters' @@ -77,20 +73,15 @@ export const getAllResults = ({ filterTarget = 'id' break case 'placesMsProduced': - q = groupBy ? productionPlacesQuery : productionCoordinatesQuery + q = productionPlacesQuery filterTarget = 'manuscripts' - mapper = groupBy ? mapPlaces : mapCoordinates + mapper = mapPlaces break case 'lastKnownLocations': q = lastKnownLocationsQuery filterTarget = 'manuscripts' mapper = mapPlaces break - case 'placesActors': - q = placesActorsQuery - filterTarget = 'actor__id' - mapper = mapPlaces - break case 'placesMsMigrations': q = migrationsQuery filterTarget = 'manuscript__id' @@ -99,14 +90,6 @@ export const getAllResults = ({ q = eventPlacesQuery filterTarget = 'event' break - case 'eventsByTimePeriod': - q = generateEventsByPeriodQuery({ startYear: 1600, endYear: 1620, periodLength: 10 }) - filterTarget = 'event' - break - case 'manuscriptsNetwork': - q = networkLinksQuery - filterTarget = 'source' - break } if (constraints == null) { q = q.replace('<FILTER>', '# no filters') @@ -119,15 +102,6 @@ export const getAllResults = ({ facetID: null })) } - if (resultClass === 'manuscriptsNetwork') { - // console.log(prefixes + q) - return runNetworkQuery({ - endpoint, - prefixes, - links: q, - nodes: networkNodesQuery - }) - } // console.log(prefixes + q) return runSelectQuery({ query: prefixes + q, diff --git a/src/server/sparql/Filters.js b/src/server/sparql/Filters.js index f1e35b754448376e521e4647fc7a2cd339de4180..8b876665b30187d70aed0d321e1d733d8f607153 100644 --- a/src/server/sparql/Filters.js +++ b/src/server/sparql/Filters.js @@ -20,11 +20,11 @@ export const hasPreviousSelectionsFromOtherFacets = (constraints, facetID) => { } export const getUriFilters = (constraints, facetID) => { - for (const [key, value] of Object.entries(constraints)) { - if (key === facetID && value.filterType === 'uriFilter') { - return value.values + constraints.forEach(facet => { + if (facet.facetID === facetID && facet.filter === 'uriFilter') { + return facet.values } - } + }) return [] } @@ -36,27 +36,16 @@ export const generateConstraintsBlock = ({ inverse, constrainSelf = false }) => { - // delete constraints[facetID]; let filterStr = '' - const constraintsArr = [] const skipFacetID = constrainSelf ? '' : facetID - for (const [key, value] of Object.entries(constraints)) { - if (key !== skipFacetID) { - constraintsArr.push({ - id: key, - filterType: value.filterType, - priority: value.priority, - values: value.values - }) - } - } - constraintsArr.sort((a, b) => a.priority - b.priority) - constraintsArr.map(c => { + const modifiedConstraints = constraints.filter(facet => facet.facetID !== skipFacetID) + modifiedConstraints.sort((a, b) => a.priority - b.priority) + modifiedConstraints.map(c => { switch (c.filterType) { case 'textFilter': filterStr += generateTextFilter({ facetClass: facetClass, - facetID: c.id, + facetID: c.facetID, filterTarget: filterTarget, queryString: c.values, inverse: inverse @@ -65,7 +54,7 @@ export const generateConstraintsBlock = ({ case 'uriFilter': filterStr += generateUriFilter({ facetClass: facetClass, - facetID: c.id, + facetID: c.facetID, filterTarget: filterTarget, values: c.values, inverse: inverse @@ -74,7 +63,7 @@ export const generateConstraintsBlock = ({ case 'spatialFilter': filterStr += generateSpatialFilter({ facetClass: facetClass, - facetID: c.id, + facetID: c.facetID, filterTarget: filterTarget, values: c.values, inverse: inverse @@ -84,7 +73,7 @@ export const generateConstraintsBlock = ({ case 'dateFilter': filterStr += generateTimespanFilter({ facetClass: facetClass, - facetID: c.id, + facetID: c.facetID, filterTarget: filterTarget, values: c.values, inverse: inverse @@ -94,7 +83,7 @@ export const generateConstraintsBlock = ({ case 'integerFilterRange': filterStr += generateIntegerFilter({ facetClass: facetClass, - facetID: c.id, + facetID: c.facetID, filterTarget: filterTarget, values: c.values, inverse: inverse