diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 98d40b1c3..000000000 --- a/.npmignore +++ /dev/null @@ -1,6 +0,0 @@ -doc -.vscode/ -npm-debug.log -# The following (and `.npmignore` in general) can be removed again once this bug is fixed: -# https://github.com/linkeddata/rdflib.js/issues/369 -lib/index.d.ts diff --git a/package-lock.json b/package-lock.json index 64cdc1a5f..ebe5a3c07 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1249,6 +1249,12 @@ "@types/node": "*" } }, + "@types/jsonld": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@types/jsonld/-/jsonld-1.5.0.tgz", + "integrity": "sha512-EG2N8JLQ1xDfO6Z/1QRdiUcYX3428CqVRqmY7LyK5or5J1RQ16dpKH6qQ4umVD0vBHU47xHlMeyMbQ6o+6tiYg==", + "dev": true + }, "@types/mime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz", @@ -1735,6 +1741,65 @@ "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" + }, + "dependencies": { + "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 + }, + "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" + } + }, + "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 + }, + "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" + } + }, + "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 + } + } + }, "babel-loader": { "version": "8.0.6", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.0.6.tgz", @@ -1791,6 +1856,15 @@ } } }, + "backbone": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/backbone/-/backbone-1.4.0.tgz", + "integrity": "sha512-RLmDrRXkVdouTg38jcgHhyQ/2zjg7a8E6sz2zxfz21Hh17xDJYUHBZimVIt5fUyS8vbfpeSmTL3gUjTEvUV3qQ==", + "dev": true, + "requires": { + "underscore": ">=1.8.3" + } + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -2035,7 +2109,7 @@ }, "browserify-rsa": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", "dev": true, "requires": { @@ -2080,7 +2154,7 @@ }, "buffer": { "version": "4.9.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", "dev": true, "requires": { @@ -2200,15 +2274,6 @@ "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, - "catharsis": { - "version": "0.8.11", - "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.8.11.tgz", - "integrity": "sha512-a+xUyMV7hD1BrDQA/3iPV7oc+6W26BgVJO05PGEoatMyIuPScQKsde6i3YorWX1qs+AZjnJ18NqdKoCtKiNh1g==", - "dev": true, - "requires": { - "lodash": "^4.17.14" - } - }, "chai": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", @@ -3001,12 +3066,6 @@ "tapable": "^1.0.0" } }, - "entities": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", - "dev": true - }, "errno": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", @@ -3537,6 +3596,22 @@ "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" }, + "fork-ts-checker-webpack-plugin": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-1.6.0.tgz", + "integrity": "sha512-vqOY5gakcoon2s12V7MMe01OPwfgqulUWFzm+geQaPPOBKjW1I7aqqoBVlU0ECn97liMB0ECs16pRdIGe9qdRw==", + "dev": true, + "requires": { + "babel-code-frame": "^6.22.0", + "chalk": "^2.4.1", + "chokidar": "^2.0.4", + "micromatch": "^3.1.10", + "minimatch": "^3.0.4", + "semver": "^5.6.0", + "tapable": "^1.0.0", + "worker-rpc": "^0.1.0" + } + }, "form-data": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", @@ -4398,6 +4473,26 @@ "integrity": "sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ==", "dev": true }, + "handlebars": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.4.2.tgz", + "integrity": "sha512-cIv17+GhL8pHHnRJzGu2wwcthL5sb8uDKBHvZ2Dtu5s1YNt0ljbzKbamnc+gr69y7bzwQiBdr5+hOpRd5pnOdg==", + "dev": true, + "requires": { + "neo-async": "^2.6.0", + "optimist": "^0.6.1", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -4421,6 +4516,23 @@ "function-bind": "^1.1.1" } }, + "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" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + } + } + }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -4490,6 +4602,12 @@ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, + "highlight.js": { + "version": "9.15.10", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.15.10.tgz", + "integrity": "sha512-RoV7OkQm0T3os3Dd2VHLNMoaoDVx77Wygln3n9l5YV172XonWG6rgQD3XnF/BuFFZw9A0TJgmMSO8FEWQgvcXw==", + "dev": true + }, "hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -5004,6 +5122,12 @@ "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, + "jquery": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.1.tgz", + "integrity": "sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw==", + "dev": true + }, "js-levenshtein": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", @@ -5026,59 +5150,11 @@ "esprima": "^4.0.0" } }, - "js2xmlparser": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.0.tgz", - "integrity": "sha512-WuNgdZOXVmBk5kUPMcTcVUpbGRzLfNkv7+7APq7WiDihpXVKrgxo6wwRpRl9OQeEBgKCVk9mR7RbzrnNWC8oBw==", - "dev": true, - "requires": { - "xmlcreate": "^2.0.0" - } - }, "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" }, - "jsdoc": { - "version": "3.6.3", - "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.6.3.tgz", - "integrity": "sha512-Yf1ZKA3r9nvtMWHO1kEuMZTlHOF8uoQ0vyo5eH7SQy5YeIiHM+B0DgKnn+X6y6KDYZcF7G2SPkKF+JORCXWE/A==", - "dev": true, - "requires": { - "@babel/parser": "^7.4.4", - "bluebird": "^3.5.4", - "catharsis": "^0.8.11", - "escape-string-regexp": "^2.0.0", - "js2xmlparser": "^4.0.0", - "klaw": "^3.0.0", - "markdown-it": "^8.4.2", - "markdown-it-anchor": "^5.0.2", - "marked": "^0.7.0", - "mkdirp": "^0.5.1", - "requizzle": "^0.2.3", - "strip-json-comments": "^3.0.1", - "taffydb": "2.6.2", - "underscore": "~1.9.1" - }, - "dependencies": { - "escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": 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" - } - } - } - }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -5185,15 +5261,6 @@ "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", "dev": true }, - "klaw": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", - "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.9" - } - }, "lcid": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", @@ -5203,15 +5270,6 @@ "invert-kv": "^2.0.0" } }, - "linkify-it": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz", - "integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==", - "dev": true, - "requires": { - "uc.micro": "^1.0.1" - } - }, "loader-runner": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", @@ -5305,6 +5363,12 @@ "yallist": "^3.0.2" } }, + "lunr": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.7.tgz", + "integrity": "sha512-HjFSiy0Y0qZoW5OA1I6qBi7OnsDdqQnaUr03jhorh30maQoaP+4lQCKklYE3Nq3WJMSUfuBl6N+bKY5wxCb9hw==", + "dev": true + }, "make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -5345,25 +5409,6 @@ "object-visit": "^1.0.0" } }, - "markdown-it": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.2.tgz", - "integrity": "sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "entities": "~1.1.1", - "linkify-it": "^2.0.0", - "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" - } - }, - "markdown-it-anchor": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-5.2.4.tgz", - "integrity": "sha512-n8zCGjxA3T+Mx1pG8HEgbJbkB8JFUuRkeTZQuIM8iPY6oQ8sWOPRZJDFC9a/pNg2QkHEjjGkhBEl/RSyzaDZ3A==", - "dev": true - }, "marked": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz", @@ -5381,12 +5426,6 @@ "safe-buffer": "^5.1.2" } }, - "mdurl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=", - "dev": true - }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -5458,6 +5497,12 @@ "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", "dev": true }, + "microevent.ts": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/microevent.ts/-/microevent.ts-0.1.1.tgz", + "integrity": "sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g==", + "dev": true + }, "micromatch": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", @@ -5535,7 +5580,7 @@ }, "minimist": { "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "dev": true }, @@ -6252,6 +6297,16 @@ "is-wsl": "^1.1.0" } }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true, + "requires": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + } + }, "original": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz", @@ -6625,6 +6680,12 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, "promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -6840,6 +6901,15 @@ } } }, + "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" + } + }, "regenerate": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", @@ -6985,15 +7055,6 @@ "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", "dev": true }, - "requizzle": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.3.tgz", - "integrity": "sha512-YanoyJjykPxGHii0fZP0uUPEXpvqfBDxWV7s6GKAiiOsiqhX6vHNyW3Qzdmqp/iq/ExbhaGbVrjB4ruEVSM4GQ==", - "dev": true, - "requires": { - "lodash": "^4.17.14" - } - }, "resolve": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz", @@ -7299,6 +7360,17 @@ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", "dev": true }, + "shelljs": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.3.tgz", + "integrity": "sha512-fc0BKlAWiLpwZljmOvAOTE/gXawtCoNrP5oaY7KIaQbbyHeQVg01pSEuEGvGh3HEdBU4baCD7wQBwADmM/7f7A==", + "dev": true, + "requires": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + } + }, "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", @@ -7912,12 +7984,6 @@ "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", "dev": true }, - "strip-json-comments": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", - "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", - "dev": true - }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -7927,12 +7993,6 @@ "has-flag": "^3.0.0" } }, - "taffydb": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/taffydb/-/taffydb-2.6.2.tgz", - "integrity": "sha1-fLy2S1oUG2ou/CxdLGe04VCyomg=", - "dev": true - }, "tapable": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", @@ -8181,17 +8241,70 @@ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, + "typedoc": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.15.0.tgz", + "integrity": "sha512-NOtfq5Tis4EFt+J2ozhVq9RCeUnfEYMFKoU6nCXCXUULJz1UQynOM+yH3TkfZCPLzigbqB0tQYGVlktUWweKlw==", + "dev": true, + "requires": { + "@types/minimatch": "3.0.3", + "fs-extra": "^8.1.0", + "handlebars": "^4.1.2", + "highlight.js": "^9.15.8", + "lodash": "^4.17.15", + "marked": "^0.7.0", + "minimatch": "^3.0.0", + "progress": "^2.0.3", + "shelljs": "^0.8.3", + "typedoc-default-themes": "^0.6.0", + "typescript": "3.5.x" + }, + "dependencies": { + "typescript": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.3.tgz", + "integrity": "sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==", + "dev": true + } + } + }, + "typedoc-default-themes": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/typedoc-default-themes/-/typedoc-default-themes-0.6.0.tgz", + "integrity": "sha512-MdTROOojxod78CEv22rIA69o7crMPLnVZPefuDLt/WepXqJwgiSu8Xxq+H36x0Jj3YGc7lOglI2vPJ2GhoOybw==", + "dev": true, + "requires": { + "backbone": "^1.4.0", + "jquery": "^3.4.1", + "lunr": "^2.3.6", + "underscore": "^1.9.1" + } + }, "typescript": { - "version": "3.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.3.tgz", - "integrity": "sha512-N7bceJL1CtRQ2RiG0AQME13ksR7DiuQh/QehubYcghzv20tnh+MQnQIuJddTmsbqYj+dztchykemz0zFzlvdQw==", + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.2.tgz", + "integrity": "sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ==", "dev": true }, - "uc.micro": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", - "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", - "dev": true + "uglify-js": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.1.tgz", + "integrity": "sha512-+dSJLJpXBb6oMHP+Yvw8hUgElz4gLTh82XuX68QiJVTXaE5ibl6buzhNkQdYhBlIhozWOC9ge16wyRmjG4TwVQ==", + "dev": true, + "optional": true, + "requires": { + "commander": "2.20.0", + "source-map": "~0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true + } + } }, "underscore": { "version": "1.9.1", @@ -8837,6 +8950,12 @@ "integrity": "sha1-+OGqHuWlPsW/FR/6CXQqatdpeHY=", "dev": true }, + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "dev": true + }, "worker-farm": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", @@ -8846,9 +8965,18 @@ "errno": "~0.1.7" } }, + "worker-rpc": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/worker-rpc/-/worker-rpc-0.1.1.tgz", + "integrity": "sha512-P1WjMrUB3qgJNI9jfmpZ/htmBEjFh//6l/5y8SD9hg1Ef5zTTVVoRjTrTEzPrNBQvmhMxkoTsjOXN10GWU7aCg==", + "dev": true, + "requires": { + "microevent.ts": "~0.1.1" + } + }, "wrap-ansi": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", "dev": true, "requires": { @@ -8917,12 +9045,6 @@ "async-limiter": "~1.0.0" } }, - "xmlcreate": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.1.tgz", - "integrity": "sha512-MjGsXhKG8YjTKrDCXseFo3ClbMGvUD4en29H2Cev1dv4P/chlpw6KdYmlCWDkhosBVKRDjM836+3e3pm1cBNJA==", - "dev": true - }, "xmldom": { "version": "0.1.27", "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz", diff --git a/package.json b/package.json index 9e0bc61bd..afcdfe146 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,18 @@ { "name": "Daniel Friedman", "url": "https://github.com/dan-f/" + }, + { + "name": "Cénotélie", + "url": "https://github.com/cenotelie/" + }, + { + "name": "Joep Meindertsma", + "url": "https://github.com/joepio/" + }, + { + "name": "Thom van Kalkeren", + "url": "https://github.com/fletcher91/" } ], "license": "MIT", @@ -50,13 +62,14 @@ "@babel/register": "^7.5.5", "@types/chai": "^4.2.3", "@types/express": "^4.17.1", + "@types/jsonld": "^1.5.0", "@types/mocha": "^5.2.7", "babel-loader": "^8.0.6", "chai": "^4.2.0", "diff": "^4.0.1", "dirty-chai": "^2.0.1", + "fork-ts-checker-webpack-plugin": "^1.6.0", "fs-grep": "^0.0.5", - "jsdoc": "^3.6.3", "mocha": "^6.2.0", "nock": "^10.0.6", "node-fetch": "^2.6.0", @@ -65,7 +78,8 @@ "sinon": "^7.4.1", "sinon-chai": "^3.3.0", "source-map-loader": "^0.2.4", - "typescript": "^3.6.3", + "typedoc": "^0.15.3", + "typescript": "^3.7.2", "webpack": "^4.39.2", "webpack-cli": "^3.3.6", "webpack-dev-server": "^3.8.0", @@ -75,7 +89,7 @@ "build": "babel src --extensions \".ts,.js\" -d lib", "build:browser": "webpack --progress", "build:types": "tsc --emitDeclarationOnly -d --declarationDir lib --allowJs false", - "doc": "rm -r doc ; jsdoc -d doc README.md src/*.js", + "doc": "rm -r doc ; typedoc", "prepare": "npm run build && npm run build:browser", "start": "webpack-dev-server --https --port 4800", "test": "npm run test:unit && npm run test:serialize", diff --git a/reference/fetcher-classes.js b/reference/fetcher-classes.js index 9f11f47b6..7635a9847 100644 --- a/reference/fetcher-classes.js +++ b/reference/fetcher-classes.js @@ -1,8 +1,7 @@ -import { isNamedNode } from '../src/util' +import { isTFNamedNode } from '../src/utils' const log = require('./log') const N3Parser = require('./n3parser') -const NamedNode = require('./named-node') const Namespace = require('./namespace') const rdfParse = require('./parse') const parseRDFaDOM = require('./rdfaparser').parseRDFaDOM @@ -668,7 +667,7 @@ class Fetcher { userCallback = p2 } else if (typeof p2 === 'undefined') { // original calling signature // referingTerm = undefined - } else if (isNamedNode(p2)) { + } else if (isTFNamedNode(p2)) { // referingTerm = p2 options = {referingTerm: p2} } else { diff --git a/src/blank-node.ts b/src/blank-node.ts index 927793fa8..ac0d5e050 100644 --- a/src/blank-node.ts +++ b/src/blank-node.ts @@ -1,13 +1,19 @@ -'use strict' import ClassOrder from './class-order' import Node from './node-internal' -import { TermType } from './types' +import IndexedFormula from './store' +import { BlankNodeTermType, TermType, TFBlankNode } from './types' -export default class BlankNode extends Node { - static termType = TermType.BlankNode - static NTAnonymousNodePrefix = '_:' - /** The next unique identifier for blank nodes */ - static nextId = 0 +/** + * An RDF blank node is a Node without a URI + * @link https://rdf.js.org/data-model-spec/#blanknode-interface + */ +export default class BlankNode extends Node implements TFBlankNode { + /** + * The next unique identifier for blank nodes + */ + static nextId: number = 0; + static NTAnonymousNodePrefix: string = '_:' + static termType: BlankNodeTermType; private static getId (id: string | unknown): string { if (id) { @@ -30,18 +36,18 @@ export default class BlankNode extends Node { classOrder = ClassOrder.BlankNode /** Whether this is a blank node */ - isBlank = 1 + isBlank: number = 1 /** * This type of node is a variable. * * Note that the existence of this property already indicates that it is a variable. */ isVar = 1 - termType = BlankNode.termType + termType: BlankNodeTermType = TermType.BlankNode; /** * Initializes this node - * @param [id] - The identifier for the blank node + * @param [id] The identifier for the blank node */ constructor (id?: string | unknown) { super(BlankNode.getId(id)) @@ -59,7 +65,7 @@ export default class BlankNode extends Node { this.value = value } - compareTerm (other) { + compareTerm (other: BlankNode): number { if (this.classOrder < other.classOrder) { return -1 } @@ -75,7 +81,11 @@ export default class BlankNode extends Node { return 0 } - copy (formula) { // depends on the formula + /** + * Gets a copy of this blank node in the specified formula + * @param formula The formula + */ + copy (formula: IndexedFormula): BlankNode { // depends on the formula var bnodeNew = new BlankNode() formula.copyTo(this, bnodeNew) return bnodeNew diff --git a/src/class-order.ts b/src/class-order.ts index 6801115bb..d614b56e5 100644 --- a/src/class-order.ts +++ b/src/class-order.ts @@ -1,7 +1,9 @@ /** * Class orders */ -export default { +const ClassOrder: { + [id: string]: number; +} = { 'Literal': 1, 'Collection': 3, 'Graph': 4, @@ -9,3 +11,5 @@ export default { 'BlankNode': 6, 'Variable': 7 } + +export default ClassOrder diff --git a/src/collection.ts b/src/collection.ts index 3bebb69dd..16125ef21 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -1,31 +1,33 @@ -'use strict' import BlankNode from './blank-node' import ClassOrder from './class-order' -import Literal from "./literal"; +import Literal from './literal' import Node from './node-internal' -import { TermType } from './types'; -import Variable from "./variable"; +import { Bindings, FromValueReturns, TermType, ValueType } from './types' +import Variable from './variable' +import { isTFTerm } from './utils/terms' /** * Creates an RDF Node from a native javascript value. * RDF Nodes are returned unchanged, undefined returned as itself. + * Arrays return Collections. + * Strings, numbers and booleans return Literals. * @param value {Node|Date|String|Number|Boolean|Undefined} * @return {Node|Collection} */ -export function fromValue (value) { +export function fromValue = any, C extends Node = any>(value: ValueType): T { if (typeof value === 'undefined' || value === null) { - return value + return value as T } - const isNode = value && value.termType - if (isNode) { // a Node subclass or a Collection - return value + + if (isTFTerm(value)) { // a Node subclass or a Collection + return value as T } if (Array.isArray(value)) { - return new Collection(value) + return new Collection(value) as T } - return Literal.fromValue(value) + return Literal.fromValue(value) } /** @@ -46,7 +48,7 @@ export default class Collection) { super((BlankNode.nextId++).toString()) if (initial && initial.length > 0) { @@ -68,7 +70,7 @@ export default class Collection ea.substitute(bindings)) return new Collection(elementsCopy) } @@ -119,7 +119,7 @@ export default class Collection' - case "BlankNode": - return '_:' + term.value - case "Literal": - return Literal.toNT(term) - case "Variable": - return Variable.toString(term) - default: - return undefined - } -} - -function literal (value, languageOrDatatype) { - if (typeof value !== "string" && !languageOrDatatype) { - return Literal.fromValue(value) - } - - const strValue = typeof value === 'string' ? value : '' + value - if (typeof languageOrDatatype === 'string') { - if (languageOrDatatype.indexOf(':') === -1) { - return new Literal(strValue, languageOrDatatype) - } else { - return new Literal(strValue, null, namedNode(languageOrDatatype)) - } - } else { - return new Literal(strValue, null, languageOrDatatype) - } -} -function namedNode (value) { - return new NamedNode(value) -} -function quad (subject, predicate, object, graph) { - graph = graph || defaultGraph() - return new Statement(subject, predicate, object, graph) -} -function variable (name) { - return new Variable(name) -} - -/** Contains the factory methods as defined in the spec, plus id */ -export default { - blankNode, - defaultGraph, - literal, - namedNode, - quad, - variable, - id, - supports: { - COLLECTIONS: false, - DEFAULT_GRAPH_TYPE: true, - EQUALS_METHOD: true, - NODE_LOOKUP: false, - VARIABLE_TYPE: true, - } -} diff --git a/src/data-factory-internal.ts b/src/data-factory-internal.ts new file mode 100644 index 000000000..d0bd7f463 --- /dev/null +++ b/src/data-factory-internal.ts @@ -0,0 +1,161 @@ +import BlankNode from './blank-node' +import Literal from './literal' +import NamedNode from './named-node' +import Statement from './statement' +import Variable from './variable' +import { + TFNamedNode, + SubjectType, + PredicateType, + ObjectType, + GraphType, + TermType, + TFDataFactory, + TFTerm, +} from './types' +import { Feature, IdentityFactory, Indexable } from './data-factory-type' +import Node from './node-internal' + +export const defaultGraphURI = 'chrome:theSession' +const defaultGraphNode = new NamedNode(defaultGraphURI) + +/** + * Creates a new blank node + * @param value The blank node's identifier + */ +function blankNode(value?: string): BlankNode { + return new BlankNode(value) +} + +/** + * Gets the default graph + */ +export function defaultGraph(): NamedNode { + return defaultGraphNode +} + +/** + * Generates a unique identifier for the object. + * + * Equivalent to {Term.hashString} + */ +function id (term: TFTerm): string | undefined { + if (!term) { + return term + } + if (Object.prototype.hasOwnProperty.call(term, "id") && typeof (term as NamedNode).id === "function") { + return (term as NamedNode).id() + } + if (Object.prototype.hasOwnProperty.call(term, "hashString")) { + return (term as Node).hashString() + } + + switch (term.termType) { + case TermType.NamedNode: + return '<' + term.value + '>' + case TermType.BlankNode: + return '_:' + term.value + case TermType.Literal: + return Literal.toNT(term as Literal) + case TermType.Variable: + return Variable.toString(term) + default: + return undefined + } +} + +/** + * Creates a new literal node + * @param value The lexical value + * @param languageOrDatatype Either the language or the datatype + */ +function literal( + value: string, + languageOrDatatype?: string | TFNamedNode +): Literal { + if (typeof value !== "string" && !languageOrDatatype) { + return Literal.fromValue(value) as Literal + } + + const strValue = typeof value === 'string' ? value : '' + value + if (typeof languageOrDatatype === 'string') { + if (languageOrDatatype.indexOf(':') === -1) { + return new Literal(strValue, languageOrDatatype) + } else { + return new Literal(strValue, null, namedNode(languageOrDatatype)) + } + } else { + return new Literal(strValue, null, languageOrDatatype) + } +} + +/** + * Creates a new named node + * @param value The new named node + */ +function namedNode(value: string): NamedNode { + return new NamedNode(value) +} + +/** +* Creates a new statement +* @param subject The subject +* @param predicate The predicate +* @param object The object +* @param graph The containing graph +*/ +function quad( + subject: SubjectType, + predicate: PredicateType, + object: ObjectType, + graph?: GraphType +): Statement { + graph = graph || defaultGraph() + return new Statement(subject, predicate, object, graph) +} + +/** + * Creates a new variable + * @param name The name for the variable + */ +function variable(name?: string): Variable { + return new Variable(name) +} + +/** The internal RDFlib datafactory, which uses Collections */ +const CanonicalDataFactory: TFDataFactory< + NamedNode, + BlankNode, + Literal, + SubjectType, + PredicateType, + ObjectType, + GraphType, + // DefaultGraph is: + NamedNode | BlankNode, + Statement +> & IdentityFactory < + Statement, + NamedNode | BlankNode | Literal | Variable +> = { + blankNode, + defaultGraph, + literal, + namedNode, + quad, + variable, + // @ts-ignore id should return an Indexable value, but in RDFlib can return `undefined` + id, + supports: { + [Feature.collections]: false, + [Feature.defaultGraphType]: true, + [Feature.equalsMethod]: true, + [Feature.identity]: false, + [Feature.id]: true, + [Feature.reversibleId]: false, + [Feature.variableType]: true, + } +} + +/** Contains the factory methods as defined in the spec, plus id */ +export default CanonicalDataFactory diff --git a/src/data-factory-type.ts b/src/data-factory-type.ts new file mode 100644 index 000000000..7e0e5a185 --- /dev/null +++ b/src/data-factory-type.ts @@ -0,0 +1,104 @@ +import { TFNamedNode, TFBlankNode, TFLiteral, TFQuad, TFTerm, TFDataFactory, TFSubject, TFPredicate, TFObject, TFGraph, TFVariable } from "./types" + +/** + * Defines a strict subset of the DataFactory as defined in the RDF/JS: Data model specification + * Non RDF-native features have been removed (e.g. no Variable, no Literal as predicate, etc.). + * bnIndex is optional but useful. + */ +export interface DataFactory< + NamedNode extends TFNamedNode = TFNamedNode, + BlankNode extends TFBlankNode = TFBlankNode, + Literal extends TFLiteral = TFLiteral, + Quad = TFQuad, + FactoryTypes = NamedNode | TFBlankNode | Literal | Quad, + Subject = TFSubject, + Predicate = TFPredicate, + Object = TFObject, + Graph = TFGraph, + DefaultGraph = NamedNode | BlankNode, +> extends TFDataFactory< + NamedNode, + BlankNode, + Literal, + Subject, + Predicate, + Object, + Graph, + DefaultGraph, + Quad +> { + /** + * BlankNode index + * @private + */ + bnIndex?: number + + supports: SupportTable + + literal(value: string, languageOrDatatype?: string | TFNamedNode): Literal + + isQuad(obj: any): obj is Quad + + equals(a: Comparable, b: Comparable): boolean + + toNQ(term: FactoryTypes): string +} + +export type TFIDFactoryTypes = TFNamedNode | TFBlankNode | TFLiteral | TFQuad | TFVariable | TFTerm + +export interface IdentityFactory< + Quad = TFQuad, + IDFactoryTypes = TFNamedNode | TFBlankNode | TFLiteral | Quad | TFVariable | TFTerm, + IndexType = Indexable, +> { + /** + * Generates a unique session-idempotent identifier for the given object. + * + * @example NQ serialization (reversible from value) + * @example MD5 hash of termType + value (irreversible from value, map needed) + * + * @return {Indexable} A unique value which must also be a valid JS object key type. + */ + id(obj: IDFactoryTypes): IndexType +} + +/** + * Factory type which supports reverse id lookups. + * + * It should be able to resolve the value for any given id which it handed out. Passing an id not + * generated by the same instance might result in a value or an exception depending on the + * implementation. + */ +export interface ReversibleIdentityFactory< + IndexType = Indexable, + FactoryTypes = TFNamedNode | TFBlankNode | TFLiteral | TFQuad +> extends IdentityFactory { + fromId(id: IndexType): FactoryTypes; +} + +export type Namespace = (term:string) => TFNamedNode +export type NamespaceCreator = (ns: string) => Namespace + +/** A set of features that may be supported by a Data Factory */ +export type SupportTable = Record + +export enum Feature { + /** Whether the factory supports termType:Collection terms */ + collections = "COLLECTIONS", + /** Whether the factory supports termType:DefaultGraph terms */ + defaultGraphType = "DEFAULT_GRAPH_TYPE", + /** Whether the factory supports equals on produced instances */ + equalsMethod = "EQUALS_METHOD", + /** Whether the factory can create a unique idempotent identifier for the given term. */ + id = "ID", + /** Whether the factory will return the same instance for subsequent calls (implies `===`). */ + identity = "IDENTITY", + /** Whether the factory supports mapping ids back to instances (should adhere to the identity setting) */ + reversibleId = "REVERSIBLE_ID", + /** Whether the factory supports termType:Variable terms */ + variableType = "VARIABLE_TYPE", +} + +export type Comparable = TFNamedNode | TFBlankNode | TFLiteral | TFQuad | undefined | null + +export type Indexable = number | string diff --git a/src/data-factory.js b/src/data-factory.js deleted file mode 100644 index 73293f38f..000000000 --- a/src/data-factory.js +++ /dev/null @@ -1,72 +0,0 @@ -'use strict' -import Collection from './collection' -import CanonicalDataFactory from './data-factory-internal' -import Fetcher from './fetcher' -import Literal from './literal' -import Statement from './statement' -import IndexedFormula from './store' - -/** - * Data factory which also supports Collections - * - * Necessary for preventing circular dependencies. - */ -const ExtendedTermFactory = { - ...CanonicalDataFactory, - collection, - id, - supports: { - COLLECTIONS: true, - DEFAULT_GRAPH_TYPE: true, - EQUALS_METHOD: true, - NODE_LOOKUP: false, - VARIABLE_TYPE: true, - } -} - -/** Full RDFLib.js Data Factory */ -const DataFactory = { - ...ExtendedTermFactory, - fetcher, - graph, - lit, - st, - triple, -} -export default DataFactory - -function id (term) { - if (!term) { - return term - } - if (Object.prototype.hasOwnProperty.call(term, "id") && typeof term.id === "function") { - return term.id() - } - if (Object.prototype.hasOwnProperty.call(term, "hashString")) { - return term.hashString() - } - - if (term.termType === "Collection") { - Collection.toNT(term) - } - - return CanonicalDataFactory.id(term) -} -function collection (elements) { - return new Collection(elements) -} -function fetcher (store, options) { - return new Fetcher(store, options) -} -function graph (features = undefined, opts = undefined) { - return new IndexedFormula(features, opts || { rdfFactory: ExtendedTermFactory }) -} -function lit (val, lang, dt) { - return new Literal('' + val, lang, dt) -} -function st (subject, predicate, object, graph) { - return new Statement(subject, predicate, object, graph) -} -function triple (subject, predicate, object) { - return CanonicalDataFactory.quad(subject, predicate, object) -} diff --git a/src/data-factory.ts b/src/data-factory.ts new file mode 100644 index 000000000..6f3bb22b8 --- /dev/null +++ b/src/data-factory.ts @@ -0,0 +1,138 @@ +import Collection from './collection' +import CanonicalDataFactory from './data-factory-internal' +import Fetcher from './fetcher' +import Literal from './literal' +import Statement from './statement' +import { ValueType, TFNamedNode, SubjectType, PredicateType, ObjectType, GraphType, TFBlankNode, TFLiteral } from './types' +import IndexedFormula from './store' +import { DataFactory, Indexable, SupportTable } from './data-factory-type' +import NamedNode from './named-node' + +const RDFlibjsSupports: SupportTable = { + COLLECTIONS: true, + DEFAULT_GRAPH_TYPE: true, + EQUALS_METHOD: true, + VARIABLE_TYPE: true, + IDENTITY: false, + REVERSIBLE_ID: false, + ID: false, +} + +/** + * Data factory which also supports Collections + * + * Necessary for preventing circular dependencies. + */ +const ExtendedTermFactory = { + ...CanonicalDataFactory, + collection, + id, + supports: RDFlibjsSupports +} + +interface IRDFlibDataFactory extends DataFactory { + fetcher: (store: IndexedFormula, options: any) => Fetcher + graph: (features, opts) => IndexedFormula + lit: (val: string, lang?: string, dt?: TFNamedNode) => Literal + st: ( + subject: SubjectType, + predicate: PredicateType, + object: ObjectType, + graph?: GraphType + ) => Statement + triple: ( + subject: SubjectType, + predicate: PredicateType, + object: ObjectType + ) => Statement +} + +/** Full RDFLib.js Data Factory + * @todo Add missing functions (isQuad, equals, toNQ), so Partial can be removed + */ +const RDFlibDataFactory: Partial = { + ...ExtendedTermFactory, + fetcher, + graph, + lit, + st, + triple, +} + +export default RDFlibDataFactory + +function id (term: TFNamedNode | TFBlankNode | TFLiteral | Collection ): Indexable { + if (!term) { + return term + } + if (Object.prototype.hasOwnProperty.call(term, "id") && typeof (term as NamedNode).id === "function") { + return (term as NamedNode).id() + } + if (Object.prototype.hasOwnProperty.call(term, "hashString")) { + return (term as NamedNode).hashString() + } + + if (term.termType === "Collection") { + Collection.toNT(term) + } + + return CanonicalDataFactory.id(term as NamedNode) +} +/** + * Creates a new collection + * @param elements - The initial element + */ +function collection(elements: ReadonlyArray): Collection { + return new Collection(elements) +} +/** + * Creates a new fetcher + * @param store - The store to use + * @param options - The options + */ +function fetcher(store: IndexedFormula, options: any): Fetcher { + return new Fetcher(store, options) +} +/** + * Creates a new graph (store) + */ +function graph (features = undefined, opts = undefined): IndexedFormula { + return new IndexedFormula(features, opts || { rdfFactory: ExtendedTermFactory }) +} +/** + * Creates a new literal node + * @param val The lexical value + * @param lang The language + * @param dt The datatype + */ +function lit(val: string, lang?: string, dt?: TFNamedNode): Literal { + return new Literal('' + val, lang, dt) +} +/** + * Creates a new statement + * @param subject The subject + * @param predicate The predicate + * @param object The object + * @param graph The containing graph + */ +function st( + subject: SubjectType, + predicate: PredicateType, + object: ObjectType, + graph?: GraphType +): Statement { + return new Statement(subject, predicate, object, graph) +} +/** + * Creates a new statement + * @param subject The subject + * @param predicate The predicate + * @param object The object + */ +function triple( + subject: SubjectType, + predicate: PredicateType, + object: ObjectType +): Statement { + return CanonicalDataFactory.quad(subject, predicate, object) +} diff --git a/src/default-graph.ts b/src/default-graph.ts index 193481213..fe7d53cab 100644 --- a/src/default-graph.ts +++ b/src/default-graph.ts @@ -1,12 +1,12 @@ 'use strict' import Node from './node-internal' -import { TermType } from "./types"; +import { TFDefaultGraph, TermType, DefaultGraphTermType } from './types' /** The RDF default graph */ -export default class DefaultGraph extends Node { +export default class DefaultGraph extends Node implements TFDefaultGraph { static termType = TermType.DefaultGraph; - termType = TermType.DefaultGraph; + termType: DefaultGraphTermType = TermType.DefaultGraph; constructor () { super('') diff --git a/src/empty.ts b/src/empty.ts index 80dbf7441..2067bdf55 100644 --- a/src/empty.ts +++ b/src/empty.ts @@ -1,10 +1,9 @@ -'use strict' import Node from './node-internal' -import { TermType } from "./types"; +import { TermType } from './types' /** - * Singleton subclass of an empty Collection. - */ +* An empty node +*/ export default class Empty extends Node { static termType = TermType.Empty diff --git a/src/fetcher.js b/src/fetcher.ts similarity index 65% rename from src/fetcher.js rename to src/fetcher.ts index 5a37eca51..1d3d8539a 100644 --- a/src/fetcher.js +++ b/src/fetcher.ts @@ -34,12 +34,23 @@ import rdfParse from './parse' import { parseRDFaDOM } from './rdfaparser' import RDFParser from './rdfxmlparser' import * as Uri from './uri' -import { isNamedNode } from './utils/terms' import * as Util from './util' +import { isCollection, isTFNamedNode, termValue } from './utils/terms' import serialize from './serialize' +// @ts-ignore This is injected import { fetch as solidAuthCli } from 'solid-auth-cli' +// @ts-ignore This is injected import { fetch as solidAuthClient } from 'solid-auth-client' +import { + TFBlankNode, + TFNamedNode, + TFGraph, + TFSubject, + ContentType, + TFDataFactory, + TFPredicate +} from './types' // This is a special fetch which does OIDC auth, catching 401 errors const fetch = typeof window === 'undefined' ? solidAuthCli : solidAuthClient @@ -70,7 +81,7 @@ const CONTENT_TYPE_BY_EXT = { // make its own list and not rely on the prefixes used here, // and not be tempted to add to them, and them clash with those of another // application. -const getNS = (factory) => { +const getNS = (factory?: TFDataFactory) => { return { link: Namespace('http://www.w3.org/2007/ont/link#', factory), http: Namespace('http://www.w3.org/2007/ont/http#', factory), @@ -83,10 +94,117 @@ const getNS = (factory) => { } const ns = getNS() +interface FetchError extends Error { + statusText?: string + status?: StatusValues + response?: ExtendedResponse +} + +/** An extended interface of Response, since RDFlib.js adds some properties. */ +interface ExtendedResponse extends Response { + /** String representation of the Body */ + responseText?: string + /** Identifier of the reqest */ + req?: TFSubject + size?: number + timeout?: number + /** Used in UpdateManager.updateDav */ + error?: string +} + +/** tell typescript that a 'panes' child may exist on Window */ +declare global { + interface Window { + panes?: any + } +} + +declare var $SolidTestEnvironment: { + localSiteMap?: any +} + +type UserCallback = ( + ok: boolean, + message: string, + response?: any +) => void + +type HTTPMethods = 'GET' | 'PUT' | 'POST' | 'PATCH' | 'HEAD' | 'DELETE' | 'CONNECT' | 'TRACE' | 'OPTIONS' + +/** All valid inputs for initFetchOptions */ +type Options = Partial + +/** Initiated by initFetchOptions, which runs on load */ +interface AutoInitOptions extends RequestInit{ + /** The used Fetch function */ + fetch?: Fetch + /** + * Referring term, the resource which + * referred to this (for tracking bad links). + * The document in which this link was found. + */ + referringTerm?: TFNamedNode + /** Provided content type (for writes) */ + contentType?: string + /** + * Override the incoming header to + * force the data to be treated as this content-type (for reads) + */ + forceContentType?: ContentType + /** + * Load the data even if loaded before. + * Also sets the `Cache-Control:` header to `no-cache` + */ + force?: boolean + /** + * Original uri to preserve + * through proxying etc (`xhr.original`). + */ + baseURI: string + /** + * Whether this request is a retry via + * a proxy (generally done from an error handler) + */ + proxyUsed?: boolean + actualProxyURI?: string + /** flag for XHR/CORS etc */ + withCredentials?: boolean + /** Before we parse new data, clear old, but only on status 200 responses */ + clearPreviousData?: boolean + /** Prevents the addition of various metadata triples (about the fetch request) to the store*/ + noMeta?: boolean + noRDFa?: boolean + handlers?: Handler[] + timeout?: number + method?: HTTPMethods + retriedWithNoCredentials?: boolean + requestedURI?: string + // Seems to be required in some functions, such as XHTML parse and RedirectToProxy + resource: TFSubject + /** The serialized resource in the body*/ + // Used for storing metadata of requests + original: TFNamedNode + // Like requeststatus? Can contain text with error. + data?: string + // Probably an identifier for request?s + req: TFBlankNode + // Might be the same as Options.data + body?: string + headers: Headers + credentials?: 'include' | 'omit' +} + class Handler { - constructor (response, dom) { + // TODO: Document, type + response: ExtendedResponse + // TODO: Document, type + dom: Document + static pattern: RegExp + + constructor (response: ExtendedResponse, dom?: Document) { this.response = response - this.dom = dom + // The bang operator here might need to be removed. + this.dom = dom! } } @@ -95,13 +213,22 @@ class RDFXMLHandler extends Handler { return 'RDFXMLHandler' } - static register (fetcher) { + static register (fetcher: Fetcher) { fetcher.mediatypes['application/rdf+xml'] = { 'q': 0.9 } } - parse (fetcher, responseText, options, response) { + parse ( + fetcher: Fetcher, + /** An XML String */ + responseText: String, + /** Requires .original */ + options: { + original: TFSubject + req: TFSubject + } & Options, + ) { let kb = fetcher.store if (!this.dom) { this.dom = Util.parseXML(responseText) @@ -110,11 +237,11 @@ class RDFXMLHandler extends Handler { if (root.nodeName === 'parsererror') { // Mozilla only See issue/issue110 // have to fail the request return fetcher.failFetch(options, 'Badly formed XML in ' + - options.resource.uri, 'parse_error') + (options as any).resource.value, 'parse_error') } let parser = new RDFParser(kb) try { - parser.parse(this.dom, options.original.uri, options.original, response) + parser.parse(this.dom, options.original.value, options.original) } catch (err) { return fetcher.failFetch(options, 'Syntax error parsing RDF/XML! ' + err, 'parse_error') @@ -133,12 +260,19 @@ class XHTMLHandler extends Handler { return 'XHTMLHandler' } - static register (fetcher) { + static register (fetcher: Fetcher) { fetcher.mediatypes['application/xhtml+xml'] = {} } - parse (fetcher, responseText, options, response) { - let relation, reverse + parse ( + fetcher: Fetcher, + responseText: string, + options: { + resource: TFSubject + original: TFSubject + } & Options, + ): Promise | ExtendedResponse { + let relation, reverse: boolean if (!this.dom) { this.dom = Util.parseXML(responseText) } @@ -147,7 +281,7 @@ class XHTMLHandler extends Handler { // dc:title let title = this.dom.getElementsByTagName('title') if (title.length > 0) { - kb.add(options.resource, ns.dc('title'), kb.literal(title[0].textContent), + kb.add(options.resource, ns.dc('title'), kb.rdfFactory.literal(title[0].textContent as string), options.resource) // log.info("Inferring title of " + xhr.resource) } @@ -163,7 +297,7 @@ class XHTMLHandler extends Handler { } if (relation) { fetcher.linkData(options.original, relation, - links[x].getAttribute('href'), options.resource, reverse) + links[x].getAttribute('href') as string, options.resource, reverse) } } @@ -171,9 +305,11 @@ class XHTMLHandler extends Handler { let scripts = this.dom.getElementsByTagName('script') for (let i = 0; i < scripts.length; i++) { let contentType = scripts[i].getAttribute('type') - if (Parsable[contentType]) { - rdfParse(scripts[i].textContent, kb, options.original.uri, contentType) - rdfParse(scripts[i].textContent, kb, options.original.uri, contentType) + if (Parsable[contentType!]) { + // @ts-ignore incompatibility between Store.add and Formula.add + rdfParse(scripts[i].textContent as string, kb, options.original.value, contentType) + // @ts-ignore incompatibility between Store.add and Formula.add + rdfParse(scripts[i].textContent as string, kb, options.original.value, contentType) } } @@ -183,15 +319,15 @@ class XHTMLHandler extends Handler { if (!options.noRDFa && parseRDFaDOM) { // enable by default try { - parseRDFaDOM(this.dom, kb, options.original.uri) + parseRDFaDOM(this.dom, kb, options.original.value) } catch (err) { let msg = 'Error trying to parse ' + options.resource + ' as RDFa:\n' + err + ':\n' + err.stack - return fetcher.failFetch(options, msg, 'parse_error') + return fetcher.failFetch(options as AutoInitOptions, msg, 'parse_error') } } - return fetcher.doneFetch(options, this.response) + return fetcher.doneFetch(options as AutoInitOptions, this.response) } } XHTMLHandler.pattern = new RegExp('application/xhtml') @@ -201,12 +337,20 @@ class XMLHandler extends Handler { return 'XMLHandler' } - static register (fetcher) { + static register (fetcher: Fetcher) { fetcher.mediatypes['text/xml'] = { 'q': 0.5 } fetcher.mediatypes['application/xml'] = { 'q': 0.5 } } - parse (fetcher, responseText, options, response) { + parse ( + fetcher: Fetcher, + responseText: string, + options: { + original: TFSubject + req: TFBlankNode + resource: TFSubject + } & Options, + ): ExtendedResponse | Promise { let dom = Util.parseXML(responseText) // XML Semantics defined by root element namespace @@ -223,7 +367,7 @@ class XMLHandler extends Handler { 'Has XML root element in the RDF namespace, so assume RDF/XML.') let rdfHandler = new RDFXMLHandler(this.response, dom) - return rdfHandler.parse(fetcher, responseText, options, response) + return rdfHandler.parse(fetcher, responseText, options) } break @@ -241,7 +385,7 @@ class XMLHandler extends Handler { 'Has XHTML DOCTYPE. Switching to XHTML Handler.\n') let xhtmlHandler = new XHTMLHandler(this.response, dom) - return xhtmlHandler.parse(fetcher, responseText, options, response) + return xhtmlHandler.parse(fetcher, responseText, options) } } @@ -254,7 +398,7 @@ class XMLHandler extends Handler { 'Has a default namespace for ' + 'XHTML. Switching to XHTMLHandler.\n') let xhtmlHandler = new XHTMLHandler(this.response, dom) - return xhtmlHandler.parse(fetcher, responseText, options, response) + return xhtmlHandler.parse(fetcher, responseText, options) } } @@ -275,13 +419,21 @@ class HTMLHandler extends Handler { return 'HTMLHandler' } - static register (fetcher) { + static register (fetcher: Fetcher) { fetcher.mediatypes['text/html'] = { 'q': 0.9 } } - parse (fetcher, responseText, options, response) { + parse ( + fetcher: Fetcher, + responseText: string, + options: { + req: TFBlankNode, + resource: TFSubject, + original: TFSubject, + } & Options + ): Promise | ExtendedResponse { let kb = fetcher.store // We only handle XHTML so we have to figure out if this is XML @@ -291,7 +443,7 @@ class HTMLHandler extends Handler { "it's XHTML as the content-type was text/html.\n") let xhtmlHandler = new XHTMLHandler(this.response) - return xhtmlHandler.parse(fetcher, responseText, options, response) + return xhtmlHandler.parse(fetcher, responseText, options) } // DOCTYPE html @@ -300,7 +452,7 @@ class HTMLHandler extends Handler { 'Has XHTML DOCTYPE. Switching to XHTMLHandler.\n') let xhtmlHandler = new XHTMLHandler(this.response) - return xhtmlHandler.parse(fetcher, responseText, options, response) + return xhtmlHandler.parse(fetcher, responseText, options) } // xmlns @@ -309,14 +461,14 @@ class HTMLHandler extends Handler { 'Has default namespace for XHTML, so switching to XHTMLHandler.\n') let xhtmlHandler = new XHTMLHandler(this.response) - return xhtmlHandler.parse(fetcher, responseText, options, response) + return xhtmlHandler.parse(fetcher, responseText, options) } // dc:title // no need to escape '/' here let titleMatch = (new RegExp('([\\s\\S]+?)', 'im')).exec(responseText) if (titleMatch) { - kb.add(options.resource, ns.dc('title'), kb.literal(titleMatch[1]), + kb.add(options.resource, ns.dc('title'), kb.rdfFactory.literal(titleMatch[1]), options.resource) // think about xml:lang later } kb.add(options.resource, ns.rdf('type'), ns.link('WebPage'), fetcher.appNode) @@ -332,13 +484,21 @@ class TextHandler extends Handler { return 'TextHandler' } - static register (fetcher) { + static register (fetcher: Fetcher) { fetcher.mediatypes['text/plain'] = { 'q': 0.5 } } - parse (fetcher, responseText, options, response) { + parse ( + fetcher: Fetcher, + responseText: string, + options: { + req: TFSubject + original: TFSubject + resource: TFSubject + } & Options + ): ExtendedResponse | Promise { // We only speak dialects of XML right now. Is this XML? // Look for an XML declaration @@ -348,7 +508,7 @@ class TextHandler extends Handler { "it's XML but its content-type wasn't XML.\n") let xmlHandler = new XMLHandler(this.response) - return xmlHandler.parse(fetcher, responseText, options, response) + return xmlHandler.parse(fetcher, responseText, options) } // Look for an XML declaration @@ -357,7 +517,7 @@ class TextHandler extends Handler { "it's XML but its content-type wasn't XML.\n") let xmlHandler = new XMLHandler(this.response) - return xmlHandler.parse(fetcher, responseText, options, response) + return xmlHandler.parse(fetcher, responseText, options) } // We give up finding semantics - this is not an error, just no data @@ -373,7 +533,7 @@ class N3Handler extends Handler { return 'N3Handler' } - static register (fetcher) { + static register (fetcher: Fetcher) { fetcher.mediatypes['text/n3'] = { 'q': '1.0' } // as per 2008 spec @@ -387,10 +547,18 @@ class N3Handler extends Handler { } // post 2008 } - parse (fetcher, responseText, options, response) { + parse ( + fetcher: Fetcher, + responseText: string, + options: { + original: TFNamedNode + req: TFSubject + } & Options, + response: ExtendedResponse + ): ExtendedResponse | Promise { // Parse the text of this N3 file let kb = fetcher.store - let p = N3Parser(kb, kb, options.original.uri, options.original.uri, + let p = N3Parser(kb, kb, options.original.value, options.original.value, null, null, '', null) // p.loadBuf(xhr.responseText) try { @@ -409,7 +577,7 @@ class N3Handler extends Handler { } N3Handler.pattern = new RegExp('(application|text)/(x-)?(rdf\\+)?(n3|turtle)') -const HANDLERS = { +const defaultHandlers = { RDFXMLHandler, XHTMLHandler, XMLHandler, HTMLHandler, TextHandler, N3Handler } @@ -422,12 +590,78 @@ function isXHTML (responseText) { return responseText.substr(docTypeStart, docTypeEnd - docTypeStart).indexOf('XHTML') !== -1 } -function isXML (responseText) { - return responseText.match(/\s*<\?xml\s+version\s*=[^<>]+\?>/) +function isXML (responseText: string): boolean { + const match = responseText.match(/\s*<\?xml\s+version\s*=[^<>]+\?>/) + return match ? true : false } -function isXMLNS (responseText) { - return responseText.match(/[^(/) +function isXMLNS (responseText: string): boolean { + const match = responseText.match(/[^(/) + return match ? true : false +} + +type StatusValues = + /** No record of web access or record reset */ + undefined | + /** Has been requested, fetch in progress */ + true | + /** Received, OK */ + 'done' | + /** Not logged in */ + 401 | + /** HTTP status unauthorized */ + 403 | + /** Not found, resource does not exist */ + 404 | + /** In attempt to counter CORS problems retried */ + 'redirected' | + /** If it did fail */ + 'failed' | + 'parse_error' | + /** + * URI is not a protocol Fetcher can deal with + * other strings mean various other errors. + */ + 'unsupported_protocol' | + 'timeout' | + /** Any other HTTP status code */ + number + +interface MediatypesMap { + [id: string]: { + // Either string '1.0' or number 1.0 is allowed + 'q'?: number | string + }; +} + +interface RequestedMap { + [uri: string]: StatusValues +} + +interface TimeOutsMap { + [uri: string]: number[] +} + +interface FetchQueue { + [uri: string]: Promise +} + +interface FetchCallbacks { + [uri: string]: UserCallback[] +} + +interface BooleanMap { + [uri: string]: boolean +} + +// Not sure about the shapes of this. Response? FetchError? +type Result = Response + +/** Differs from normal Fetch, has an extended Response type */ +type Fetch = (input: RequestInfo, init?: RequestInit) => Promise; + +interface CallbackifyInterface { + fireCallbacks: Function } /** Fetcher @@ -438,11 +672,50 @@ function isXMLNS (responseText) { * figuring how to parse them. It will also refresh, remove, the data * and put back the fata to the web. */ -export default class Fetcher { +export default class Fetcher implements CallbackifyInterface { + store: IndexedFormula + timeout: number + _fetch: Fetch + mediatypes: MediatypesMap + /** Denoting this session */ + appNode: TFBlankNode /** - * @constructor - */ - constructor (store, options = {}) { + * this.requested[uri] states: + * undefined no record of web access or records reset + * true has been requested, fetch in progress + * 'done' received, Ok + * 401 Not logged in + * 403 HTTP status unauthorized + * 404 Resource does not exist. Can be created etc. + * 'redirected' In attempt to counter CORS problems retried. + * 'parse_error' Parse error + * 'unsupported_protocol' URI is not a protocol Fetcher can deal with + * other strings mean various other errors. + */ + requested: RequestedMap + /** List of timeouts associated with a requested URL */ + timeouts: TimeOutsMap + /** Redirected from *key uri* to *value uri* */ + redirectedTo: Record + fetchQueue: FetchQueue + /** fetchCallbacks[uri].push(callback) */ + fetchCallbacks: FetchCallbacks + /** Keep track of explicit 404s -> we can overwrite etc */ + nonexistent: BooleanMap + lookedUp: BooleanMap + handlers: Array + ns: { [k: string]: (ln: string) => TFPredicate } + static HANDLERS: { + [handlerName: number]: Handler + } + static CONTENT_TYPE_BY_EXT: Record + // TODO: Document this + static crossSiteProxyTemplate: any + + /** Methods added by calling Util.callbackify in the constructor*/ + fireCallbacks!: Function + + constructor (store: IndexedFormula, options: Options = {}) { this.store = store || new IndexedFormula() this.ns = getNS(this.store.rdfFactory) this.timeout = options.timeout || 30000 @@ -453,27 +726,14 @@ export default class Fetcher { throw new Error('No _fetch function availble for Fetcher') } - this.appNode = this.store.bnode() // Denoting this session + this.appNode = this.store.rdfFactory.blankNode() this.store.fetcher = this // Bi-linked this.requested = {} - // this.requested[uri] states: - // undefined no record of web access or records reset - // true has been requested, fetch in progress - // 'done' received, Ok - // 401 Not logged in - // 403 HTTP status unauthorized - // 404 Resource does not exist. Can be created etc. - // 'redirected' In attempt to counter CORS problems retried. - // 'parse_error' Parse error - // 'unsupported_protocol' URI is not a protocol Fetcher can deal with - // other strings mean various other errors. - // - this.timeouts = {} // list of timeouts associated with a requested URL - this.redirectedTo = {} // When 'redirected' + this.timeouts = {} + this.redirectedTo = {} this.fetchQueue = {} - this.fetchCallbacks = {} // fetchCallbacks[uri].push(callback) - - this.nonexistent = {} // keep track of explicit 404s -> we can overwrite etc + this.fetchCallbacks = {} + this.nonexistent = {} this.lookedUp = {} this.handlers = [] this.mediatypes = { @@ -486,10 +746,10 @@ export default class Fetcher { // In switching to fetch(), 'recv', 'headers' and 'load' do not make sense Util.callbackify(this, ['request', 'fail', 'refresh', 'retract', 'done']) - Object.keys(HANDLERS).map(key => this.addHandler(HANDLERS[key])) + Object.keys(options.handlers || defaultHandlers).map(key => this.addHandler(defaultHandlers[key])) } - static crossSiteProxy (uri) { + static crossSiteProxy (uri: string): undefined | any { if (Fetcher.crossSiteProxyTemplate) { return Fetcher.crossSiteProxyTemplate .replace('{uri}', encodeURIComponent(uri)) @@ -498,12 +758,7 @@ export default class Fetcher { } } - /** - * @param uri {string} - * - * @returns {string} - */ - static offlineOverride (uri) { + static offlineOverride (uri: string): string { // Map the URI to a localhost proxy if we are running on localhost // This is used for working offline, e.g. on planes. // Is the script itself is running in localhost, then access all @@ -529,9 +784,14 @@ export default class Fetcher { return requestedURI } - static proxyIfNecessary (uri) { + static proxyIfNecessary (uri: string) { var UI - if (typeof window !== 'undefined' && window.panes && (UI = window.panes.UI) && UI.isExtension) { + if ( + typeof window !== 'undefined' && + (window as any).panes && + (UI = (window as any).panes.UI) && + UI.isExtension + ) { return uri } // Extension does not need proxy @@ -577,24 +837,19 @@ export default class Fetcher { /** * Tests whether the uri's protocol is supported by the Fetcher. - * - * @param uri {string} - * - * @returns {boolean} + * @param uri */ - static unsupportedProtocol (uri) { + static unsupportedProtocol (uri: string): boolean { let pcol = Uri.protocol(uri) return (pcol === 'tel' || pcol === 'mailto' || pcol === 'urn') } /** Decide on credentials using old XXHR api or new fetch() one - * @param requestedURI {string} - * @param options {Object} - * - * @returns {} + * @param requestedURI + * @param options */ - static setCredentials (requestedURI, options = {}) { + static setCredentials (requestedURI: string, options: Options = {}) { // 2014 CORS problem: // XMLHttpRequest cannot load http://www.w3.org/People/Berners-Lee/card. // A wildcard '*' cannot be used in the 'Access-Control-Allow-Origin' @@ -618,7 +873,7 @@ export default class Fetcher { * Loads a web resource or resources into the store. * * A resource may be given as NamedNode object, or as a plain URI. - * an arrsy of resources will be given, in which they will be fetched in parallel. + * an array of resources will be given, in which they will be fetched in parallel. * By default, the HTTP headers are recorded also, in the same store, in a separate graph. * This allows code like editable() for example to test things about the resource. * @@ -657,29 +912,31 @@ export default class Fetcher { * * @returns {Promise} */ - load (uri, options = {}) { + load ( + uri: TFNamedNode | string | Array, + options: Options = {} + ): Promise | Promise[] { options = Object.assign({}, options) // Take a copy as we add stuff to the options!! if (uri instanceof Array) { return Promise.all( + // @ts-ignore Returns an array of promises. Without this ignore, the type is recursive uri.map(x => { return this.load(x, Object.assign({}, options)) }) ) } - let docuri = uri.uri || uri + let docuri = termValue(uri as NamedNode) docuri = docuri.split('#')[0] options = this.initFetchOptions(docuri, options) - return this.pendingFetchPromise(docuri, options.baseURI, options) + return this.pendingFetchPromise(docuri, (options as AutoInitOptions).baseURI, (options as AutoInitOptions)) } - /** - * @param uri {string} - * @param originalUri {string} - * @param options {Object} - * @returns {Promise} - */ - pendingFetchPromise (uri, originalUri, options) { + pendingFetchPromise ( + uri: string, + originalUri: string, + options: AutoInitOptions + ): Promise { let pendingPromise // Check to see if some request is already dealing with this uri @@ -694,7 +951,7 @@ export default class Fetcher { this.fetchQueue[originalUri] = pendingPromise // Clean up the queued promise after a time, if it's resolved - this.cleanupFetchRequest(originalUri, options, this.timeout) + this.cleanupFetchRequest(originalUri, this.timeout) } return pendingPromise.then(x => { @@ -706,21 +963,18 @@ export default class Fetcher { }) } - cleanupFetchRequest (originalUri, options, timeout) { + cleanupFetchRequest (originalUri: string, timeout: number) { this.timeouts[originalUri] = (this.timeouts[originalUri] || []).concat(setTimeout(() => { if (!this.isPending(originalUri)) { delete this.fetchQueue[originalUri] } - }, timeout)) + }, timeout) as unknown as number) } - /** - * @param uri {string} - * @param options {Object} - * - * @returns {Object} - */ - initFetchOptions (uri, options) { + initFetchOptions ( + uri: string, + options: Options + ): AutoInitOptions { let kb = this.store let isGet = !options.method || options.method.toUpperCase() === 'GET' @@ -728,11 +982,11 @@ export default class Fetcher { options.force = true } - options.resource = kb.sym(uri) // This might be proxified + options.resource = kb.rdfFactory.namedNode(uri) // This might be proxified options.baseURI = options.baseURI || uri // Preserve though proxying etc - options.original = kb.sym(options.baseURI) + options.original = kb.rdfFactory.namedNode(options.baseURI) options.req = kb.bnode() - options.headers = options.headers || {} + options.headers = options.headers || new Headers if (options.contentType) { options.headers['content-type'] = options.contentType @@ -756,7 +1010,7 @@ export default class Fetcher { } options.actualProxyURI = actualProxyURI - return options + return options as AutoInitOptions } /** @@ -767,7 +1021,7 @@ export default class Fetcher { * * @returns {Promise} fetch() result or an { error, status } object */ - fetchUri (docuri, options) { + fetchUri (docuri: string, options: AutoInitOptions): Promise { if (!docuri) { return Promise.reject(new Error('Cannot fetch an empty uri')) } @@ -781,17 +1035,24 @@ export default class Fetcher { if (!options.force) { if (state === 'fetched') { // URI already fetched and added to store return Promise.resolve( - this.doneFetch(options, {status: 200, ok: true, statusText: 'Already loaded into quadstore.'}) + // @ts-ignore This is not a valid response object + this.doneFetch(options, { + status: 200, + ok: true, + statusText: 'Already loaded into quadstore.' + }) ) } if (state === 'failed' && this.requested[docuri] === 404) { // Remember nonexistence let message = 'Previously failed: ' + this.requested[docuri] - let dummyResponse = { + // @ts-ignore This is not a valid response object + let dummyResponse: ExtendedResponse = { url: docuri, - status: this.requested[docuri], + // This does not comply to Fetch spec, it can be a string value in rdflib + status: this.requested[docuri] as number, statusText: message, responseText: message, - headers: {}, // Headers() ??? + headers: new Headers, // Headers() ??? ok: false, body: null, bodyUsed: false, @@ -816,31 +1077,32 @@ export default class Fetcher { let { actualProxyURI } = options - return this._fetch(actualProxyURI, options) + return this._fetch((actualProxyURI as string), options) .then(response => this.handleResponse(response, docuri, options), - error => { // @@ handleError? - let dummyResponse = { - url: actualProxyURI, - status: 999, // @@ what number/string should fetch failures report? - statusText: (error.name || 'network failure') + ': ' + - (error.errno || error.code || error.type), - responseText: error.message, - headers: {}, // Headers() ??? - ok: false, - body: null, - bodyUsed: false, - size: 0, - timeout: 0 - } - console.log('Fetcher: <' + actualProxyURI + '> Non-HTTP fetch exception: ' + error) - return this.handleError(dummyResponse, docuri, options) // possible credentials retry - // return this.failFetch(options, 'fetch failed: ' + error, 999, dummyResponse) // Fake status code: fetch exception - - // handleError expects a response so we fake some important bits. - /* - this.handleError(, docuri, options) - */ - } + error => { // @@ handleError? + // @ts-ignore Invalid response object + let dummyResponse: ExtendedResponse = { + url: actualProxyURI as string, + status: 999, // @@ what number/string should fetch failures report? + statusText: (error.name || 'network failure') + ': ' + + (error.errno || error.code || error.type), + responseText: error.message, + headers: new Headers(), // Headers() ??? + ok: false, + body: null, + bodyUsed: false, + size: 0, + timeout: 0 + } + console.log('Fetcher: <' + actualProxyURI + '> Non-HTTP fetch exception: ' + error) + return this.handleError(dummyResponse, docuri, options) // possible credentials retry + // return this.failFetch(options, 'fetch failed: ' + error, 999, dummyResponse) // Fake status code: fetch exception + + // handleError expects a response so we fake some important bits. + /* + this.handleError(, docuri, options) + */ + } ) } @@ -871,15 +1133,20 @@ export default class Fetcher { * response The fetch Response object (was: XHR) if there was was one * includes response.status as the HTTP status if any. */ - nowOrWhenFetched (uri, p2, userCallback, options = {}) { - uri = uri.uri || uri // allow symbol object or string to be passed + nowOrWhenFetched ( + uriIn: string | TFNamedNode, + p2?: UserCallback | Options, + userCallback?: UserCallback, + options: Options = {} + ): void { + const uri = termValue(uriIn) if (typeof p2 === 'function') { // nowOrWhenFetched (uri, userCallback) userCallback = p2 } else if (typeof p2 === 'undefined') { // original calling signature // referringTerm = undefined - } else if (isNamedNode(p2)) { + } else if (isTFNamedNode(p2)) { // referringTerm = p2 options.referringTerm = p2 } else { @@ -887,11 +1154,11 @@ export default class Fetcher { options = p2 } - this.load(uri, options) - .then(fetchResponse => { + (this.load(uri, options) as Promise) + .then((fetchResponse: ExtendedResponse) => { if (userCallback) { if (fetchResponse) { - if (fetchResponse.ok) { + if ((fetchResponse as Response).ok) { userCallback(true, 'OK', fetchResponse) } else { // console.log('@@@ fetcher.js Should not take this path !!!!!!!!!!!!') @@ -908,14 +1175,14 @@ export default class Fetcher { userCallback(false, oops) } } - }, function (err) { + }, function (err: FetchError) { var message = err.message || err.statusText message = 'Failed to load <' + uri + '> ' + message console.log(message) if (err.response && err.response.status) { message += ' status: ' + err.response.status } - userCallback(false, message, err.response) + (userCallback as any)(false, message, err.response) }) } @@ -923,10 +1190,8 @@ export default class Fetcher { * Records a status message (as a literal node) by appending it to the * request's metadata status collection. * - * @param req {BlankNode} - * @param statusMessage {string} */ - addStatus (req, statusMessage) { + addStatus (req: TFBlankNode, statusMessage: string) { // let now = new Date() statusMessage = '[' + now.getHours() + ':' + now.getMinutes() + ':' + @@ -934,9 +1199,9 @@ export default class Fetcher { // let kb = this.store - let statusNode = kb.the(req, this.ns.link('status')) - if (statusNode && statusNode.append) { - statusNode.append(kb.literal(statusMessage)) + const statusNode = kb.the(req, this.ns.link('status')) + if (isCollection(statusNode)) { + statusNode.append(kb.rdfFactory.literal(statusMessage)) } else { log.warn('web.js: No list to add to: ' + statusNode + ',' + statusMessage) } @@ -949,35 +1214,40 @@ export default class Fetcher { * - Adds an error triple with the fail message to the metadata * - Fires the 'fail' callback * - Rejects with an error result object, which has a response object if any - * - * @param options {Object} - * @param errorMessage {string} - * @param statusCode {number} - * @param response {Response} // when an fetch() error - * - * @returns {Promise} */ - failFetch (options, errorMessage, statusCode, response) { + failFetch ( + options: { + req: TFBlankNode + original: TFSubject + } & Options, + errorMessage: string, + statusCode: StatusValues, + response?: ExtendedResponse + ): Promise { this.addStatus(options.req, errorMessage) if (!options.noMeta) { - this.store.add(options.original, this.ns.link('error'), errorMessage) + this.store.add( + options.original, + this.ns.link('error'), + this.store.rdfFactory.literal(errorMessage) + ) } let meth = (options.method || 'GET').toUpperCase() let isGet = meth === 'GET' || meth === 'HEAD' if (isGet) { // only cache the status code on GET or HEAD - if (!options.resource.equals(options.original)) { + if (!(options as any).resource.equals(options.original)) { // console.log('@@ Recording failure ' + meth + ' original ' + options.original +option '( as ' + options.resource + ') : ' + statusCode) } else { // console.log('@@ Recording ' + meth + ' failure for ' + options.original + ': ' + statusCode) } - this.requested[Uri.docpart(options.original.uri)] = statusCode - this.fireCallbacks('fail', [options.original.uri, errorMessage]) + this.requested[Uri.docpart(options.original.value)] = statusCode + this.fireCallbacks('fail', [options.original.value, errorMessage]) } - var err = new Error('Fetcher: ' + errorMessage) + var err: FetchError = new Error('Fetcher: ' + errorMessage) // err.ok = false // Is taken as a response, will work too @@ phase out? err.status = statusCode @@ -989,24 +1259,30 @@ export default class Fetcher { // in the why part of the quad distinguish between HTML and HTTP header // Reverse is set iif the link was rev= as opposed to rel= - linkData (originalUri, rel, uri, why, reverse) { + linkData ( + originalUri: TFNamedNode, + rel: string, + uri: string, + why: TFGraph, + reverse?: boolean + ) { if (!uri) return let kb = this.store let predicate // See http://www.w3.org/TR/powder-dr/#httplink for describedby 2008-12-10 - let obj = kb.sym(Uri.join(uri, originalUri.uri)) + let obj = kb.rdfFactory.namedNode(Uri.join(uri, originalUri.value)) if (rel === 'alternate' || rel === 'seeAlso' || rel === 'meta' || rel === 'describedby') { - if (obj.uri === originalUri.uri) { return } + if (obj.value === originalUri.value) { return } predicate = this.ns.rdfs('seeAlso') } else if (rel === 'type') { - predicate = kb.sym('http://www.w3.org/1999/02/22-rdf-syntax-ns#type') + predicate = kb.rdfFactory.namedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#type') } else { // See https://www.iana.org/assignments/link-relations/link-relations.xml // Alas not yet in RDF yet for each predicate // encode space in e.g. rel="shortcut icon" - predicate = kb.sym( + predicate = kb.rdfFactory.namedNode( Uri.join(encodeURIComponent(rel), 'http://www.iana.org/assignments/link-relations/') ) @@ -1018,7 +1294,11 @@ export default class Fetcher { } } - parseLinkHeader (linkHeader, originalUri, reqNode) { + parseLinkHeader ( + linkHeader: string, + originalUri: TFNamedNode, + reqNode: TFGraph + ): void { if (!linkHeader) { return } // const linkexp = /<[^>]*>\s*(\s*;\s*[^()<>@,;:"/[\]?={} \t]+=(([^()<>@,;:"/[]?={} \t]+)|("[^"]*")))*(,|$)/g @@ -1033,11 +1313,14 @@ export default class Fetcher { const matches = linkHeader.match(linkexp) + if (matches == null) return; + for (let i = 0; i < matches.length; i++) { let split = matches[i].split('>') let href = split[0].substring(1) let ps = split[1] let s = ps.match(paramexp) + if (s == null) return for (let j = 0; j < s.length; j++) { let p = s[j] let paramsplit = p.split('=') @@ -1048,11 +1331,17 @@ export default class Fetcher { } } - doneFetch (options, response) { + doneFetch ( + options: { + req: TFSubject, + original: TFSubject + } & Options, + response: ExtendedResponse + ): Response { this.addStatus(options.req, 'Done.') - this.requested[options.original.uri] = 'done' + this.requested[options.original.value] = 'done' - this.fireCallbacks('done', [options.original.uri]) + this.fireCallbacks('done', [options.original.value]) response.req = options.req // Set the request meta blank node @@ -1064,14 +1353,14 @@ export default class Fetcher { * If only one was flagged as looked up, then the new node is looked up again, * which will make sure all the URIs are dereferenced */ - nowKnownAs (was, now) { - if (this.lookedUp[was.uri]) { + nowKnownAs (was: TFSubject, now: TFSubject): void { + if (this.lookedUp[was.value]) { // Transfer userCallback - if (!this.lookedUp[now.uri]) { + if (!this.lookedUp[now.value]) { this.lookUpThing(now, was) } - } else if (this.lookedUp[now.uri]) { - if (!this.lookedUp[was.uri]) { + } else if (this.lookedUp[now.value]) { + if (!this.lookedUp[was.value]) { this.lookUpThing(was, now) } } @@ -1079,21 +1368,19 @@ export default class Fetcher { /** * Writes back to the web what we have in the store for this uri - * - * @param uri {Node|string} - * @param [options={}] - * - * @returns {Promise} */ - putBack (uri, options = {}) { - uri = uri.uri || uri // Accept object or string + putBack ( + uri: TFNamedNode | string, + options: Options = {} + ): Promise { + uri = (uri as TFNamedNode).value || uri // Accept object or string let doc = new NamedNode(uri).doc() // strip off # options.contentType = options.contentType || 'text/turtle' - options.data = serialize(doc, this.store, doc.uri, options.contentType) + options.data = serialize(doc, this.store, doc.value, options.contentType) as string return this.webOperation('PUT', uri, options) } - webCopy (here, there, contentType) { + webCopy (here: string, there: string, contentType): Promise { return this.webOperation('GET', here) .then((result) => { return this.webOperation( @@ -1102,18 +1389,12 @@ export default class Fetcher { }) } - /** - * @param uri {string} - * @param [options] {Object} - * - * @returns {Promise} - */ - delete (uri, options) { + delete (uri: string, options: Options): Promise { return this.webOperation('DELETE', uri, options) .then(response => { this.requested[uri] = 404 this.nonexistent[uri] = true - this.unload(this.store.sym(uri)) + this.unload(this.store.rdfFactory.namedNode(uri)) return response }) @@ -1122,23 +1403,26 @@ export default class Fetcher { /** Create an empty resource if it really does not exist * Be absolutely sure something does not exist before creating a new empty file * as otherwise existing could be deleted. - * @param doc {NamedNode} - The resource - * @returns {Promise} + * @param doc - The resource */ - async createIfNotExists (doc, contentType = 'text/turtle', data = '') { + async createIfNotExists ( + doc: NamedNode, + contentType = 'text/turtle', + data = '' + ): Promise { const fetcher = this try { - var response = await fetcher.load(doc) + var response = await fetcher.load(doc as TFNamedNode) } catch (err) { if (err.response.status === 404) { console.log('createIfNotExists: doc does NOT exist, will create... ' + doc) try { - response = await fetcher.webOperation('PUT', doc.uri, {data, contentType}) + response = await fetcher.webOperation('PUT', doc.value, {data, contentType}) } catch (err) { console.log('createIfNotExists doc FAILED: ' + doc + ': ' + err) throw err } - delete fetcher.requested[doc.uri] // delete cached 404 error + delete fetcher.requested[doc.value] // delete cached 404 error // console.log('createIfNotExists doc created ok ' + doc) return response } else { @@ -1147,20 +1431,22 @@ export default class Fetcher { } } // console.log('createIfNotExists: doc exists, all good: ' + doc) - return response + return response as Response } /** - * @param parentURI {string} URI of parent container - * @param [folderName] {string} Optional folder name (slug) - * @param [data] {string} Optional folder metadata - * - * @returns {Promise} + * @param parentURI URI of parent container + * @param folderName - Optional folder name (slug) + * @param data - Optional folder metadata */ - createContainer (parentURI, folderName, data) { + createContainer ( + parentURI: string, + folderName: string, + data: string + ): Promise { let headers = { // Force the right mime type for containers - 'content-type': 'text/turtle', + 'content-type': ContentType.turtle, 'link': this.ns.ldp('BasicContainer') + '; rel="type"' } @@ -1168,7 +1454,8 @@ export default class Fetcher { headers['slug'] = folderName } - let options = { headers } + // @ts-ignore These headers lack some of the required operators. + let options: Options = { headers } if (data) { options.body = data @@ -1177,13 +1464,13 @@ export default class Fetcher { return this.webOperation('POST', parentURI, options) } - invalidateCache (uri) { - uri = uri.uri || uri // Allow a NamedNode to be passed as it is very common + invalidateCache (iri: string | TFNamedNode): void { + const uri = termValue(iri) const fetcher = this if (fetcher.fetchQueue && fetcher.fetchQueue[uri]) { console.log('Internal error - fetchQueue exists ' + uri) var promise = fetcher.fetchQueue[uri] - if (promise.PromiseStatus === 'resolved') { + if (promise['PromiseStatus'] === 'resolved') { delete fetcher.fetchQueue[uri] } else { // pending delete fetcher.fetchQueue[uri] @@ -1199,21 +1486,21 @@ export default class Fetcher { delete fetcher.nonexistent[uri] } } + /** * A generic web opeation, at the fetch() level. * does not invole the quadstore. * * Returns promise of Response * If data is returned, copies it to response.responseText before returning - * - * @param method - * @param uri or NamedNode - * @param options - * - * @returns {Promise} */ - webOperation (method, uri, options = {}) { - uri = uri.uri || uri // Allow a NamedNode to be passed as it is very common + webOperation ( + method: HTTPMethods, + uriIn: string | TFNamedNode, + // Not sure about this type. Maybe this Options is different? + options: Options = {} + ): Promise { + const uri = termValue(uriIn) options.method = method options.body = options.data || options.body options.force = true @@ -1223,8 +1510,8 @@ export default class Fetcher { throw new Error('Web operation sending data must have a defined contentType.') } if (options.contentType) { - options.headers = options.headers || {} - options.headers['content-type'] = options.contentType + (options as any).headers = options.headers || {}; + (options as any).headers['content-type'] = options.contentType } Fetcher.setCredentials(uri, options) @@ -1247,11 +1534,11 @@ export default class Fetcher { if (response.statusText) msg += ' (' + response.statusText + ')' msg += ' on ' + method + ' of <' + uri + '>' if (response.responseText) msg += ': ' + response.responseText - let e2 = new Error(msg) + let e2: FetchError = new Error(msg) e2.response = response reject(e2) } - }, err => { + }, (err: Error) => { let msg = 'Fetch error for ' + method + ' of <' + uri + '>:' + err reject(new Error(msg)) }) @@ -1262,14 +1549,15 @@ export default class Fetcher { * Looks up something. * Looks up all the URIs a things has. * - * @param term {NamedNode} canonical term for the thing whose URI is + * @param term - canonical term for the thing whose URI is * to be dereferenced - * @param rterm {NamedNode} the resource which referred to this + * @param rterm - the resource which referred to this * (for tracking bad links) - * - * @returns {Promise} */ - lookUpThing (term, rterm) { + lookUpThing ( + term: TFSubject, + rterm: TFSubject + ): Promise | Promise[] { let uris = this.store.uris(term) // Get all URIs uris = uris.map(u => Uri.docpart(u)) // Drop hash fragments @@ -1277,30 +1565,30 @@ export default class Fetcher { this.lookedUp[u] = true }) + // @ts-ignore Recursive type return this.load(uris, { referringTerm: rterm }) } /** * Looks up response header. * - * @param doc - * @param header - * * @returns {Array|undefined} a list of header values found in a stored HTTP * response, or [] if response was found but no header found, * or undefined if no response is available. * Looks for { [] link:requestedURI ?uri; link:response [ httph:header-name ?value ] } */ - getHeader (doc, header) { + getHeader ( + doc: TFNamedNode, + header: string + ): undefined | string[] { const kb = this.store - const requests = kb.each(undefined, this.ns.link('requestedURI'), doc.uri) + const requests = kb.each(undefined, this.ns.link('requestedURI'), doc) as TFSubject[] for (let r = 0; r < requests.length; r++) { let request = requests[r] if (request !== undefined) { - let response = kb.any(request, this.ns.link('response')) - - if (response !== undefined && kb.anyValue(response, this.ns.http('status')) && kb.anyValue(response, this.ns.http('status')).startsWith('2')) { + let response = kb.any(request, this.ns.link('response')) as TFSubject + if (response !== undefined && kb.anyValue(response, this.ns.http('status')) && (kb.anyValue(response, this.ns.http('status')) as string).startsWith('2')) { // Only look at success returns - not 401 error messagess etc let results = kb.each(response, this.ns.httph(header.toLowerCase())) @@ -1315,24 +1603,22 @@ export default class Fetcher { return undefined } - /** - * - * @param docuri - * @param options - */ - saveRequestMetadata (docuri, options) { + saveRequestMetadata ( + docuri: string, + options: AutoInitOptions + ) { let req = options.req let kb = this.store let rterm = options.referringTerm this.addStatus(options.req, 'Accept: ' + options.headers['accept']) - if (rterm && rterm.uri) { - kb.add(docuri, this.ns.link('requestedBy'), rterm.uri, this.appNode) + if (isTFNamedNode(rterm)) { + kb.add(kb.rdfFactory.namedNode(docuri), this.ns.link('requestedBy'), rterm, this.appNode) } - if (options.original && options.original.uri !== docuri) { - kb.add(req, this.ns.link('orginalURI'), kb.literal(options.original.uri), + if (options.original && options.original.value !== docuri) { + kb.add(req, this.ns.link('orginalURI'), kb.rdfFactory.literal(options.original.value), this.appNode) } @@ -1341,35 +1627,41 @@ export default class Fetcher { now.getSeconds() + '] ' kb.add(req, this.ns.rdfs('label'), - kb.literal(timeNow + ' Request for ' + docuri), this.appNode) - kb.add(req, this.ns.link('requestedURI'), kb.literal(docuri), this.appNode) + kb.rdfFactory.literal(timeNow + ' Request for ' + docuri), this.appNode) + kb.add(req, this.ns.link('requestedURI'), kb.rdfFactory.literal(docuri), this.appNode) kb.add(req, this.ns.link('status'), kb.collection(), this.appNode) } - saveResponseMetadata (response, options) { + saveResponseMetadata ( + response: Response, + options: { + req: TFBlankNode, + resource: TFSubject + } & Options + ): TFBlankNode { const kb = this.store let responseNode = kb.bnode() kb.add(options.req, this.ns.link('response'), responseNode, responseNode) kb.add(responseNode, this.ns.http('status'), - kb.literal(response.status), responseNode) + kb.rdfFactory.literal(response.status as any), responseNode) kb.add(responseNode, this.ns.http('statusText'), - kb.literal(response.statusText), responseNode) + kb.rdfFactory.literal(response.statusText), responseNode) - if (!options.resource.uri.startsWith('http')) { + if (!options.resource.value.startsWith('http')) { return responseNode } // Save the response headers response.headers.forEach((value, header) => { - kb.add(responseNode, this.ns.httph(header), value, responseNode) + kb.add(responseNode, this.ns.httph(header), this.store.rdfFactory.literal(value), responseNode) if (header === 'content-type') { kb.add( options.resource, this.ns.rdf('type'), - kb.namedNode(Util.mediaTypeClass(value).value), + kb.rdfFactory.namedNode(Util.mediaTypeClass(value).value), responseNode ) } @@ -1378,11 +1670,11 @@ export default class Fetcher { return responseNode } - objectRefresh (term) { + objectRefresh (term: TFNamedNode): void { let uris = this.store.uris(term) // Get all URIs if (typeof uris !== 'undefined') { for (let i = 0; i < uris.length; i++) { - this.refresh(this.store.sym(Uri.docpart(uris[i]))) + this.refresh(this.store.rdfFactory.namedNode(Uri.docpart(uris[i]))) // what about rterm? } } @@ -1390,10 +1682,13 @@ export default class Fetcher { /* refresh Reload data from a given document ** - ** @param {NamedNode} term - An RDF Named Node for the eodcument in question - ** @param {function } userCallback - A function userCallback(ok, message, response) + ** @param term - An RDF Named Node for the eodcument in question + ** @param userCallback - A function userCallback(ok, message, response) */ - refresh (term, userCallback) { // sources_refresh + refresh ( + term: TFNamedNode, + userCallback?: UserCallback + ): void { // sources_refresh this.fireCallbacks('refresh', arguments) this.nowOrWhenFetched(term, { force: true, clearPreviousData: true }, @@ -1402,27 +1697,30 @@ export default class Fetcher { /* refreshIfExpired Conditional refresh if Expired ** - ** @param {NamedNode} term - An RDF Named Node for the eodcument in question - ** @param {function } userCallback - A function userCallback(ok, message, response) + ** @param term - An RDF Named Node for the eodcument in question + ** @param userCallback - A function userCallback(ok, message, response) */ - refreshIfExpired (term, userCallback) { + refreshIfExpired ( + term: TFNamedNode, + userCallback: UserCallback + ): void { let exp = this.getHeader(term, 'Expires') - if (!exp || (new Date(exp).getTime()) <= (new Date().getTime())) { + if (!exp || (new Date(exp[0]).getTime()) <= (new Date().getTime())) { this.refresh(term, userCallback) } else { userCallback(true, 'Not expired', {}) } } - retract (term) { // sources_retract + retract (term: TFGraph) { // sources_retract this.store.removeMany(undefined, undefined, undefined, term) - if (term.uri) { - delete this.requested[Uri.docpart(term.uri)] + if (term.value) { + delete this.requested[Uri.docpart(term.value)] } this.fireCallbacks('retract', arguments) } - getState (docuri) { + getState (docuri: string) { if (typeof this.requested[docuri] === 'undefined') { return 'unrequested' } else if (this.requested[docuri] === true) { @@ -1436,24 +1734,27 @@ export default class Fetcher { } } - isPending (docuri) { // sources_pending + isPending (docuri: string) { // sources_pending // doing anyStatementMatching is wasting time // if it's not pending: false -> flailed // 'done' -> done 'redirected' -> redirected return this.requested[docuri] === true } - unload (term) { + unload (term: TFNamedNode) { this.store.removeDocument(term) - delete this.requested[term.uri] // So it can be loaded again + delete this.requested[term.value] // So it can be load2ed again } - addHandler (handler) { - this.handlers.push(handler) - handler.register(this) + addHandler (handler: typeof Handler) { + this.handlers.push(handler); + (handler as any).register(this) } - retryNoCredentials (docuri, options) { + retryNoCredentials ( + docuri: string, + options + ): Promise { console.log('Fetcher: CORS: RETRYING with NO CREDENTIALS for ' + options.resource) options.retriedWithNoCredentials = true // protect against being called twice @@ -1466,18 +1767,14 @@ export default class Fetcher { this.addStatus(options.req, 'Abort: Will retry with credentials SUPPRESSED to see if that helps') - return this.load(docuri, newOptions) + return this.load(docuri, newOptions) as Promise } /** * Tests whether a request is being made to a cross-site URI (for purposes * of retrying with a proxy) - * - * @param uri {string} - * - * @returns {boolean} */ - isCrossSite (uri) { + isCrossSite (uri: string): boolean { // Mashup situation, not node etc if (typeof document === 'undefined' || !document.location) { return false @@ -1485,20 +1782,18 @@ export default class Fetcher { const hostpart = Uri.hostpart const here = '' + document.location - return hostpart(here) && hostpart(uri) && hostpart(here) !== hostpart(uri) + return (hostpart(here) && hostpart(uri) && hostpart(here)) !== hostpart(uri) } /** * Called when there's a network error in fetch(), or a response * with status of 0. - * - * @param response {Response|Error} - * @param docuri {string} - * @param options {Object} - * - * @returns {Promise} */ - handleError (response, docuri, options) { + handleError ( + response: ExtendedResponse | Error, + docuri: string, + options: AutoInitOptions + ): Promise { if (this.isCrossSite(docuri)) { // Make sure we haven't retried already if (options.credentials && options.credentials === 'include' && !options.retriedWithNoCredentials) { @@ -1516,7 +1811,7 @@ export default class Fetcher { } var message - if (response.message) { + if (response instanceof Error) { message = 'Fetch error: ' + response.message } else { message = response.statusText @@ -1526,43 +1821,50 @@ export default class Fetcher { } // This is either not a CORS error, or retries have been made - return this.failFetch(options, message, response.status || 998, response) + return this.failFetch(options, message, (response as Response).status || 998, (response as Response)) } // deduce some things from the HTTP transaction - addType (rdfType, req, kb, locURI) { // add type to all redirected resources too + addType ( + rdfType: TFNamedNode, + req: TFSubject, + kb: IndexedFormula, + locURI: string + ): void { // add type to all redirected resources too let prev = req if (locURI) { var reqURI = kb.any(prev, this.ns.link('requestedURI')) - if (reqURI && reqURI !== locURI) { - kb.add(kb.sym(locURI), this.ns.rdf('type'), rdfType, this.appNode) + if (reqURI && reqURI.value !== locURI) { + kb.add(kb.rdfFactory.namedNode(locURI), this.ns.rdf('type'), rdfType, this.appNode) } } for (;;) { const doc = kb.any(prev, this.ns.link('requestedURI')) if (doc && doc.value) { - kb.add(kb.sym(doc.value), this.ns.rdf('type'), rdfType, this.appNode) + kb.add(kb.rdfFactory.namedNode(doc.value), this.ns.rdf('type'), rdfType, this.appNode) } // convert Literal - prev = kb.any(undefined, kb.sym('http://www.w3.org/2007/ont/link#redirectedRequest'), prev) + prev = kb.any(undefined, kb.rdfFactory.namedNode('http://www.w3.org/2007/ont/link#redirectedRequest'), prev) as TFSubject if (!prev) { break } - var response = kb.any(prev, kb.sym('http://www.w3.org/2007/ont/link#response')) + var response = kb.any(prev, kb.rdfFactory.namedNode('http://www.w3.org/2007/ont/link#response')) if (!response) { break } - var redirection = kb.any(response, kb.sym('http://www.w3.org/2007/ont/http#status')) + var redirection = kb.any((response as TFNamedNode), kb.rdfFactory.namedNode('http://www.w3.org/2007/ont/http#status')) if (!redirection) { break } - if (redirection !== '301' && redirection !== '302') { break } + // @ts-ignore always true? + if ((redirection !== '301') && (redirection !== '302')) { break } } } /** * Handle fetch() response - * - * @param response {Response} fetch() response object - * @param docuri {string} - * @param options {Object} */ - handleResponse (response, docuri, options) { + handleResponse ( + response: ExtendedResponse, + docuri: string, + options: AutoInitOptions + ): Promise | ExtendedResponse { + const kb = this.store - const headers = response.headers + const headers: Headers = (response as Response).headers const reqNode = options.req @@ -1583,7 +1885,7 @@ export default class Fetcher { if (response.status >= 400) { if (response.status === 404) { - this.nonexistent[options.original.uri] = true + this.nonexistent[options.original.value] = true this.nonexistent[docuri] = true } @@ -1595,8 +1897,8 @@ export default class Fetcher { }) } - var diffLocation = null - var absContentLocation = null + var diffLocation: null | string = null + var absContentLocation: null | string = null if (contentLocation) { absContentLocation = Uri.join(contentLocation, docuri) if (absContentLocation !== docuri) { @@ -1604,9 +1906,9 @@ export default class Fetcher { } } if (response.status === 200) { - this.addType(this.ns.link('Document'), reqNode, kb, docuri) + this.addType(this.ns.link('Document') as TFNamedNode, reqNode, kb, docuri) if (diffLocation) { - this.addType(this.ns.link('Document'), reqNode, kb, + this.addType(this.ns.link('Document') as TFNamedNode, reqNode, kb, diffLocation) } @@ -1619,10 +1921,10 @@ export default class Fetcher { contentType.includes('application/pdf') if (contentType && isImage) { - this.addType(kb.sym('http://purl.org/dc/terms/Image'), reqNode, kb, + this.addType(kb.rdfFactory.namedNode('http://purl.org/dc/terms/Image'), reqNode, kb, docuri) if (diffLocation) { - this.addType(kb.sym('http://purl.org/dc/terms/Image'), reqNode, kb, + this.addType(kb.rdfFactory.namedNode('http://purl.org/dc/terms/Image'), reqNode, kb, diffLocation) } } @@ -1630,7 +1932,7 @@ export default class Fetcher { // If we have already got the thing at this location, abort if (contentLocation) { - if (!options.force && diffLocation && this.requested[absContentLocation] === 'done') { + if (!options.force && diffLocation && this.requested[absContentLocation as string] === 'done') { // we have already fetched this // should we smush too? // log.info("HTTP headers indicate we have already" + " retrieved " + @@ -1638,12 +1940,12 @@ export default class Fetcher { return this.doneFetch(options, response) } - this.requested[absContentLocation] = true + this.requested[absContentLocation as string] = true } - this.parseLinkHeader(headers.get('link'), options.original, reqNode) + this.parseLinkHeader(headers.get('link') as string, options.original, reqNode) - let handler = this.handlerForContentType(contentType, response) + let handler = this.handlerForContentType(contentType, response) as Handler if (!handler) { // Not a problem, we just don't extract data @@ -1651,30 +1953,30 @@ export default class Fetcher { return this.doneFetch(options, response) } - return response.text() + return response + .text() + // @ts-ignore Types seem right .then(responseText => { response.responseText = responseText - return handler.parse(this, responseText, options, response) + return (handler as N3Handler).parse(this, responseText, options, response) }) } - saveErrorResponse (response, responseNode) { + saveErrorResponse ( + response: ExtendedResponse, + responseNode: TFSubject + ): Promise { let kb = this.store return response.text() .then(content => { if (content.length > 10) { - kb.add(responseNode, this.ns.http('content'), kb.literal(content), responseNode) + kb.add(responseNode, this.ns.http('content'), kb.rdfFactory.literal(content), responseNode) } }) } - /** - * @param contentType {string} - * - * @returns {Handler|null} - */ - handlerForContentType (contentType, response) { + handlerForContentType (contentType: string, response: ExtendedResponse): Handler | null { if (!contentType) { return null } @@ -1683,39 +1985,32 @@ export default class Fetcher { return contentType.match(handler.pattern) }) + // @ts-ignore in practice all Handlers have constructors. return Handler ? new Handler(response) : null } - /** - * @param uri {string} - * - * @returns {string} - */ - guessContentType (uri) { - return CONTENT_TYPE_BY_EXT[uri.split('.').pop()] + guessContentType (uri: string): ContentType | undefined { + return CONTENT_TYPE_BY_EXT[uri.split('.').pop() as string] } - /** - * @param options {Object} - * @param headers {Headers} - * - * @returns {string} - */ - normalizedContentType (options, headers) { + normalizedContentType ( + options: AutoInitOptions, + headers: Headers + ): ContentType | string | null { if (options.forceContentType) { return options.forceContentType } let contentType = headers.get('content-type') if (!contentType || contentType.includes('application/octet-stream')) { - let guess = this.guessContentType(options.resource.uri) + let guess = this.guessContentType(options.resource.value) if (guess) { return guess } } - let protocol = Uri.protocol(options.resource.uri) + let protocol = Uri.protocol(options.resource.value) as string if (!contentType && ['file', 'chrome'].includes(protocol)) { return 'text/xml' @@ -1726,13 +2021,11 @@ export default class Fetcher { /** * Sends a new request to the specified uri. (Extracted from `onerrorFactory()`) - * - * @param newURI {string} - * @param options {Object} - * - * @returns {Promise} */ - redirectToProxy (newURI, options) { + redirectToProxy ( + newURI: string, + options: AutoInitOptions + ): Promise { this.addStatus(options.req, 'BLOCKED -> Cross-site Proxy to <' + newURI + '>') options.proxyUsed = true @@ -1741,15 +2034,15 @@ export default class Fetcher { const oldReq = options.req // request metadata blank node if (!options.noMeta) { - kb.add(oldReq, this.ns.link('redirectedTo'), kb.sym(newURI), oldReq) + kb.add(oldReq, this.ns.link('redirectedTo'), kb.rdfFactory.namedNode(newURI), oldReq) this.addStatus(oldReq, 'redirected to new request') // why } - this.requested[options.resource.uri] = 'redirected' - this.redirectedTo[options.resource.uri] = newURI + this.requested[options.resource.value] = 'redirected' + this.redirectedTo[options.resource.value] = newURI let newOptions = Object.assign({}, options) - newOptions.baseURI = options.resource.uri + newOptions.baseURI = options.resource.value return this.fetchUri(newURI, newOptions) .then(response => { @@ -1761,7 +2054,13 @@ export default class Fetcher { }) } - setRequestTimeout (uri, options) { + setRequestTimeout ( + uri: string, + options: { + req: TFSubject + original: TFSubject + } & Options + ): Promise { return new Promise((resolve) => { this.timeouts[uri] = (this.timeouts[uri] || []).concat(setTimeout(() => { if (this.isPending(uri) && @@ -1769,11 +2068,14 @@ export default class Fetcher { !options.proxyUsed) { resolve(this.failFetch(options, `Request to ${uri} timed out`, 'timeout')) } - }, this.timeout)) + }, this.timeout) as unknown as number) }) } - addFetchCallback (uri, callback) { + addFetchCallback ( + uri: string, + callback: UserCallback + ): void { if (!this.fetchCallbacks[uri]) { this.fetchCallbacks[uri] = [callback] } else { @@ -1803,5 +2105,5 @@ export default class Fetcher { // whether we want to track it ot not. including ontologies loaed though the XSSproxy } -Fetcher.HANDLERS = HANDLERS +Fetcher.HANDLERS = defaultHandlers Fetcher.CONTENT_TYPE_BY_EXT = CONTENT_TYPE_BY_EXT diff --git a/src/formula.ts b/src/formula.ts index 859038cb4..573a36a02 100644 --- a/src/formula.ts +++ b/src/formula.ts @@ -1,4 +1,3 @@ -'use strict' import BlankNode from './blank-node' import ClassOrder from './class-order' import Collection from './collection' @@ -6,18 +5,50 @@ import CanonicalDataFactory from './data-factory-internal' import log from './log' import NamedNode from './named-node' import Namespace from './namespace' -import Node from './node' +import Node from './node-internal' import Serializer from './serialize' import Statement from './statement' -import { TermType } from "./types"; -import { appliedFactoryMethods, arrayToStatements } from './util' +import { + Bindings, + TermType, + TFBlankNode, + TFGraph, + TFObject, + TFPredicate, + TFQuad, + TFSubject, + TFTerm +} from './types' import { isStatement } from './utils/terms' import Variable from './variable' - -/** @module formula */ +import { + DataFactory, + IdentityFactory, + Indexable, + TFIDFactoryTypes +} from './data-factory-type' +import { appliedFactoryMethods, arrayToStatements } from './utils' + +export function isFormula(value: T | TFTerm): value is Formula { + return (value as Node).termType === TermType.Graph +} export interface FormulaOpts { - rdfFactory?: any + dataCallback?: (q: TFQuad) => void + rdfArrayRemove?: (arr: TFQuad[], q: TFQuad) => void + rdfFactory?: IdentityFactory & DataFactory +} + +interface BooleanMap { + [uri: string]: boolean; +} + +interface MembersMap { + [uri: string]: TFQuad; +} + +interface UriMap { + [uri: string]: string; } /** @@ -27,22 +58,35 @@ export default class Formula extends Node { static termType = TermType.Graph classOrder = ClassOrder.Graph + /** The additional constraints */ constraints: ReadonlyArray; + /** * The accompanying fetcher instance. * * Is set by the fetcher when initialized. */ fetcher?: any - initBindings: { [id: string]: Node; } + + initBindings: ReadonlyArray + isVar = 0 + + /** + * A namespace for the specified namespace's URI + * @param nsuri The URI for the namespace + */ ns = Namespace - optional: any[] + + optional: ReadonlyArray + /** The factory used to generate statements and terms */ rdfFactory: any + /** The stored statements */ - statements: Statement[]; + statements: TFQuad[]; + termType = TermType.Graph /** @@ -53,9 +97,15 @@ export default class Formula extends Node { * @param initBindings - initial bindings used in Query * @param optional - optional * @param opts - * @param {DataFactory} opts.rdfFactory - The rdf factory that should be used by the store + * @param opts.rdfFactory - The rdf factory that should be used by the store */ - constructor (statements?: Statement[], constraints?, initBindings?, optional?, opts: Partial = {}) { + constructor ( + statements?: TFQuad[], + constraints?: ReadonlyArray, + initBindings?: ReadonlyArray, + optional?: ReadonlyArray, + opts: FormulaOpts = {} + ) { super('') this.termType = Formula.termType this.statements = statements || [] @@ -69,25 +119,32 @@ export default class Formula extends Node { this[factoryMethod] = (...args) => this.rdfFactory[factoryMethod](...args) } } + /** Add a statement from its parts - * @param {Node} subject - the first part of the statement - * @param {Node} predicate - the second part of the statement - * @param {Node} object - the third part of the statement - * @param {Node} graph - the last part of the statement + * @param subject - the first part of the statement + * @param predicate - the second part of the statement + * @param object - the third part of the statement + * @param graph - the last part of the statement */ - add (subject, predicate, object, graph) { - return this.statements.push(this.rdfFactory.quad(subject, predicate, object, graph)) + add ( + subject: TFSubject, + predicate: TFPredicate, + object: TFObject, + graph?: TFGraph + ): number { + return this.statements + .push(this.rdfFactory.quad(subject, predicate, object, graph)) } /** Add a statment object * @param {Statement} statement - An existing constructed statement to add */ - addStatement (statement) { + addStatement (statement: TFQuad): number { return this.statements.push(statement) } /** @deprecated use {this.rdfFactory.blankNode} instead */ - bnode (id) { + bnode (id?: string): TFBlankNode { return this.rdfFactory.blankNode(id) } @@ -95,7 +152,7 @@ export default class Formula extends Node { * Adds all the statements to this formula * @param statements - A collection of statements */ - addAll (statements) { + addAll (statements: TFQuad[]): void { statements.forEach(quad => { this.add(quad.subject, quad.predicate, quad.object, quad.graph) }) @@ -107,14 +164,19 @@ export default class Formula extends Node { * any(me, knows, null, null) - a person I know accoring to anything in store . * any(null, knows, me, null) - a person who know me accoring to anything in store . * - * @param {Node} s - A node to search for as subject, or if null, a wildcard - * @param {Node} p - A node to search for as predicate, or if null, a wildcard - * @param {Node} o - A node to search for as object, or if null, a wildcard - * @param {Node} g - A node to search for as graph, or if null, a wildcard - * @returns {Node} - A node which match the wildcard position, or null + * @param s - A node to search for as subject, or if null, a wildcard + * @param p - A node to search for as predicate, or if null, a wildcard + * @param o - A node to search for as object, or if null, a wildcard + * @param g - A node to search for as graph, or if null, a wildcard + * @returns A node which match the wildcard position, or null */ - any (s, p, o, g): any | undefined { - let st = this.anyStatementMatching(s, p, o, g) + any( + s?: TFSubject | null, + p?: TFPredicate | null, + o?: TFObject | null, + g?: TFGraph | null + ): TFTerm | null | undefined { + const st = this.anyStatementMatching(s, p, o, g) if (st == null) { return void 0 } else if (s == null) { @@ -130,28 +192,52 @@ export default class Formula extends Node { /** * Gets the value of a node that matches the specified pattern + * @param s The subject + * @param p The predicate + * @param o The object + * @param g The graph that contains the statement */ - anyValue (s, p, o, g) { - let y = this.any(s, p, o, g) + anyValue( + s?: TFSubject | null, + p?: TFPredicate | null, + o?: TFObject | null, + g?: TFGraph | null + ): string | void { + const y = this.any(s, p, o, g) return y ? y.value : void 0 } /** * Gets the first JavaScript object equivalent to a node based on the specified pattern + * @param s The subject + * @param p The predicate + * @param o The object + * @param g The graph that contains the statement */ - anyJS (s, p, o, g) { - let y = this.any(s, p, o, g) + anyJS( + s?: TFSubject | null, + p?: TFPredicate | null, + o?: TFObject | null, + g?: TFGraph | null + ): any { + const y = this.any(s, p, o, g) return y ? Node.toJS(y) : void 0 } /** * Gets the first statement that matches the specified pattern */ - anyStatementMatching (subj, pred, obj, why) { - let x = this.statementsMatching(subj, pred, obj, why, true) + anyStatementMatching( + s?: TFSubject | null, + p?: TFPredicate | null, + o?: TFObject | null, + g?: TFGraph | null + ): TFQuad | undefined { + let x = this.statementsMatching(s, p, o, g, true) if (!x || x.length === 0) { return undefined } + return x[0] } @@ -160,7 +246,7 @@ export default class Formula extends Node { * * Falls back to the rdflib hashString implementation if the given factory doesn't support id. */ - id (term): string | number { + id (term: TFIDFactoryTypes): Indexable { return this.rdfFactory.id(term) } @@ -168,20 +254,32 @@ export default class Formula extends Node { * Search the Store * This is really a teaching method as to do this properly you would use IndexedFormula * - * @param {Node} subj - A node to search for as subject, or if null, a wildcard - * @param {Node} pred - A node to search for as predicate, or if null, a wildcard - * @param {Node} obj - A node to search for as object, or if null, a wildcard - * @param {Node} graph - A node to search for as graph, or if null, a wildcard - * @param {Boolean} justOne - flag - stop when found one rather than get all of them? + * @param s - A node to search for as subject, or if null, a wildcard + * @param p - A node to search for as predicate, or if null, a wildcard + * @param o - A node to search for as object, or if null, a wildcard + * @param g - A node to search for as graph, or if null, a wildcard + * @param justOne - flag - stop when found one rather than get all of them? * @returns {Array} - An array of nodes which match the wildcard position */ - statementsMatching (subj?, pred?, obj?, graph?, justOne = false): Statement[] { - return this.statements.filter(st => - (!subj || subj.equals(st.subject)) && - (!pred || pred.equals(st.predicate)) && - (!obj || subj.equals(st.object)) && - (!graph || graph.equals(st.subject)) - ) + statementsMatching( + s?: TFSubject | null, + p?: TFPredicate | null, + o?: TFObject | null, + g?: TFGraph | null, + justOne?: boolean + ): TFQuad[] { + const sts = this.statements.filter(st => + (!s || s.equals(st.subject)) && + (!p || p.equals(st.predicate)) && + (!o || o.equals(st.object)) && + (!g || g.equals(st.subject)) + ) + + if (justOne) { + return sts.length === 0 ? [] : [sts[0]] + } + + return sts } /** @@ -237,14 +335,19 @@ export default class Formula extends Node { * each(me, knows, null, null) - people I know accoring to anything in store . * each(null, knows, me, null) - people who know me accoring to anything in store . * - * @param {Node} s - A node to search for as subject, or if null, a wildcard - * @param {Node} p - A node to search for as predicate, or if null, a wildcard - * @param {Node} o - A node to search for as object, or if null, a wildcard - * @param {Node} g - A node to search for as graph, or if null, a wildcard + * @param s - A node to search for as subject, or if null, a wildcard + * @param p - A node to search for as predicate, or if null, a wildcard + * @param o - A node to search for as object, or if null, a wildcard + * @param g - A node to search for as graph, or if null, a wildcard * @returns {Array} - An array of nodes which match the wildcard position */ - each (s?: any | null, p?: any | null, o?: any | null, g?: any | null) { - const results: any[] = [] + each( + s?: TFSubject | null, + p?: TFPredicate | null, + o?: TFObject | null, + g?: TFGraph | null + ): TFTerm[] { + const results: TFTerm[] = [] let sts = this.statementsMatching(s, p, o, g, false) if (s == null) { for (let i = 0, len = sts.length; i < len; i++) { @@ -288,20 +391,20 @@ export default class Formula extends Node { * @return a hash of URIs */ findMembersNT (thisClass) { - let len2 - let len4 - let m - let members - let pred + let len2: number + let len4: number + let m: number + let members: MembersMap + let pred: TFPredicate let ref - let ref1 - let ref2 - let ref3 - let ref4 - let ref5 + let ref1: TFQuad[] + let ref2: TFTerm[] + let ref3: TFQuad[] + let ref4: TFTerm[] + let ref5: TFQuad[] let seeds let st - let u + let u: number seeds = {} seeds[thisClass.toNT()] = true members = {} @@ -319,7 +422,7 @@ export default class Formula extends Node { this.rdfFactory.namedNode('http://www.w3.org/2000/01/rdf-schema#domain'), this.fromNT(t)) for (let l = 0, len1 = ref2.length; l < len1; l++) { - pred = ref2[l] + pred = ref2[l] as TFPredicate ref3 = this.statementsMatching(void 0, pred) for (m = 0, len2 = ref3.length; m < len2; m++) { st = ref3[m] @@ -330,7 +433,7 @@ export default class Formula extends Node { this.rdfFactory.namedNode('http://www.w3.org/2000/01/rdf-schema#range'), this.fromNT(t)) for (let q = 0, len3 = ref4.length; q < len3; q++) { - pred = ref4[q] + pred = ref4[q] as TFPredicate ref5 = this.statementsMatching(void 0, pred) for (u = 0, len4 = ref5.length; u < len4; u++) { st = ref5[u] @@ -351,7 +454,7 @@ export default class Formula extends Node { * Get all the Classes of which we can RDFS-infer the subject is a member * @param subject - A named node */ - findMemberURIs (subject) { + findMemberURIs(subject: Node): UriMap { return this.NTtoURI(this.findMembersNT(subject)) } @@ -362,7 +465,7 @@ export default class Formula extends Node { * Does NOT return terms, returns URI strings. * We use NT representations in this version because they handle blank nodes. */ - findSubClassesNT (subject) { + findSubClassesNT(subject: Node): { [uri: string]: boolean } { let types = {} types[subject.toNT()] = true return this.transitiveClosure( @@ -380,7 +483,7 @@ export default class Formula extends Node { * Does NOT return terms, returns URI strings. * We use NT representations in this version because they handle blank nodes. */ - findSuperClassesNT (subject) { + findSuperClassesNT(subject: Node): { [uri: string]: boolean } { let types = {} types[subject.toNT()] = true return this.transitiveClosure(types, @@ -443,22 +546,36 @@ export default class Formula extends Node { ) } - findTypeURIs (subject) { + /** + * Get all the Classes of which we can RDFS-infer the subject is a member + * todo: This will loop is there is a class subclass loop (Sublass loops are + * not illegal) + * Returns a hash table where key is NT of type and value is statement why we + * think so. + * Does NOT return terms, returns URI strings. + * We use NT representations in this version because they handle blank nodes. + * @param subject - A subject node + */ + findTypeURIs(subject: TFSubject): UriMap { return this.NTtoURI(this.findTypesNT(subject)) } /** Trace statements which connect directly, or through bnodes * - * @param {NamedNode} subject - The node to start looking for statments - * @param {NamedNode} doc - The document to be searched, or null to search all documents + * @param subject - The node to start looking for statments + * @param doc - The document to be searched, or null to search all documents * @returns an array of statements, duplicate statements are suppresssed. */ - connectedStatements (subject, doc, excludePredicateURIs): Statement[] { + connectedStatements( + subject: TFSubject, + doc: TFGraph, + excludePredicateURIs?: ReadonlyArray + ): TFQuad[] { excludePredicateURIs = excludePredicateURIs || [] let todo = [subject] let done: { [k: string]: boolean } = {} let doneArcs: { [k: string]: boolean } = {} - let result: Statement[] = [] + let result: TFQuad[] = [] let self = this let follow = function (x) { let queue = function (x) { @@ -469,15 +586,15 @@ export default class Formula extends Node { } let sts = self.statementsMatching(null, null, x, doc) .concat(self.statementsMatching(x, null, null, doc)) - sts = sts.filter(function (st) { - if (excludePredicateURIs[st.predicate.uri]) return false - let hash = st.toNT() + sts = sts.filter(function (st): boolean { + if (excludePredicateURIs![st.predicate.value]) return false + let hash = (st as Statement).toNT() if (doneArcs[hash]) return false doneArcs[hash] = true return true } ) - sts.forEach(function (st, i) { + sts.forEach(function (st) { queue(st.subject) queue(st.object) }) @@ -490,7 +607,12 @@ export default class Formula extends Node { return result } - formula () { + /** + * Creates a new empty formula + * + * @param _features - Not applicable, but necessary for typing to pass + */ + formula(_features?: ReadonlyArray): Formula { return new Formula() } @@ -546,7 +668,7 @@ export default class Formula extends Node { } return true } else if (isStatement(s)) { - return this.holds(s.subject, s.predicate, s.object, s.why) + return this.holds(s.subject, s.predicate, s.object, s.graph) } else if (s.statements) { return this.holds(s.statements) } @@ -646,13 +768,14 @@ export default class Formula extends Node { * Creates a new formula with the substituting bindings applied * @param bindings - The bindings to substitute */ - substitute (bindings) { + //@ts-ignore signature not compatible with Node + substitute(bindings: Bindings): Formula { let statementsCopy = this.statements.map(function (ea) { - return ea.substitute(bindings) + return (ea as Statement).substitute(bindings) }) console.log('Formula subs statmnts:' + statementsCopy) const y = new Formula() - y.addAll(statementsCopy) + y.addAll(statementsCopy as TFQuad[]) console.log('indexed-form subs formula:' + y) return y } @@ -669,12 +792,22 @@ export default class Formula extends Node { /** * Gets the node matching the specified pattern. Throws when no match could be made. + * @param s - The subject + * @param p - The predicate + * @param o - The object + * @param g - The graph that contains the statement */ - the (s, p, o, g) { + the ( + s?: TFSubject | null, + p?: TFPredicate | null, + o?: TFObject | null, + g?: TFGraph | null + ): TFTerm | null | undefined { let x = this.any(s, p, o, g) if (x == null) { log.error('No value found for the() {' + s + ' ' + p + ' ' + o + '}.') } + return x } @@ -682,11 +815,17 @@ export default class Formula extends Node { * RDFS Inference * These are hand-written implementations of a backward-chaining reasoner * over the RDFS axioms. - * @param seeds {Object} - A hash of NTs of classes to start with + * @param seeds - A hash of NTs of classes to start with * @param predicate - The property to trace though * @param inverse - Trace inverse direction */ - transitiveClosure (seeds, predicate, inverse) { + transitiveClosure( + seeds: BooleanMap, + predicate: TFPredicate, + inverse?: boolean + ): { + [uri: string]: boolean; + } { let elt, i, len, s, sups, t let agenda = {} Object.assign(agenda, seeds) // make a copy @@ -701,7 +840,9 @@ export default class Formula extends Node { if (t == null) { return done } - sups = inverse ? this.each(void 0, predicate, this.fromNT(t)) : this.each(this.fromNT(t), predicate) + sups = inverse ? + this.each(void 0, predicate, this.fromNT(t)) + : this.each(this.fromNT(t) as TFPredicate, predicate) for (i = 0, len = sups.length; i < len; i++) { elt = sups[i] s = elt.toNT() @@ -722,8 +863,13 @@ export default class Formula extends Node { * Finds the types in the list which have no *stored* supertypes * We exclude the universal class, owl:Things and rdf:Resource, as it is * information-free. + * @param types - The types */ - topTypeURIs (types) { + topTypeURIs(types: { + [id: string]: string | NamedNode; + }): { + [id: string]: string | NamedNode; + } { let i let j let k @@ -783,7 +929,12 @@ export default class Formula extends Node { * @param o - The object * @param g - The graph that contains the statement */ - whether (s, p, o, g): number { + whether( + s?: TFSubject | null, + p?: TFPredicate | null, + o?: TFObject | null, + g?: TFGraph | null + ): number { return this.statementsMatching(s, p, o, g, false).length } } diff --git a/src/jsonldparser.js b/src/jsonldparser.js index 928414526..0a1818891 100644 --- a/src/jsonldparser.js +++ b/src/jsonldparser.js @@ -1,6 +1,6 @@ import jsonld from 'jsonld' -import { arrayToStatements } from './util' +import { arrayToStatements } from './utils' /** * Parses json-ld formatted JS objects to a rdf Term. diff --git a/src/literal.ts b/src/literal.ts index 5af121984..257e28e6c 100644 --- a/src/literal.ts +++ b/src/literal.ts @@ -1,27 +1,46 @@ -'use strict' import ClassOrder from './class-order' import NamedNode from './named-node' import Node from './node-internal' -import { TermType } from './types'; +import { + LiteralTermType, + TermType, + TFLiteral, + TFTerm, + ValueType +} from './types' +import { isTFLiteral } from './utils/terms' import XSD from './xsd-internal' /** * An RDF literal, containing some value which isn't expressed as an IRI. * @link https://rdf.js.org/data-model-spec/#literal-interface */ -export default class Literal extends Node { +// @ts-ignore Incorrectly extends due to fromValue() +export default class Literal extends Node implements TFLiteral { static termType = TermType.Literal classOrder = ClassOrder.Literal - datatype = XSD.string + + /** + * The literal's datatype as a named node + */ + datatype: NamedNode = XSD.string + isVar = 0 + /** * The language for the literal */ language: string = '' - termType = TermType.Literal + termType: LiteralTermType = TermType.Literal + /** + * Initializes a literal + * @param value - The literal's lexical value + * @param language - The language for the literal. Defaults to ''. + * @param datatype - The literal's datatype as a named node. Defaults to xsd:string. + */ constructor (value: string, language?: string | null, datatype?) { super(value) @@ -46,31 +65,31 @@ export default class Literal extends Node { * Gets whether two literals are the same * @param other The other statement */ - equals (other: any): boolean { + equals (other: TFTerm): boolean { if (!other) { return false } return (this.termType === other.termType) && (this.value === other.value) && - (this.language === other.language) && - ((!this.datatype && !other.datatype) || - (this.datatype && this.datatype.equals(other.datatype))) + (this.language === (other as Literal).language) && + ((!this.datatype && !(other as Literal).datatype) || + (this.datatype && this.datatype.equals((other as Literal).datatype))) } /** * The language for the literal * @deprecated use {language} instead */ - get lang () { + get lang (): string { return this.language } - set lang (language) { + set lang (language: string) { this.language = language || '' } - toNT() { + toNT(): string { return Literal.toNT(this) } @@ -103,7 +122,7 @@ export default class Literal extends Node { /** * Builds a literal node from a boolean value - * @param value {Boolean} The value + * @param value - The value */ static fromBoolean (value: boolean): Literal { let strValue = value ? '1' : '0' @@ -129,13 +148,13 @@ export default class Literal extends Node { /** * Builds a literal node from a number value - * @param value The value + * @param value - The value */ static fromNumber(value: number): Literal { if (typeof value !== 'number') { throw new TypeError('Invalid argument to Literal.fromNumber()') } - let datatype + let datatype: NamedNode const strValue = value.toString() if (strValue.indexOf('e') < 0 && Math.abs(value) <= Number.MAX_SAFE_INTEGER) { datatype = Number.isInteger(value) ? XSD.integer : XSD.decimal @@ -147,29 +166,26 @@ export default class Literal extends Node { /** * Builds a literal node from an input value - * @param value The input value + * @param value - The input value */ - static fromValue (value) { - if (typeof value === 'undefined' || value === null) { - return value - } - if (typeof value === 'object' && value.termType) { // this is a Node instance - return value + static fromValue(value: ValueType): T { + if (isTFLiteral(value)) { + return value as T } switch (typeof value) { case 'object': if (value instanceof Date) { - return Literal.fromDate(value) + return Literal.fromDate(value) as T } case 'boolean': - return Literal.fromBoolean(value) + return Literal.fromBoolean(value as boolean) as T case 'number': - return Literal.fromNumber(value) + return Literal.fromNumber(value as number) as T case 'string': - return new Literal(value) + return new Literal(value) as T } + throw new Error("Can't make literal from " + value + ' of type ' + typeof value) - } } diff --git a/src/n3parser.js b/src/n3parser.js index 56152a9a6..779d7c1c9 100644 --- a/src/n3parser.js +++ b/src/n3parser.js @@ -5,7 +5,7 @@ * **/ import * as Uri from './uri' -import { ArrayIndexOf } from './util' +import { ArrayIndexOf } from './utils' export default (function () { diff --git a/src/named-node.ts b/src/named-node.ts index d22ab5848..2b333c0b1 100644 --- a/src/named-node.ts +++ b/src/named-node.ts @@ -1,17 +1,17 @@ 'use strict' import ClassOrder from './class-order' import Node from './node-internal' -import { TermType } from './types'; -import { termValue } from "./utils/terms"; +import { NamedNodeTermType, TermType, TFNamedNode } from './types' +import { termValue } from './utils/terms' /** * A named (IRI) RDF node */ -export default class NamedNode extends Node { +export default class NamedNode extends Node implements TFNamedNode { static termType = TermType.NamedNode classOrder = ClassOrder.NamedNode - termType = TermType.NamedNode + termType: NamedNodeTermType = TermType.NamedNode /** * Create a named (IRI) RDF Node @@ -39,7 +39,7 @@ export default class NamedNode extends Node { * Returns an $rdf node for the containing directory, ending in slash. */ dir () { - var str = this.uri.split('#')[0] + var str = this.value.split('#')[0] var p = str.slice(0, -1).lastIndexOf('/') var q = str.indexOf('//') if ((q >= 0 && p < q + 2) || p < 0) return null @@ -51,7 +51,7 @@ export default class NamedNode extends Node { * Contrast with the "origin" which does NOT have a trailing slash */ site () { - var str = this.uri.split('#')[0] + var str = this.value.split('#')[0] var p = str.indexOf('//') if (p < 0) throw new Error('This URI does not have a web site part (origin)') var q = str.indexOf('/', p+2) @@ -67,10 +67,10 @@ export default class NamedNode extends Node { * Removes everything from the # anchor tag. */ doc () { - if (this.uri.indexOf('#') < 0) { + if (this.value.indexOf('#') < 0) { return this } else { - return new NamedNode(this.uri.split('#')[0]) + return new NamedNode(this.value.split('#')[0]) } } @@ -78,12 +78,12 @@ export default class NamedNode extends Node { * Returns the URI including */ toString () { - return '<' + this.uri + '>' + return '<' + this.value + '>' } /** The local identifier with the document */ id () { - return this.uri.split('#')[1] + return this.value.split('#')[1] } /** diff --git a/src/namespace.ts b/src/namespace.ts index 814b3c225..63066df4b 100644 --- a/src/namespace.ts +++ b/src/namespace.ts @@ -1,14 +1,15 @@ import NamedNode from './named-node' +import { TFDataFactory, TFNamedNode } from './types' /** * Gets a namespace for the specified namespace's URI * @param nsuri - The URI for the namespace * @param [factory] - The factory for creating named nodes with */ -export default function Namespace (nsuri: string, factory?) { - const dataFactory = factory || { namedNode: (value) => new NamedNode(value) } +export default function Namespace (nsuri: string, factory?: TFDataFactory): (ln: string) => TFNamedNode { + const dataFactory = factory || { namedNode: (value) => new NamedNode(value) as TFNamedNode } - return function (ln: string) { + return function (ln: string): TFNamedNode { return dataFactory.namedNode(nsuri + (ln || '')) } } diff --git a/src/node-internal.ts b/src/node-internal.ts index 208409683..b3ffda72f 100644 --- a/src/node-internal.ts +++ b/src/node-internal.ts @@ -1,3 +1,5 @@ +import { ValueType, Bindings, TFTerm, FromValueReturns } from './types' + /** * The superclass of all RDF Statement objects, that is * NamedNode, Literal, BlankNode, etc. @@ -6,23 +8,23 @@ * @link https://rdf.js.org/data-model-spec/#term-interface * @class Node */ -export default class Node { - static fromValue: (value: any) => T; +export default abstract class Node { + // Specified in './node.ts' to prevent circular dependency + static fromValue: (value: ValueType) => T + // Specified in './node.ts' to prevent circular dependency static toJS: (term: any) => Date | Number | string | boolean | object | Array; - /** - * The type of node + * The nodes in this collection */ + elements!: Node[]; + + /** The type of node */ termType!: string; - /** - * The class order for this node - */ + /** The class order for this node */ classOrder!: number; - /** - * The node's value - */ + /** The node's value */ value: string; constructor(value: string) { @@ -33,16 +35,16 @@ export default class Node { * Creates the substituted node for this one, according to the specified bindings * @param bindings - Bindings of identifiers to nodes */ - substitute (bindings): Node | any { + substitute (bindings: Bindings): T { console.log('@@@ node substitute' + this) - return this + return this as unknown as T } /** * Compares this node with another * @param other - The other node */ - compareTerm (other): number { + compareTerm (other: Node): number { if (this.classOrder < other.classOrder) { return -1 } @@ -62,7 +64,7 @@ export default class Node { * Compares whether the two nodes are equal * @param other The other node */ - equals (other): boolean { + equals (other: TFTerm): boolean { if (!other) { return false } @@ -82,8 +84,9 @@ export default class Node { * Compares whether this node is the same as the other one * @param other - Another node */ - sameTerm (other): boolean { + sameTerm(other: Node): boolean { return this.equals(other) + } /** diff --git a/src/node.ts b/src/node.ts index 55c6b26b4..9d360c0ba 100644 --- a/src/node.ts +++ b/src/node.ts @@ -1,11 +1,10 @@ -'use strict' - // This file attaches all functionality to Node // that would otherwise require circular dependencies. +import { fromValue } from './collection' import Node from './node-internal' -import Collection, { fromValue } from "./collection"; - -export default Node +import { TFTerm } from './types' +import Namespace from './namespace' +import { isCollection, isTFLiteral } from './utils/terms' /** * Creates an RDF Node from a native javascript value. @@ -15,18 +14,23 @@ export default Node * @param value {Node|Date|String|Number|Boolean|Undefined} * @return {Node|Collection} */ -Node.fromValue = fromValue +Node.fromValue = fromValue; + +export default Node -import Namespace from './namespace' const ns = { xsd: Namespace('http://www.w3.org/2001/XMLSchema#') } -Node.toJS = function toJS (term) { - if (term.elements) { +/** + * Gets the javascript object equivalent to a node + * @param term The RDF node + */ +Node.toJS = function (term: TFTerm): TFTerm | boolean | number | Date | string | any[] { + if (isCollection(term)) { return term.elements.map(Node.toJS) // Array node (not standard RDFJS) } - if (!term.datatype) return term // Objects remain objects + if (!isTFLiteral(term)) return term if (term.datatype.equals(ns.xsd('boolean'))) { - return term.value === '1' + return term.value === '1' || term.value === 'true' } if (term.datatype.equals(ns.xsd('dateTime')) || term.datatype.equals(ns.xsd('date'))) { diff --git a/src/parse.js b/src/parse.ts similarity index 50% rename from src/parse.js rename to src/parse.ts index ae6c47012..db22fba15 100644 --- a/src/parse.js +++ b/src/parse.ts @@ -1,45 +1,63 @@ import DataFactory from './data-factory' import jsonldParser from './jsonldparser' +// @ts-ignore is this injected? import { Parser as N3jsParser } from 'n3' // @@ Goal: remove this dependency import N3Parser from './n3parser' import { parseRDFaDOM } from './rdfaparser' import RDFParser from './rdfxmlparser' import sparqlUpdateParser from './patch-parser' import * as Util from './util' +import Formula from './formula' +import { TFQuad, ContentType } from './types' + +type CallbackFunc = (error: any, kb: Formula | null) => void /** * Parse a string and put the result into the graph kb. * Normal method is sync. * Unfortunately jsdonld is currently written to need to be called async. * Hence the mess below with executeCallback. + * @param str - The input string to parse + * @param kb - The store to use + * @param base - The base URI to use + * @param contentType - The MIME content type string for the input + * @param callback - The callback to call when the data has been loaded */ -export default function parse (str, kb, base, contentType, callback) { - contentType = contentType || 'text/turtle' - contentType = contentType.split(';')[0] +export default function parse ( + str: string, + kb: Formula, + base: string, + contentType: string | ContentType, + callback?: CallbackFunc +) { + contentType = contentType || ContentType.turtle + contentType = contentType.split(';')[0] as ContentType try { - if (contentType === 'text/n3' || contentType === 'text/turtle') { + if (contentType === ContentType.n3 || contentType === ContentType.turtle) { var p = N3Parser(kb, kb, base, base, null, null, '', null) p.loadBuf(str) executeCallback() - } else if (contentType === 'application/rdf+xml') { + } else if (contentType === ContentType.rdfxml) { var parser = new RDFParser(kb) parser.parse(Util.parseXML(str), base, kb.sym(base)) executeCallback() - } else if (contentType === 'application/xhtml+xml') { - parseRDFaDOM(Util.parseXML(str, {contentType: 'application/xhtml+xml'}), kb, base) + } else if (contentType === ContentType.xhtml) { + parseRDFaDOM(Util.parseXML(str, {contentType: ContentType.xhtml}), kb, base) executeCallback() - } else if (contentType === 'text/html') { - parseRDFaDOM(Util.parseXML(str, {contentType: 'text/html'}), kb, base) + } else if (contentType === ContentType.html) { + parseRDFaDOM(Util.parseXML(str, {contentType: ContentType.html}), kb, base) executeCallback() - } else if (contentType === 'application/sparql-update') { // @@ we handle a subset + } else if (contentType === ContentType.sparqlupdate) { // @@ we handle a subset sparqlUpdateParser(str, kb, base) executeCallback() - } else if (contentType === 'application/ld+json') { + } else if (contentType === ContentType.jsonld) { jsonldParser(str, kb, base, executeCallback) - } else if (contentType === 'application/nquads' || - contentType === 'application/n-quads') { + } else if (contentType === ContentType.nQuads || + contentType === ContentType.nQuadsAlt) { var n3Parser = new N3jsParser({ factory: DataFactory }) nquadCallback(null, str) + } else if (contentType === undefined) { + throw new Error("contentType is undefined") } else { throw new Error("Don't know how to parse " + contentType + ' yet') } @@ -47,7 +65,7 @@ export default function parse (str, kb, base, contentType, callback) { executeErrorCallback(e) } - parse.handled = { + (parse as any).handled= { 'text/n3': true, 'text/turtle': true, 'application/rdf+xml': true, @@ -67,14 +85,20 @@ export default function parse (str, kb, base, contentType, callback) { } } - function executeErrorCallback (e) { - if (contentType !== 'application/ld+json' || - contentType !== 'application/nquads' || - contentType !== 'application/n-quads') { + function executeErrorCallback (e: Error): void { + if ( + // TODO: Always true, what is the right behavior + contentType !== ContentType.jsonld || + // @ts-ignore always true? + contentType !== ContentType.nQuads || + // @ts-ignore always true? + contentType !== ContentType.nQuadsAlt + ) { if (callback) { callback(e, kb) } else { let e2 = new Error('' + e + ' while trying to parse <' + base + '> as ' + contentType) + //@ts-ignore .cause is not a default error property e2.cause = e throw e2 } @@ -91,22 +115,22 @@ export default function parse (str, kb, base, contentType, callback) { doc['@context']['@base'] = base } */ - function nquadCallback (err, nquads) { + function nquadCallback (err?: Error | null, nquads?: string): void { if (err) { - callback(err, kb) + (callback as CallbackFunc)(err, kb) } try { n3Parser.parse(nquads, tripleCallback) } catch (err) { - callback(err, kb) + (callback as CallbackFunc)(err, kb) } } - function tripleCallback (err, triple, prefixes) { + function tripleCallback (err: Error, triple: TFQuad) { if (triple) { - kb.add(triple) + kb.add(triple.subject, triple.predicate, triple.object, triple.graph) } else { - callback(err, kb) + (callback as CallbackFunc)(err, kb) } } } diff --git a/src/rdfaparser.js b/src/rdfaparser.js index ef3e4763b..5283f2055 100644 --- a/src/rdfaparser.js +++ b/src/rdfaparser.js @@ -14,10 +14,10 @@ import BlankNode from './blank-node' import Literal from './literal' -import rdf from './data-factory' import NamedNode from './named-node' import * as Uri from './uri' import * as Util from './util' +import rdf from './data-factory-internal' if (typeof Node === 'undefined') { // @@@@@@ Global. Interface to xmldom. var Node = { diff --git a/src/serialize.js b/src/serialize.ts similarity index 54% rename from src/serialize.js rename to src/serialize.ts index ea48d8879..b380b8c7b 100644 --- a/src/serialize.js +++ b/src/serialize.ts @@ -1,54 +1,74 @@ import * as convert from './convert' import Serializer from './serializer' +import { ContentType, TFNamedNode, TFBlankNode } from './types' +import IndexedFormula from './store' +import { Formula } from './index' /** * Serialize to the appropriate format - * @@ Currently NQuads and JSON/LD are deal with extrelemently inefficiently - * through mutiple conversions. */ -export default function serialize (target, kb, base, contentType, callback, options) { - base = base || target.uri - options = options || {} - contentType = contentType || 'text/turtle' // text/n3 if complex? - var documentString = null +export default function serialize ( + /** The graph or nodes that should be serialized */ + target: Formula | TFNamedNode | TFBlankNode, + /** The store */ + kb?: IndexedFormula, + base?: unknown, + /** + * The mime type. + * Defaults to Turtle. + */ + contentType?: string | ContentType, + callback?: (err?: Error | null, result?: string ) => any, + options?: { + /** + * A string of letters, each of which set an options + * e.g. `deinprstux` + */ + flags: string + } +): string | undefined { + base = base || target.value + const opts = options || {} + contentType = contentType || ContentType.turtle // text/n3 if complex? + var documentString: string | null = null try { var sz = Serializer(kb) - if (options.flags) sz.setFlags(options.flags) - var newSts = kb.statementsMatching(undefined, undefined, undefined, target) - var n3String - sz.suggestNamespaces(kb.namespaces) + if ((opts as any).flags) sz.setFlags((opts as any).flags) + var newSts = kb!.statementsMatching(undefined, undefined, undefined, target as TFNamedNode) + var n3String: string + sz.suggestNamespaces(kb!.namespaces) sz.setBase(base) switch (contentType) { - case 'application/rdf+xml': + case ContentType.rdfxml: documentString = sz.statementsToXML(newSts) return executeCallback(null, documentString) - case 'text/n3': - case 'application/n3': // Legacy + case ContentType.n3: + case ContentType.n3Legacy: documentString = sz.statementsToN3(newSts) return executeCallback(null, documentString) - case 'text/turtle': - case 'application/x-turtle': // Legacy + case ContentType.turtle: + case ContentType.turtleLegacy: sz.setFlags('si') // Suppress = for sameAs and => for implies documentString = sz.statementsToN3(newSts) return executeCallback(null, documentString) - case 'application/n-triples': + case ContentType.nTriples: sz.setFlags('deinprstux') // Suppress nice parts of N3 to make ntriples documentString = sz.statementsToNTriples(newSts) return executeCallback(null, documentString) - case 'application/ld+json': + case ContentType.jsonld: sz.setFlags('deinprstux') // Use adapters to connect to incmpatible parser n3String = sz.statementsToNTriples(newSts) // n3String = sz.statementsToN3(newSts) convert.convertToJson(n3String, callback) break - case 'application/n-quads': - case 'application/nquads': // @@@ just outpout the quads? Does not work for collections + case ContentType.nQuads: + case ContentType.nQuadsAlt: // @@@ just outpout the quads? Does not work for collections sz.setFlags('deinprstux q') // Suppress nice parts of N3 to make ntriples documentString = sz.statementsToNTriples(newSts) // q in flag means actually quads return executeCallback(null, documentString) // n3String = sz.statementsToN3(newSts) // documentString = convert.convertToNQuads(n3String, callback) - break + // break default: throw new Error('Serialize: Content-type ' + contentType + ' not supported for data write.') } @@ -59,7 +79,7 @@ export default function serialize (target, kb, base, contentType, callback, opti throw err // Don't hide problems from caller in sync mode } - function executeCallback (err, result) { + function executeCallback (err?: Error | null, result?: string) { if (callback) { callback(err, result) return diff --git a/src/statement.ts b/src/statement.ts index 01a04420d..67c7c33d5 100644 --- a/src/statement.ts +++ b/src/statement.ts @@ -1,29 +1,33 @@ -import BlankNode from './blank-node' -import Collection from './collection' -import DefaultGraph from './default-graph' -import Empty from './empty' +import { defaultGraph } from './data-factory-internal' import NamedNode from './named-node' import Node from './node-internal' -import IndexedFormula from './store' -import Variable from './variable' +import { + Bindings, + GraphType, + ObjectType, + PredicateType, + SubjectType, + TermType, + TFQuad, +} from './types' +import Literal from './literal' /** A Statement represents an RDF Triple or Quad. */ -export default class Statement { +export default class Statement implements TFQuad { /** The subject of the triple. What the Statement is about. */ - subject: NamedNode | BlankNode | Variable + subject: SubjectType /** The relationship which is asserted between the subject and object */ - predicate: NamedNode | Variable + predicate: PredicateType /** The thing or data value which is asserted to be related to the subject */ - object: NamedNode | BlankNode | Collection | Empty | Variable + object: ObjectType /** * The graph param is a named node of the document in which the triple when * it is stored on the web. */ - graph: NamedNode | DefaultGraph | IndexedFormula - + graph: GraphType /** * Construct a new statement @@ -42,11 +46,16 @@ export default class Statement { * and give the document you are patching. In future, we may have a more * powerful update() which can update more than one document. */ - constructor (subject, predicate, object, graph) { + constructor ( + subject: SubjectType, + predicate: PredicateType, + object: ObjectType, + graph?: GraphType, + ) { this.subject = Node.fromValue(subject) this.predicate = Node.fromValue(predicate) this.object = Node.fromValue(object) - this.graph = graph // property currently used by rdflib + this.graph = graph == undefined ? defaultGraph() : graph // property currently used by rdflib } /** @deprecated use {graph} instead */ @@ -62,21 +71,26 @@ export default class Statement { * Checks whether two statements are the same * @param other - The other statement */ - equals (other): boolean { - return other.subject.equals(this.subject) && other.predicate.equals(this.predicate) && - other.object.equals(this.object) && other.graph.equals(this.graph) + equals (other: TFQuad): boolean { + return ( + other.subject.equals(this.subject) && + other.predicate.equals(this.predicate) && + other.object.equals(this.object as Literal) && + other.graph.equals(this.graph) + ) } /** * Creates a statement with the bindings substituted - * @param bindings - The bindings + * @param bindings The bindings */ - substitute (bindings): Statement { + substitute (bindings: Bindings): Statement { const y = new Statement( this.subject.substitute(bindings), this.predicate.substitute(bindings), this.object.substitute(bindings), - this.graph.substitute(bindings)) // 2016 + this.graph.substitute(bindings), + ) // 2016 console.log('@@@ statement substitute:' + y) return y } @@ -88,7 +102,7 @@ export default class Statement { this.predicate.toCanonical(), this.object.toCanonical() ] - if (this.graph && this.graph.termType !== 'DefaultGraph') { + if (this.graph && this.graph.termType !== TermType.DefaultGraph) { terms.push(this.graph.toCanonical()) } return terms.join(' ') + ' .' diff --git a/src/store.js b/src/store.ts similarity index 58% rename from src/store.js rename to src/store.ts index 88a01ce06..ef400695b 100644 --- a/src/store.js +++ b/src/store.ts @@ -10,6 +10,7 @@ * * 2005-10 Written Tim Berners-Lee * 2007 Changed so as not to munge statements from documents when smushing + * 2019 Converted to typescript * * */ @@ -18,22 +19,42 @@ import ClassOrder from './class-order' import { defaultGraphURI } from './data-factory-internal' -import DataFactory from './data-factory' -import Formula from './formula' -import { ArrayIndexOf, RDFArrayRemove } from './util' -import { isStatement, isStore } from './utils/terms' -import Statement from './statement' +import Formula, { FormulaOpts } from './formula' +import { ArrayIndexOf } from './utils' +import { RDFArrayRemove } from './util' +import { + isRDFObject, + isStore, + isTFGraph, + isTFPredicate, + isTFStatement, + isTFSubject +} from './utils/terms' import Node from './node' import Variable from './variable' import { Query, indexedFormulaQuery } from './query' +import UpdateManager from './update-manager' +import { Bindings, TFTerm, TFPredicate, TFSubject, TFObject, TFGraph, TFQuad, TFNamedNode, TFBlankNode } from './types' +import Statement from './statement' +import { Indexable } from './data-factory-type' +import NamedNode from './named-node' +import Collection from './collection' +import Fetcher from './fetcher' const owlNamespaceURI = 'http://www.w3.org/2002/07/owl#' +type FeaturesType = Array<('sameAs' | 'InverseFunctionalProperty' | 'FunctionalProperty')> | undefined + export { defaultGraphURI } // var link_ns = 'http://www.w3.org/2007/ont/link#' // Handle Functional Property -function handleFP (formula, subj, pred, obj) { +function handleFP ( + formula: IndexedFormula, + subj: TFSubject, + pred: TFPredicate, + obj: TFObject +): boolean { var o1 = formula.any(subj, pred, undefined) if (!o1) { return false // First time with this value @@ -44,7 +65,12 @@ function handleFP (formula, subj, pred, obj) { } // handleFP // Handle Inverse Functional Property -function handleIFP (formula, subj, pred, obj) { +function handleIFP ( + formula: IndexedFormula, + subj: TFSubject, + pred: TFPredicate, + obj: TFObject +): boolean { var s1 = formula.any(undefined, pred, obj) if (!s1) { return false // First time with this value @@ -54,9 +80,16 @@ function handleIFP (formula, subj, pred, obj) { return true } // handleIFP -function handleRDFType (formula, subj, pred, obj, why) { +function handleRDFType ( + formula: IndexedFormula, + subj: TFSubject, + pred: TFPredicate, + obj: TFObject, + why: TFGraph +) { + //@ts-ignore this method does not seem to exist in this library if (formula.typeCallback) { - formula.typeCallback(formula, obj, why) + (formula as any).typeCallback(formula, obj, why) } var x = formula.classActions[formula.id(obj)] @@ -68,32 +101,76 @@ function handleRDFType (formula, subj, pred, obj, why) { } return done // statement given is not needed if true } + /** * Indexed Formula aka Store */ export default class IndexedFormula extends Formula { // IN future - allow pass array of statements to constructor /** - * @constructor - * @param {Array} features - What sort of autmatic processing to do? Array of string - * @param {Boolean} features.sameAs - Smush together A and B nodes whenever { A sameAs B } + * An UpdateManager initialised to this store + */ + updater?: UpdateManager + + /** + * Dictionary of namespace prefixes + */ + namespaces: {[key: string]: string} + + /** Map of iri predicates to functions to call when adding { s type X } */ + classActions: { [k: string]: Function[] } + /** Map of iri predicates to functions to call when getting statement with {s X o} */ + propertyActions: { [k: string]: Function[] } + /** Redirect to lexically smaller equivalent symbol */ + redirections: any[] + /** Reverse mapping to redirection: aliases for this */ + aliases: any[] + /** Redirections we got from HTTP */ + HTTPRedirects: TFQuad[] + /** Array of statements with this X as subject */ + subjectIndex: TFQuad[] + /** Array of statements with this X as predicate */ + predicateIndex: TFQuad[] + /** Array of statements with this X as object */ + objectIndex: TFQuad[] + /** Array of statements with X as provenance */ + whyIndex: TFQuad[] + index: [ + TFQuad[], + TFQuad[], + TFQuad[], + TFQuad[] + ] + features: FeaturesType + static handleRDFType: Function + _universalVariables?: TFNamedNode[] + _existentialVariables?: TFBlankNode[] + + /** Function to remove quads from the store arrays with */ + private rdfArrayRemove: (arr: TFQuad[], q: TFQuad) => void + /** Callbacks which are triggered after a statement has been added to the store */ + private dataCallbacks?: Array<(q: TFQuad) => void> + + /** + * Creates a new formula + * @param features - What sort of autmatic processing to do? Array of string + * @param features.sameAs - Smush together A and B nodes whenever { A sameAs B } * @param opts - * @param {DataFactory} [opts.rdfFactory] - The data factory that should be used by the store - * @param {DataFactory} [opts.rdfArrayRemove] - Function which removes statements from the store - * @param {DataFactory} [opts.dataCallback] - Callback when a statement is added to the store, will not trigger when adding duplicates + * @param [opts.rdfFactory] - The data factory that should be used by the store + * @param [opts.rdfArrayRemove] - Function which removes statements from the store + * @param [opts.dataCallback] - Callback when a statement is added to the store, will not trigger when adding duplicates */ - constructor (features, opts = {}) { + constructor (features?: FeaturesType, opts: FormulaOpts = {}) { super(undefined, undefined, undefined, undefined, opts) - this.propertyActions = [] // Array of functions to call when getting statement with {s X o} - // maps to [f(F,s,p,o),...] - this.classActions = [] // Array of functions to call when adding { s type X } - this.redirections = [] // redirect to lexically smaller equivalent symbol - this.aliases = [] // reverse mapping to redirection: aliases for this - this.HTTPRedirects = [] // redirections we got from HTTP - this.subjectIndex = [] // Array of statements with this X as subject - this.predicateIndex = [] // Array of statements with this X as subject - this.objectIndex = [] // Array of statements with this X as object - this.whyIndex = [] // Array of statements with X as provenance + this.propertyActions = {} + this.classActions = {} + this.redirections = [] + this.aliases = [] + this.HTTPRedirects = [] + this.subjectIndex = [] + this.predicateIndex = [] + this.objectIndex = [] + this.whyIndex = [] this.index = [ this.subjectIndex, this.predicateIndex, @@ -114,41 +191,68 @@ export default class IndexedFormula extends Formula { // IN future - allow pass this.initPropertyActions(this.features) } - static get defaultGraphURI () { + /** + * Gets the URI of the default graph + */ + static get defaultGraphURI(): string { return defaultGraphURI } - substitute (bindings) { - var statementsCopy = this.statements.map(function (ea) { - return ea.substitute(bindings) + /** + * Gets this graph with the bindings substituted + * @param bindings The bindings + */ + //@ts-ignore different from signature in Formula + substitute(bindings: Bindings): IndexedFormula { + var statementsCopy = this.statements.map(function (ea: TFQuad) { + return (ea as Statement).substitute(bindings) }) var y = new IndexedFormula() y.add(statementsCopy) return y } - addDataCallback(cb) { + /** + * Add a callback which will be triggered after a statement has been added to the store. + * @param cb + */ + addDataCallback(cb: (q: TFQuad) => void): void { if (!this.dataCallbacks) { this.dataCallbacks = [] } this.dataCallbacks.push(cb) } - applyPatch (patch, target, patchCallback) { // patchCallback(err) + /** + * Apply a set of statements to be deleted and to be inserted + * + * @param patch - The set of statements to be deleted and to be inserted + * @param target - The name of the document to patch + * @param patchCallback - Callback to be called when patching is complete + */ + applyPatch( + patch: { + delete?: ReadonlyArray, + patch?: ReadonlyArray, + where?: any + }, + target: TFNamedNode, + patchCallback: (errorString: string) => void + ): void { var targetKB = this var ds - var binding = null + var binding: Bindings | null = null - function doPatch (onDonePatch) { + function doPatch (onDonePatch: Function) { if (patch['delete']) { ds = patch['delete'] // console.log(bindingDebug(binding)) // console.log('ds before substitute: ' + ds) if (binding) ds = ds.substitute(binding) // console.log('applyPatch: delete: ' + ds) - ds = ds.statements - var bad = [] - var ds2 = ds.map(function (st) { // Find the actual statemnts in the store + ds = ds.statements as Statement[] + var bad: TFQuad[] = [] + var ds2 = ds.map(function (st: TFQuad) { // Find the actual statemnts in the store var sts = targetKB.statementsMatching(st.subject, st.predicate, st.object, target) if (sts.length === 0) { // log.info("NOT FOUND deletable " + st) @@ -164,7 +268,7 @@ export default class IndexedFormula extends Formula { // IN future - allow pass // console.log('despite ' + targetKB.statementsMatching(bad[0].subject, bad[0].predicate)[0]) return patchCallback('Could not find to delete: ' + bad.join('\n or ')) } - ds2.map(function (st) { + ds2.map(function (st: TFQuad) { targetKB.remove(st) }) } @@ -173,9 +277,9 @@ export default class IndexedFormula extends Formula { // IN future - allow pass ds = patch['insert'] if (binding) ds = ds.substitute(binding) ds = ds.statements - ds.map(function (st) { - st.why = target - targetKB.add(st.subject, st.predicate, st.object, st.why) + ds.map(function (st: TFQuad) { + st.graph = target + targetKB.add(st.subject, st.predicate, st.object, st.graph) }) } onDonePatch() @@ -185,13 +289,16 @@ export default class IndexedFormula extends Formula { // IN future - allow pass var query = new Query('patch') query.pat = patch.where query.pat.statements.map(function (st) { - st.why = target + st.graph = target }) + //@ts-ignore TODO: add sync property to Query when converting Query to typescript query.sync = true - var bindingsFound = [] + var bindingsFound: Bindings[] = [] - targetKB.query(query, function onBinding (binding) { + targetKB.query( + query, + function onBinding (binding) { bindingsFound.push(binding) // console.log(' got a binding: ' + bindingDebug(binding)) }, @@ -211,13 +318,21 @@ export default class IndexedFormula extends Formula { // IN future - allow pass } } - declareExistential (x) { + /** + * N3 allows for declaring blank nodes, this function enables that support + * + * @param x The blank node to be declared, supported in N3 + */ + declareExistential(x: TFBlankNode): TFBlankNode { if (!this._existentialVariables) this._existentialVariables = [] this._existentialVariables.push(x) return x } - initPropertyActions (features) { + /** + * @param features + */ + initPropertyActions(features: FeaturesType) { // If the predicate is #type, use handleRDFType to create a typeCallback on the object this.propertyActions[this.rdfFactory.id(this.rdfFactory.namedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'))] = [ handleRDFType ] @@ -250,44 +365,68 @@ export default class IndexedFormula extends Formula { // IN future - allow pass } /** @deprecated Use {add} instead */ - addStatement (st) { - return this.add(st.subject, st.predicate, st.object, st.graph) + addStatement (st: TFQuad): number { + this.add(st.subject, st.predicate, st.object, st.graph) + return this.statements.length } /** * Adds a triple (quad) to the store. * - * @param {Term} subject - The thing about which the fact a relationship is asserted - * @param {namedNode} predicate - The relationship which is asserted - * @param {Term} object - The object of the relationship, e.g. another thing or avalue - * @param {namedNode} why - The document in which the triple (S,P,O) was or will be stored on the web - * @returns {Statement} The statement added to the store + * @param subj - The thing about which the fact a relationship is asserted. + * Also accepts a statement or an array of Statements. + * @param pred - The relationship which is asserted + * @param obj - The object of the relationship, e.g. another thing or avalue + * @param why - The document in which the triple (S,P,O) was or will be stored on the web + * @returns The statement added to the store, or the store */ - add (subj, pred, obj, why) { - var i + // @ts-ignore differs from signature in Formula + add ( + subj: TFSubject | TFQuad | TFQuad[] | Statement | Statement[], + pred?: TFPredicate, + obj?: TFObject | Collection, + why?: TFGraph + ): TFQuad | null | IndexedFormula { + var i: number if (arguments.length === 1) { if (subj instanceof Array) { for (i = 0; i < subj.length; i++) { this.add(subj[i]) } - } else if (isStatement(subj)) { - this.add(subj.subject, subj.predicate, subj.object, subj.why) + } else if (isTFStatement(subj)) { + this.add(subj.subject, subj.predicate, subj.object, subj.graph) } else if (isStore(subj)) { this.add(subj.statements) } return this } - var actions - var st + var actions: Function[] + var st: TFQuad if (!why) { // system generated - why = this.fetcher ? this.fetcher.appNode : this.defaultGraph() + why = this.fetcher ? this.fetcher.appNode : this.rdfFactory.defaultGraph() + } + if (typeof subj == 'string') { + subj = this.rdfFactory.namedNode(subj) } - subj = Node.fromValue(subj) pred = Node.fromValue(pred) obj = Node.fromValue(obj) why = Node.fromValue(why) + if (!isTFSubject(subj)) { + throw new Error('Subject is not a subject type') + } + if (!isTFPredicate(pred)) { + throw new Error(`Predicate ${pred} is not a predicate type`) + } + if (!isRDFObject(obj)) { + throw new Error(`Object ${obj} is not an object type`) + } + if (!isTFGraph(why)) { + throw new Error("Why is not a graph type") + } + //@ts-ignore This is not used internally if (this.predicateCallback) { + //@ts-ignore This is not used internally this.predicateCallback(this, pred, why) } // Action return true if the statement does not need to be added @@ -309,11 +448,12 @@ export default class IndexedFormula extends Formula { // IN future - allow pass // Don't put it in the store // still return this statement for owl:sameAs input var hash = [ - this.id(this.canon(subj)), + this.id(this.canon(subj as TFSubject)), predHash, - this.id(this.canon(obj)), - this.id(this.canon(why)) + this.id(this.canon(obj as TFObject)), + this.id(this.canon(why as TFGraph)) ] + // @ts-ignore this will fail if you pass a collection and the factory does not allow Collections st = this.rdfFactory.quad(subj, pred, obj, why) for (i = 0; i < 4; i++) { var ix = this.index[i] @@ -338,8 +478,9 @@ export default class IndexedFormula extends Formula { // IN future - allow pass /** * Returns the symbol with canonical URI as smushed + * @param term - An RDF node */ - canon (term) { + canon(term: TFTerm): TFTerm { if (!term) { return term } @@ -350,12 +491,17 @@ export default class IndexedFormula extends Formula { // IN future - allow pass return y } - check () { + + /** + * Checks this formula for consistency + */ + check(): void { this.checkStatementList(this.statements) for (var p = 0; p < 4; p++) { var ix = this.index[p] for (var key in ix) { if (ix.hasOwnProperty(key)) { + // @ts-ignore should this pass an array or a single statement? checkStateMentsList expects an array. this.checkStatementList(ix[key], p) } } @@ -363,28 +509,35 @@ export default class IndexedFormula extends Formula { // IN future - allow pass } /** - * Self-consistency checking for diagnostis only - * Is each statement properly indexed? + * Checks a list of statements for consistency + * @param sts - The list of statements to check + * @param from - An index with the array ['subject', 'predicate', 'object', 'why'] */ - checkStatementList (sts, from) { + checkStatementList( + sts: ReadonlyArray, + from?: number + ): boolean | void { + if (from === undefined) { + from = 0 + } var names = ['subject', 'predicate', 'object', 'why'] var origin = ' found in ' + names[from] + ' index.' - var st + var st: TFQuad for (var j = 0; j < sts.length; j++) { st = sts[j] - var term = [ st.subject, st.predicate, st.object, st.why ] - var arrayContains = function (a, x) { + var term = [ st.subject, st.predicate, st.object, st.graph ] + var arrayContains = function (a: Array, x: TFQuad) { for (var i = 0; i < a.length; i++) { if (a[i].subject.equals(x.subject) && a[i].predicate.equals(x.predicate) && a[i].object.equals(x.object) && - a[i].why.equals(x.why)) { + a[i].why.equals(x.graph)) { return true } } } for (var p = 0; p < 4; p++) { - var c = this.canon(term[p]) + var c = this.canon(term[p]) as TFNamedNode var h = this.id(c) if (!this.index[p][h]) { // throw new Error('No ' + name[p] + ' index for statement ' + st + '@' + st.why + origin) @@ -395,21 +548,24 @@ export default class IndexedFormula extends Formula { // IN future - allow pass } } if (!arrayContains(this.statements, st)) { - throw new Error('Statement list does not statement ' + st + '@' + st.why + origin) + throw new Error('Statement list does not statement ' + st + '@' + st.graph + origin) } } } - close () { + /** + * Closes this formula (and return it) + */ + close(): IndexedFormula { return this } - compareTerm(u1, u2) { + // @ts-ignore incompatible with Forumala.compareTerm + compareTerm(u1: TFTerm, u2: TFTerm): number { // Keep compatibility with downstream classOrder changes if (Object.prototype.hasOwnProperty.call(u1, "compareTerm")) { - return u1.compareTerm(u2) + return (u1 as Node).compareTerm(u2 as Node) } - if (ClassOrder[u1.termType] < ClassOrder[u2.termType]) { return -1 } @@ -425,13 +581,19 @@ export default class IndexedFormula extends Formula { // IN future - allow pass return 0 } + /** - * replaces @template with @target and add appropriate triples (no triple - * removed) - * one-direction replication - * @method copyTo + * replaces @template with @target and add appropriate triples + * removes no triples by default and is a one-direction replication + * @param template node to copy + * @param target node to copy to + * @param flags Whether or not to do a two-directional copy and/or delete triples */ - copyTo (template, target, flags) { + copyTo( + template: TFSubject, + target: TFSubject, + flags?: Array<('two-direction' | 'delete')> + ): void { if (!flags) flags = [] var statList = this.statementsMatching(template) if (ArrayIndexOf(flags, 'two-direction') !== -1) { @@ -445,7 +607,9 @@ export default class IndexedFormula extends Formula { // IN future - allow pass break case 'Literal': case 'BlankNode': + // @ts-ignore Collections can appear here case 'Collection': + // @ts-ignore Possible bug: copy is not available on Collections this.add(target, st.predicate, st.object.copy(this)) } if (ArrayIndexOf(flags, 'delete') !== -1) { @@ -455,15 +619,17 @@ export default class IndexedFormula extends Formula { // IN future - allow pass } /** - * simplify graph in store when we realize two identifiers are equivalent + * Simplify graph in store when we realize two identifiers are equivalent * We replace the bigger with the smaller. + * @param u1in The first node + * @param u2in The second node */ - equate (u1, u2) { + equate(u1in: TFTerm, u2in : TFTerm): boolean { // log.warn("Equating "+u1+" and "+u2); // @@ // @@JAMBO Must canonicalize the uris to prevent errors from a=b=c // 03-21-2010 - u1 = this.canon(u1) - u2 = this.canon(u2) + const u1 = this.canon(u1in) as TFSubject + const u2 = this.canon(u2in) as TFSubject var d = this.compareTerm(u1, u2) if (!d) { return true // No information in {a = a} @@ -477,7 +643,13 @@ export default class IndexedFormula extends Formula { // IN future - allow pass } } - formula (features) { + /** + * Creates a new empty indexed formula + * Only applicable for IndexedFormula, but TypeScript won't allow a subclass to override a property + * @param features The list of features + */ + //@ts-ignore Incompatible signature with Formula.formula + formula(features: FeaturesType): IndexedFormula { return new IndexedFormula(features) } @@ -491,7 +663,7 @@ export default class IndexedFormula extends Formula { // IN future - allow pass * ``` * @returns {Number} */ - get length () { + get length (): number { return this.statements.length } @@ -499,13 +671,17 @@ export default class IndexedFormula extends Formula { // IN future - allow pass * Returns any quads matching the given arguments. * Standard RDFJS Taskforce method for Source objects, implemented as an * alias to `statementsMatching()` - * @method match - * @param subject {Node|String|Object} - * @param predicate {Node|String|Object} - * @param object {Node|String|Object} - * @param graph {NamedNode|String} + * @param subject The subject + * @param predicate The predicate + * @param object The object + * @param graph The graph that contains the statement */ - match (subject, predicate, object, graph) { + match( + subject?: TFSubject | null, + predicate?: TFPredicate | null, + object?: TFObject | null, + graph?: TFGraph | null + ): TFQuad[] { return this.statementsMatching( Node.fromValue(subject), Node.fromValue(predicate), @@ -516,22 +692,40 @@ export default class IndexedFormula extends Formula { // IN future - allow pass /** * Find out whether a given URI is used as symbol in the formula + * @param uri The URI to look for */ - mentionsURI (uri) { + mentionsURI(uri: string): boolean { var hash = '<' + uri + '>' return (!!this.subjectIndex[hash] || !!this.objectIndex[hash] || !!this.predicateIndex[hash]) } - // Existentials are BNodes - something exists without naming - newExistential (uri) { + /** + * Existentials are BNodes - something exists without naming + * @param uri An URI + */ + newExistential(uri: string): TFTerm { if (!uri) return this.bnode() var x = this.sym(uri) + // @ts-ignore x should be blanknode, but is namedNode. return this.declareExistential(x) } - newPropertyAction (pred, action) { + /** + * Adds a new property action + * @param pred the predicate that the function should be triggered on + * @param action the function that should trigger + */ + newPropertyAction( + pred: TFPredicate, + action: ( + store: IndexedFormula, + subject: TFSubject, + predicate: TFPredicate, + object: TFObject + ) => boolean + ): boolean { // log.debug("newPropertyAction: "+pred) var hash = this.id(pred) if (!this.propertyActions[hash]) { @@ -547,8 +741,12 @@ export default class IndexedFormula extends Formula { // IN future - allow pass return done } - // Universals are Variables - newUniversal (uri) { + /** + * Creates a new universal node + * Universals are Variables + * @param uri An URI + */ + newUniversal(uri: string): TFNamedNode { var x = this.sym(uri) if (!this._universalVariables) this._universalVariables = [] this._universalVariables.push(x) @@ -556,39 +754,55 @@ export default class IndexedFormula extends Formula { // IN future - allow pass } // convenience function used by N3 parser - variable (name) { + // @ts-ignore does not correctly extends from Formula + variable (name: string) { return new Variable(name) } /** * Find an unused id for a file being edited: return a symbol * (Note: Slow iff a lot of them -- could be O(log(k)) ) + * @param doc A document named node */ - nextSymbol (doc) { + nextSymbol(doc: TFNamedNode): TFNamedNode { for (var i = 0; ;i++) { - var uri = doc.uri + '#n' + i + var uri = doc.value + '#n' + i if (!this.mentionsURI(uri)) return this.sym(uri) } } - query (myQuery, callback, dummy, onDone) { + /** + * Query this store asynchronously, return bindings in callback + * + * @param myQuery The query to be run + * @param callback Function to call when bindings + * @param dummy OBSOLETE - do not use this + * @param onDone OBSOLETE - do not use this + */ + query( + myQuery: Query, + callback: (bindings: Bindings) => void, + dummy?: Fetcher | null, + onDone?: () => void + ): void { return indexedFormulaQuery.call(this, myQuery, callback, dummy, onDone) } -/** Query this store synchhronously and return bindings - * - * @param myQuery {Query} - the query object - * @returns {Array} - */ - querySync (myQuery) { - function saveBinginds (bindings) { + /** + * Query this store synchronously and return bindings + * + * @param myQuery The query to be run + */ + querySync(myQuery: Query): any[] { + var results: Bindings[] = [] + function saveBinginds (bindings: Bindings) { results.push(bindings) } function onDone () { done = true } - var results = [] var done = false + // @ts-ignore TODO: Add .sync to Query myQuery.sync = true indexedFormulaQuery.call(this, myQuery, saveBinginds, null, onDone) @@ -599,9 +813,10 @@ export default class IndexedFormula extends Formula { // IN future - allow pass } /** - * Finds a statement object and removes it + * Removes one or multiple statement(s) from this formula + * @param st - A Statement or array of Statements to remove */ - remove (st) { + remove(st: TFQuad | TFQuad[]): IndexedFormula { if (st instanceof Array) { for (var i = 0; i < st.length; i++) { this.remove(st[i]) @@ -611,8 +826,7 @@ export default class IndexedFormula extends Formula { // IN future - allow pass if (isStore(st)) { return this.remove(st.statements) } - var sts = this.statementsMatching(st.subject, st.predicate, st.object, - st.why) + var sts = this.statementsMatching(st.subject, st.predicate, st.object, st.graph) if (!sts.length) { throw new Error('Statement to be removed is not on store: ' + st) } @@ -622,9 +836,10 @@ export default class IndexedFormula extends Formula { // IN future - allow pass /** * Removes all statemnts in a doc + * @param doc - The document / graph */ - removeDocument (doc) { - var sts = this.statementsMatching(undefined, undefined, undefined, doc).slice() // Take a copy as this is the actual index + removeDocument(doc: TFGraph): IndexedFormula { + var sts: TFQuad[] = this.statementsMatching(undefined, undefined, undefined, doc).slice() // Take a copy as this is the actual index for (var i = 0; i < sts.length; i++) { this.removeStatement(sts[i]) } @@ -632,37 +847,61 @@ export default class IndexedFormula extends Formula { // IN future - allow pass } /** - * remove all statements matching args (within limit) * + * Remove all statements matching args (within limit) * + * @param subj The subject + * @param pred The predicate + * @param obj The object + * @param why The graph that contains the statement + * @param limit The number of statements to remove */ - removeMany (subj, pred, obj, why, limit) { + removeMany( + subj?: TFSubject | null, + pred?: TFPredicate | null, + obj?: TFObject | null, + why?: TFGraph | null, + limit?: number + ): void { // log.debug("entering removeMany w/ subj,pred,obj,why,limit = " + subj +", "+ pred+", " + obj+", " + why+", " + limit) var sts = this.statementsMatching(subj, pred, obj, why, false) // This is a subtle bug that occcured in updateCenter.js too. // The fact is, this.statementsMatching returns this.whyIndex instead of a copy of it // but for perfromance consideration, it's better to just do that // so make a copy here. - var statements = [] + var statements: TFQuad[] = [] for (var i = 0; i < sts.length; i++) statements.push(sts[i]) if (limit) statements = statements.slice(0, limit) for (i = 0; i < statements.length; i++) this.remove(statements[i]) } - removeMatches (subject, predicate, object, why) { - this.removeStatements(this.statementsMatching(subject, predicate, object, - why)) + /** + * Remove all matching statements + * @param subject The subject + * @param predicate The predicate + * @param object The object + * @param graph The graph that contains the statement + */ + removeMatches( + subject?: TFSubject | null, + predicate?: TFPredicate | null, + object?: TFObject | null, + graph?: TFGraph | null + ): IndexedFormula { + this.removeStatements( + this.statementsMatching(subject, predicate, object, graph) + ) return this } /** * Remove a particular statement object from the store * - * st a statement which is already in the store and indexed. - * Make sure you only use this for these. - * Otherwise, you should use remove() above. + * @param st - a statement which is already in the store and indexed. + * Make sure you only use this for these. + * Otherwise, you should use remove() above. */ - removeStatement (st) { + removeStatement(st: TFQuad): IndexedFormula { // log.debug("entering remove w/ st=" + st) - var term = [ st.subject, st.predicate, st.object, st.why ] + var term = [ st.subject, st.predicate, st.object, st.graph ] for (var p = 0; p < 4; p++) { var c = this.canon(term[p]) var h = this.id(c) @@ -676,7 +915,11 @@ export default class IndexedFormula extends Formula { // IN future - allow pass return this } - removeStatements (sts) { + /** + * Removes statements + * @param sts The statements to remove + */ + removeStatements(sts: ReadonlyArray): IndexedFormula { for (var i = 0; i < sts.length; i++) { this.remove(sts[i]) } @@ -686,7 +929,7 @@ export default class IndexedFormula extends Formula { // IN future - allow pass /** * Replace big with small, obsoleted with obsoleting. */ - replaceWith (big, small) { + replaceWith (big: TFSubject, small: TFSubject): boolean { // log.debug("Replacing "+big+" with "+small) // this.id(@@ var oldhash = this.id(big) var newhash = this.id(small) @@ -708,7 +951,7 @@ export default class IndexedFormula extends Formula { // IN future - allow pass moveIndex(this.index[i]) } this.redirections[oldhash] = small - if (big.uri) { + if (big.value) { // @@JAMBO: must update redirections,aliases from sub-items, too. if (!this.aliases[newhash]) { this.aliases[newhash] = [] @@ -720,7 +963,7 @@ export default class IndexedFormula extends Formula { // IN future - allow pass this.aliases[newhash].push(this.aliases[oldhash][i]) } } - this.add(small, this.sym('http://www.w3.org/2007/ont/link#uri'), big.uri) + this.add(small, this.sym('http://www.w3.org/2007/ont/link#uri'), big) // If two things are equal, and one is requested, we should request the other. if (this.fetcher) { this.fetcher.nowKnownAs(big, small) @@ -734,8 +977,9 @@ export default class IndexedFormula extends Formula { // IN future - allow pass /** * Return all equivalent URIs by which this is known + * @param x A named node */ - allAliases (x) { + allAliases(x: NamedNode): NamedNode[] { var a = this.aliases[this.id(this.canon(x))] || [] a.push(this.canon(x)) return a @@ -743,8 +987,10 @@ export default class IndexedFormula extends Formula { // IN future - allow pass /** * Compare by canonical URI as smushed + * @param x A named node + * @param y Another named node */ - sameThings (x, y) { + sameThings(x: NamedNode, y: NamedNode): boolean { if (x.equals(y)) { return true } @@ -754,10 +1000,10 @@ export default class IndexedFormula extends Formula { // IN future - allow pass var y1 = this.canon(y) // alert('y1='+y1); //@@ if (!y1) return false - return (x1.uri === y1.uri) + return (x1.value === y1.value) } - setPrefixForURI (prefix, nsuri) { + setPrefixForURI (prefix: string, nsuri: string): void { // TODO: This is a hack for our own issues, which ought to be fixed // post-release // See http://dig.csail.mit.edu/cgi-bin/roundup.cgi/$rdf/issue227 @@ -773,21 +1019,27 @@ export default class IndexedFormula extends Formula { // IN future - allow pass /** Search the Store * * ALL CONVENIENCE LOOKUP FUNCTIONS RELY ON THIS! - * @param {Node} subject - A node to search for as subject, or if null, a wildcard - * @param {Node} predicate - A node to search for as predicate, or if null, a wildcard - * @param {Node} object - A node to search for as object, or if null, a wildcard - * @param {Node} graph - A node to search for as graph, or if null, a wildcard - * @param {Boolean} justOne - flag - stop when found one rather than get all of them? - * @returns {Array} - An array of nodes which match the wildcard position - */ - statementsMatching (subj, pred, obj, why, justOne) { + * @param subj - A node to search for as subject, or if null, a wildcard + * @param pred - A node to search for as predicate, or if null, a wildcard + * @param obj - A node to search for as object, or if null, a wildcard + * @param why - A node to search for as graph, or if null, a wildcard + * @param justOne - flag - stop when found one rather than get all of them? + * @returns An array of nodes which match the wildcard position + */ + statementsMatching ( + subj?: TFSubject | null, + pred?: TFPredicate | null, + obj?: TFObject | null, + why?: TFGraph | null, + justOne?: boolean + ): TFQuad[] { // log.debug("Matching {"+subj+" "+pred+" "+obj+"}") var pat = [ subj, pred, obj, why ] - var pattern = [] - var hash = [] - var wild = [] // wildcards - var given = [] // Not wild - var p + var pattern: TFTerm[] = [] + var hash: Indexable[] = [] + var wild: number[] = [] // wildcards + var given: number[] = [] // Not wild + var p: number var list for (p = 0; p < 4; p++) { pattern[p] = this.canon(Node.fromValue(pat[p])) @@ -831,12 +1083,12 @@ export default class IndexedFormula extends Formula { // IN future - allow pass } // Ok, we have picked the shortest index but now we have to filter it var pBest = given[iBest] - var possibles = this.index[pBest][hash[pBest]] + var possibles: TFQuad[] = this.index[pBest][hash[pBest]] var check = given.slice(0, iBest).concat(given.slice(iBest + 1)) // remove iBest - var results = [] + var results: TFQuad[] = [] var parts = [ 'subject', 'predicate', 'object', 'why' ] for (var j = 0; j < possibles.length; j++) { - var st = possibles[j] + var st: TFQuad | null = possibles[j] for (i = 0; i < check.length; i++) { // for each position to be checked p = check[i] @@ -854,13 +1106,14 @@ export default class IndexedFormula extends Formula { // IN future - allow pass } /** - * A list of all the URIs by which this thing is known + * A list of all the URIs by which this thing is known + * @param term */ - uris (term) { + uris(term: TFSubject): string[] { var cterm = this.canon(term) var terms = this.aliases[this.id(cterm)] - if (!cterm.uri) return [] - var res = [ cterm.uri ] + if (!cterm.value) return [] + var res = [ cterm.value ] if (terms) { for (var i = 0; i < terms.length; i++) { res.push(terms[i].uri) diff --git a/src/types.ts b/src/types.ts index 1f6b770be..e65b55f3b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,19 +1,78 @@ +import Node from './node-internal' +import Variable from './variable' +import BlankNode from './blank-node' +import Collection from './collection' +import Literal from './literal' +import NamedNode from './named-node' +import DefaultGraph from './default-graph' +import { SupportTable } from './data-factory-type' + +/** + * Types that support both Enums (for typescript) and regular strings + */ +export type NamedNodeTermType = "NamedNode" | TermType.NamedNode +export type BlankNodeTermType = "BlankNode" | TermType.BlankNode +export type LiteralTermType = "Literal" | TermType.Literal +export type VariableTermType = "Variable" | TermType.Variable +export type CollectionTermType = "Collection" | TermType.Collection +export type DefaultGraphTermType = "DefaultGraph" | TermType.DefaultGraph + /** * All the possible TermTypes * @todo Convert these to const enums when it's supported https://github.com/babel/babel/issues/8741 */ -export const TermType = { - NamedNode: 'NamedNode', - BlankNode: 'BlankNode', - Literal: 'Literal', - Variable: 'Variable', - DefaultGraph: 'DefaultGraph', +export enum TermType { + NamedNode = 'NamedNode', + BlankNode = 'BlankNode', + Literal = 'Literal', + Variable = 'Variable', + DefaultGraph = 'DefaultGraph', // The next ones are not specified by the rdf.js taskforce - Collection: 'Collection', - Empty: 'Empty', - Graph: 'Graph', + Collection = 'Collection', + Empty = 'Empty', + Graph = 'Graph', +} + +/** + * A valid mime type header + * @todo Convert these to const enums when it's supported https://github.com/babel/babel/issues/8741 + */ +export enum ContentType { + rdfxml = "application/rdf+xml", + turtle = "text/turtle", + turtleLegacy = "application/x-turtle", + n3 = "text/n3", + n3Legacy = "application/n3", + nTriples = "application/n-triples", + nQuads = "application/n-quads", + nQuadsAlt = "application/nquads", + jsonld = "application/ld+json", + xhtml = "application/xhtml+xml", + html = "text/html", + sparqlupdate = "application/sparql-update", } +/** A type for values that serves as inputs */ +export type ValueType = TFTerm | Node | Date | string | number | boolean | undefined | null | Collection + +/** + * In this project, there exist two types for the same kind of RDF concept. + * We have RDF/JS Taskforce types (standardized, generic), and RDFlib types (internal, specific). + * When deciding which type to use in a function, it is preferable to accept generic inputs, + * whenever possible, and provide strict outputs. + * In some ways, the TF types in here are a bit more strict. + * Variables are missing, and the statement requires specific types of terms (e.g. NamedNode instead of Term). + */ + +/** An RDF/JS Subject */ +export type SubjectType = BlankNode | NamedNode | Variable +/** An RDF/JS Predicate */ +export type PredicateType = NamedNode | Variable +/** An RDF/JS Object */ +export type ObjectType = NamedNode | Literal | Collection | BlankNode | Variable // | Empty +/** An RDF/JS Graph */ +export type GraphType = DefaultGraph | NamedNode | Variable // | Formula + /** * RDF/JS taskforce Term * @link https://rdf.js.org/data-model-spec/#term-interface @@ -23,3 +82,166 @@ export interface TFTerm { value: string equals(other: TFTerm): boolean } + +/** + * RDF/JS taskforce NamedNode + * @link https://rdf.js.org/data-model-spec/#namednode-interface + */ +export interface TFNamedNode extends TFTerm { + termType: NamedNodeTermType + value: string + equals(other: TFTerm): boolean +} + +/** + * RDF/JS taskforce Literal + * @link https://rdf.js.org/data-model-spec/#literal-interface + */ +export interface TFBlankNode extends TFTerm { + termType: BlankNodeTermType + value: string + equals(other: TFTerm): boolean +} + +/** + * RDF/JS taskforce Quad + * @link https://rdf.js.org/data-model-spec/#quad-interface + */ +export interface TFQuad< + S extends TFSubject = TFSubject, + P extends TFPredicate = TFPredicate, + O extends TFTerm = TFObject, + G extends TFGraph = TFGraph +> { + subject: S + predicate: P + object: O + graph: G + equals(other: TFQuad): boolean +} + +/** + * RDF/JS taskforce Literal + * @link https://rdf.js.org/data-model-spec/#literal-interface + */ +export interface TFLiteral extends TFTerm { + /** Contains the constant "Literal". */ + termType: LiteralTermType + /** The text value, unescaped, without language or type (example: "Brad Pitt") */ + value: string + /** + * The language as lowercase BCP-47 [BCP47] string (examples: "en", "en-gb") + * or an empty string if the literal has no language. + */ + language: string + /** A NamedNode whose IRI represents the datatype of the literal. */ + datatype: TFNamedNode + equals(other: TFTerm): boolean +} + +/** + * RDF/JS taskforce Variable + * @link https://rdf.js.org/data-model-spec/#variable-interface + */ +export interface TFVariable extends TFTerm { + /** Contains the constant "Variable". */ + termType: VariableTermType + /** The name of the variable without leading "?" (example: "a"). */ + value: string + /** + * Returns true if all general Term.equals conditions hold and term.value + * is the same string as other.value; otherwise, it returns false. + */ + equals(other: TFTerm): boolean +} + +/** + * RDF/JS taskforce DefaultGraph + * An instance of DefaultGraph represents the default graph. + * It's only allowed to assign a DefaultGraph to the graph property of a Quad. + * @link https://rdf.js.org/data-model-spec/#defaultgraph-interface + */ +export interface TFDefaultGraph extends TFTerm { + termType: DefaultGraphTermType; + /** should return and empty string'' */ + value: string; + equals(other: TFTerm): boolean +} + +/** + * RDF/JS taskforce DataFactory + * @link https://rdf.js.org/data-model-spec/#datafactory-interface + */ +export interface TFDataFactory< + DFNamedNode extends TFNamedNode = TFNamedNode, + DFBlankNode extends TFBlankNode = TFBlankNode, + DFLiteral extends TFLiteral = TFLiteral, + DFSubject = TFSubject, + DFPredicate = TFPredicate, + DFObject = TFObject, + DFGraph = TFGraph, + DFDefaultGraph = TFDefaultGraph, + DFQuad = TFQuad, +> { + /** Returns a new instance of NamedNode. */ + namedNode: (value: string) => DFNamedNode, + /** + * Returns a new instance of BlankNode. + * If the value parameter is undefined a new identifier for the + * blank node is generated for each call. + */ + blankNode: (value?: string) => DFBlankNode, + /** + * Returns a new instance of Literal. + * If languageOrDatatype is a NamedNode, then it is used for the value of datatype. + * Otherwise languageOrDatatype is used for the value of language. */ + literal: (value: string, languageOrDatatype: string | DFNamedNode) => DFLiteral, + /** Returns a new instance of Variable. This method is optional. */ + variable?: (value: string) => TFVariable, + /** + * Returns an instance of DefaultGraph. + */ + defaultGraph: () => DFDefaultGraph, + /** + * Returns a new instance of the specific Term subclass given by original.termType + * (e.g., NamedNode, BlankNode, Literal, etc.), + * such that newObject.equals(original) returns true. + * Not implemented in RDFJS, so optional. + */ + fromTerm?: (original: TFTerm) => TFTerm + /** + * Returns a new instance of Quad, such that newObject.equals(original) returns true. + * Not implemented in RDFJS, so optional. + */ + fromQuad?: (original: DFQuad) => DFQuad + /** + * Returns a new instance of Quad. + * If graph is undefined or null it MUST set graph to a DefaultGraph. + */ + quad: ( + subject: DFSubject, + predicate: DFPredicate, + object: DFObject, + graph?: DFGraph, + ) => DFQuad + /** + * This does not exist on the original RDF/JS spec + */ + supports: SupportTable +} + +export interface Bindings { + [id: string]: TFTerm; +} + +/** A RDF/JS taskforce Subject */ +export type TFSubject = TFNamedNode | TFBlankNode | TFVariable +/** A RDF/JS taskforce Predicate */ +export type TFPredicate = TFNamedNode | TFVariable +/** A RDF/JS taskforce Object */ +export type TFObject = TFNamedNode | TFBlankNode | TFLiteral | TFVariable +/** A RDF/JS taskforce Graph */ +export type TFGraph = TFNamedNode | TFDefaultGraph | TFBlankNode | TFVariable + +/** All the types that a .fromValue() method might return */ +export type FromValueReturns = TFTerm | undefined | null | Collection diff --git a/src/update-manager.js b/src/update-manager.ts similarity index 76% rename from src/update-manager.js rename to src/update-manager.ts index 3d39aedcc..f98a040e7 100644 --- a/src/update-manager.js +++ b/src/update-manager.ts @@ -7,34 +7,53 @@ import IndexedFormula from './store' import { docpart } from './uri' import Fetcher from './fetcher' -import DataFactory from './data-factory' import Namespace from './namespace' import Serializer from './serializer' import { join as uriJoin } from './uri' -import { isStore } from './utils/terms' +import { isStore, isTFBlankNode, termValue } from './utils/terms' import * as Util from './util' +import Statement from './statement' +import { NamedNode } from './index' +import { TFNamedNode, TFQuad, TFBlankNode, TFSubject, TFPredicate, TFObject, TFGraph, TFTerm } from './types' -/** Update Manager -* -* The update manager is a helper object for a store. +interface UpdateManagerFormula extends IndexedFormula { + fetcher: Fetcher +} + +type CallBackFunction = (uri: string, ok: boolean, message: string, response: Error | Response) => {} | void + +/** +* The UpdateManager is a helper object for a store. * Just as a Fetcher provides the store with the ability to read and write, * the Update Manager provides functionality for making small patches in real time, * and also looking out for concurrent updates from other agents */ - export default class UpdateManager { - /** @constructor - * @param {IndexedFormula} store - the quadstore to store data and metadata. Created if not passed.f + + store: UpdateManagerFormula + + ifps: {} + + fps: {} + + /** Index of objects for coordinating incoming and outgoing patches */ + patchControl: [] + + /** Object of namespaces */ + ns: any + + /** + * @param store - The quadstore to store data and metadata. Created if not passed. */ - constructor (store) { - store = store || new IndexedFormula() // If none provided make a store - this.store = store + constructor (store?: IndexedFormula) { + store = store || new IndexedFormula() if (store.updater) { throw new Error("You can't have two UpdateManagers for the same store") } - if (!store.fetcher) { // The store must also/already have a fetcher - store.fetcher = new Fetcher(store) + if (!(store as UpdateManagerFormula).fetcher) { + (store as UpdateManagerFormula).fetcher = new Fetcher(store) } + this.store = store as UpdateManagerFormula store.updater = this this.ifps = {} this.fps = {} @@ -48,14 +67,14 @@ export default class UpdateManager { this.ns.rdf = Namespace('http://www.w3.org/1999/02/22-rdf-syntax-ns#') this.ns.owl = Namespace('http://www.w3.org/2002/07/owl#') - this.patchControl = [] // index of objects fro coordinating incomng and outgoing patches + this.patchControl = [] } - patchControlFor (doc) { - if (!this.patchControl[doc.uri]) { - this.patchControl[doc.uri] = [] + patchControlFor (doc: TFNamedNode) { + if (!this.patchControl[doc.value]) { + this.patchControl[doc.value] = [] } - return this.patchControl[doc.uri] + return this.patchControl[doc.value] } /** @@ -64,26 +83,23 @@ export default class UpdateManager { * for safety. * We don't actually check for write access on files. * - * @param uri {string} - * @param kb {IndexedFormula} - * - * @returns {string|boolean|undefined} The method string SPARQL or DAV or + * @returns The method string SPARQL or DAV or * LOCALFILE or false if known, undefined if not known. */ - editable (uri, kb) { + editable (uri: string | TFNamedNode, kb: IndexedFormula): string | boolean | undefined { if (!uri) { return false // Eg subject is bnode, no known doc to write to } if (!kb) { kb = this.store } - uri = uri.uri || uri // Allow Named Node to be passed + uri = termValue(uri) - if (uri.slice(0, 8) === 'file:///') { + if ((uri as string).slice(0, 8) === 'file:///') { if (kb.holds( - kb.sym(uri), - DataFactory.namedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'), - DataFactory.namedNode('http://www.w3.org/2007/ont/link#MachineEditableDocument'))) { + this.store.rdfFactory.namedNode(uri), + this.store.rdfFactory.namedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'), + this.store.rdfFactory.namedNode('http://www.w3.org/2007/ont/link#MachineEditableDocument'))) { return 'LOCALFILE' } @@ -91,7 +107,7 @@ export default class UpdateManager { console.log('UpdateManager.editable: Not MachineEditableDocument file ' + uri + '\n') - console.log(sts.map((x) => { return x.toNT() }).join('\n')) + console.log(sts.map((x) => { return (x as Statement).toNT() }).join('\n')) return false // @@ Would be nifty of course to see whether we actually have write access first. @@ -99,6 +115,7 @@ export default class UpdateManager { var request var definitive = false + // @ts-ignore passes a string to kb.each, which expects a term. Should this work? var requests = kb.each(undefined, this.ns.link('requestedURI'), docpart(uri)) // This if-statement does not follow the Solid spec, but temporarily reverts this change: @@ -110,14 +127,14 @@ export default class UpdateManager { // https://github.com/solid/node-solid-server/pull/1313 // Once that release has been published to the major Pod hosters, the commit that introduced // this statement should be reverted: - if (kb.holds(DataFactory.namedNode(uri), this.ns.rdf('type'), this.ns.ldp('Resource'))) { + if (kb.holds(this.store.rdfFactory.namedNode(uri), this.ns.rdf('type'), this.ns.ldp('Resource'))) { return 'SPARQL' } - var method + var method: string for (var r = 0; r < requests.length; r++) { request = requests[r] if (request !== undefined) { - var response = kb.any(request, this.ns.link('response')) + var response = kb.any(request, this.ns.link('response')) as TFSubject if (request !== undefined) { var wacAllow = kb.anyValue(response, this.ns.httph('wac-allow')) if (wacAllow) { @@ -151,6 +168,7 @@ export default class UpdateManager { var status = kb.each(response, this.ns.http('status')) if (status.length) { for (let i = 0; i < status.length; i++) { + // @ts-ignore since statuses should be TFTerms, this should always be false if (status[i] === 200 || status[i] === 404) { definitive = true // return false // A definitive answer @@ -179,7 +197,7 @@ export default class UpdateManager { : obj.toNT() } - anonymizeNT (stmt) { + anonymizeNT (stmt: TFQuad) { return this.anonymize(stmt.subject) + ' ' + this.anonymize(stmt.predicate) + ' ' + this.anonymize(stmt.object) + ' .' @@ -189,23 +207,23 @@ export default class UpdateManager { * Returns a list of all bnodes occurring in a statement * @private */ - statementBnodes (st) { + statementBnodes (st: TFQuad): TFBlankNode[] { return [st.subject, st.predicate, st.object].filter(function (x) { - return x.isBlank - }) + return isTFBlankNode(x) + }) as TFBlankNode[] } /** * Returns a list of all bnodes occurring in a list of statements * @private */ - statementArrayBnodes (sts) { - var bnodes = [] + statementArrayBnodes (sts: TFQuad[]) { + var bnodes: TFBlankNode[] = [] for (let i = 0; i < sts.length; i++) { bnodes = bnodes.concat(this.statementBnodes(sts[i])) } bnodes.sort() // in place sort - result may have duplicates - var bnodes2 = [] + var bnodes2: TFBlankNode[] = [] for (let j = 0; j < bnodes.length; j++) { if (j === 0 || !bnodes[j].equals(bnodes[j - 1])) { bnodes2.push(bnodes[j]) @@ -223,12 +241,12 @@ export default class UpdateManager { var a = this.store.each(undefined, this.ns.rdf('type'), this.ns.owl('InverseFunctionalProperty')) for (let i = 0; i < a.length; i++) { - this.ifps[a[i].uri] = true + this.ifps[a[i].value] = true } this.fps = {} a = this.store.each(undefined, this.ns.rdf('type'), this.ns.owl('FunctionalProperty')) for (let i = 0; i < a.length; i++) { - this.fps[a[i].uri] = true + this.fps[a[i].value] = true } } @@ -244,7 +262,7 @@ export default class UpdateManager { var y var res for (let i = 0; i < sts.length; i++) { - if (this.fps[sts[i].predicate.uri]) { + if (this.fps[sts[i].predicate.value]) { y = sts[i].subject if (!y.isBlank) { return [ sts[i] ] @@ -260,7 +278,7 @@ export default class UpdateManager { // outgoing links sts = this.store.statementsMatching(x, undefined, undefined, source) for (let i = 0; i < sts.length; i++) { - if (this.ifps[sts[i].predicate.uri]) { + if (this.ifps[sts[i].predicate.value]) { y = sts[i].object if (!y.isBlank) { return [ sts[i] ] @@ -296,9 +314,9 @@ export default class UpdateManager { * @private */ mentioned (x) { - return this.store.statementsMatching(x).length !== 0 || // Don't pin fresh bnodes - this.store.statementsMatching(undefined, x).length !== 0 || - this.store.statementsMatching(undefined, undefined, x).length !== 0 + return this.store.statementsMatching(x, null, null, null).length !== 0 || // Don't pin fresh bnodes + this.store.statementsMatching(null, x).length !== 0 || + this.store.statementsMatching(null, null, x).length !== 0 } /** @@ -321,9 +339,9 @@ export default class UpdateManager { * Returns the best context for a single statement * @private */ - statementContext (st) { + statementContext (st: TFQuad) { var bnodes = this.statementBnodes(st) - return this.bnodeContext(bnodes, st.why) + return this.bnodeContext(bnodes, st.graph) } /** @@ -342,7 +360,11 @@ export default class UpdateManager { /** * @private */ - fire (uri, query, callbackFunction) { + fire ( + uri: string, + query: string, + callbackFunction: CallBackFunction + ): Promise { return Promise.resolve() .then(() => { if (!uri) { @@ -369,7 +391,7 @@ export default class UpdateManager { console.log('UpdateManager: update Ok for <' + uri + '>') - callbackFunction(uri, response.ok, response.responseText, response) + callbackFunction(uri, response.ok, response.responseText as string, response) }) .catch(err => { callbackFunction(uri, false, err.message, err) @@ -384,15 +406,15 @@ export default class UpdateManager { * It returns an object which includes * function which can be used to change the object of the statement. */ - update_statement (statement) { - if (statement && !statement.why) { + update_statement (statement: TFQuad) { + if (statement && !statement.graph) { return } var updater = this var context = this.statementContext(statement) return { - statement: statement ? [statement.subject, statement.predicate, statement.object, statement.why] : undefined, + statement: statement ? [statement.subject, statement.predicate, statement.object, statement.graph] : undefined, statementNT: statement ? this.anonymizeNT(statement) : undefined, where: updater.contextWhere(context), @@ -400,16 +422,19 @@ export default class UpdateManager { var query = this.where query += 'DELETE DATA { ' + this.statementNT + ' } ;\n' query += 'INSERT DATA { ' + + // @ts-ignore `this` might refer to the wrong scope. Does this work? this.anonymize(this.statement[0]) + ' ' + + // @ts-ignore this.anonymize(this.statement[1]) + ' ' + + // @ts-ignore this.anonymize(obj) + ' ' + ' . }\n' - updater.fire(this.statement[3].uri, query, callbackFunction) + updater.fire((this.statement as [TFSubject, TFPredicate, TFObject, TFGraph])[3].value, query, callbackFunction) } } } - insert_statement (st, callbackFunction) { + insert_statement (st: TFQuad, callbackFunction: CallBackFunction): void { var st0 = st instanceof Array ? st[0] : st var query = this.contextWhere(this.statementContext(st0)) @@ -424,10 +449,10 @@ export default class UpdateManager { this.anonymize(st.object) + ' ' + ' . }\n' } - this.fire(st0.why.uri, query, callbackFunction) + this.fire(st0.graph.value, query, callbackFunction) } - delete_statement (st, callbackFunction) { + delete_statement (st: TFQuad | TFQuad[], callbackFunction: CallBackFunction): void { var st0 = st instanceof Array ? st[0] : st var query = this.contextWhere(this.statementContext(st0)) @@ -442,7 +467,7 @@ export default class UpdateManager { this.anonymize(st.object) + ' ' + ' . }\n' } - this.fire(st0.why.uri, query, callbackFunction) + this.fire(st0.graph.value, query, callbackFunction) } /// ////////////////////// @@ -456,7 +481,7 @@ export default class UpdateManager { * @param doc * @param action */ - requestDownstreamAction (doc, action) { + requestDownstreamAction (doc: TFNamedNode, action): void { var control = this.patchControlFor(doc) if (!control.pendingUpstream) { action(doc) @@ -475,27 +500,27 @@ export default class UpdateManager { * We want to start counting websocket notifications * to distinguish the ones from others from our own. */ - clearUpstreamCount (doc) { + clearUpstreamCount (doc: TFNamedNode): void { var control = this.patchControlFor(doc) control.upstreamCount = 0 } - getUpdatesVia (doc) { + getUpdatesVia (doc: TFNamedNode): string | null { var linkHeaders = this.store.fetcher.getHeader(doc, 'updates-via') if (!linkHeaders || !linkHeaders.length) return null return linkHeaders[0].trim() } - addDownstreamChangeListener (doc, listener) { + addDownstreamChangeListener (doc: TFNamedNode, listener): void { var control = this.patchControlFor(doc) if (!control.downstreamChangeListeners) { control.downstreamChangeListeners = [] } control.downstreamChangeListeners.push(listener) - this.setRefreshHandler(doc, (doc) => { + this.setRefreshHandler(doc, (doc: TFNamedNode) => { this.reloadAndSync(doc) }) } - reloadAndSync (doc) { + reloadAndSync (doc: TFNamedNode): void { var control = this.patchControlFor(doc) var updater = this @@ -524,14 +549,14 @@ export default class UpdateManager { } } else { control.reloading = false - if (response.status === 0) { + if ((response as Response).status === 0) { console.log('Network error refreshing the data. Retrying in ' + retryTimeout / 1000) control.reloading = true retryTimeout = retryTimeout * 2 setTimeout(tryReload, retryTimeout) } else { - console.log('Error ' + response.status + 'refreshing the data:' + + console.log('Error ' + (response as Response).status + 'refreshing the data:' + message + '. Stopped' + doc) } } @@ -557,8 +582,8 @@ export default class UpdateManager { * * @returns {boolean} */ - setRefreshHandler (doc, handler) { - var wssURI = this.getUpdatesVia(doc) // relative + setRefreshHandler (doc: TFNamedNode, handler): boolean { + let wssURI = this.getUpdatesVia(doc) // relative // var kb = this.store var theHandler = handler var self = this @@ -571,19 +596,21 @@ export default class UpdateManager { return false } - wssURI = uriJoin(wssURI, doc.uri) - wssURI = wssURI.replace(/^http:/, 'ws:').replace(/^https:/, 'wss:') + wssURI = uriJoin(wssURI, doc.value) + const validWssURI = wssURI.replace(/^http:/, 'ws:').replace(/^https:/, 'wss:') console.log('Web socket URI ' + wssURI) var openWebsocket = function () { // From https://github.com/solid/solid-spec#live-updates var socket if (typeof WebSocket !== 'undefined') { - socket = new WebSocket(wssURI) + socket = new WebSocket(validWssURI) + //@ts-ignore Firefox Addon } else if (typeof Services !== 'undefined') { // Firefox add on http://stackoverflow.com/questions/24244886/is-websocket-supported-in-firefox-for-android-addons + //@ts-ignore Firefox Addon socket = (Services.wm.getMostRecentWindow('navigator:browser').WebSocket)(wssURI) } else if (typeof window !== 'undefined' && window.WebSocket) { - socket = window.WebSocket(wssURI) + socket = (window as any).WebSocket(validWssURI) } else { console.log('Live update disabled, as WebSocket not supported by platform :-(') return @@ -591,7 +618,7 @@ export default class UpdateManager { socket.onopen = function () { console.log(' websocket open') retryTimeout = 1500 // reset timeout to fast on success - this.send('sub ' + doc.uri) + this.send('sub ' + doc.value) if (retries) { console.log('Web socket has been down, better check for any news.') updater.requestDownstreamAction(doc, theHandler) @@ -600,7 +627,7 @@ export default class UpdateManager { var control = self.patchControlFor(doc) control.upstreamCount = 0 - socket.onerror = function onerror (err) { + socket.onerror = function onerror (err: Error) { console.log('Error on Websocket:', err) } @@ -617,9 +644,9 @@ export default class UpdateManager { // 1006 CLOSE_ABNORMAL Reserved. Used to indicate that a connection was closed abnormally ( // // - socket.onclose = function (event) { + socket.onclose = function (event: CloseEvent) { console.log('*** Websocket closed with code ' + event.code + - ", reason '" + event.reason + "' clean = " + event.clean) + ", reason '" + event.reason + "' clean = " + event.wasClean) retryTimeout *= 2 retries += 1 console.log('Retrying in ' + retryTimeout + 'ms') // (ask user?) @@ -628,7 +655,7 @@ export default class UpdateManager { openWebsocket() }, retryTimeout) } - socket.onmessage = function (msg) { + socket.onmessage = function (msg: MessageEvent) { if (msg.data && msg.data.slice(0, 3) === 'pub') { if ('upstreamCount' in control) { control.upstreamCount -= 1 @@ -648,25 +675,27 @@ export default class UpdateManager { return true } - /** Update - * - * This high-level function updates the local store iff the web is changed - * successfully. - * - * Deletions, insertions may be undefined or single statements or lists or formulae - * (may contain bnodes which can be indirectly identified by a where clause). - * The `why` property of each statement must be the same and give the web document to be updated - * - * @param deletions - Statement or statments to be deleted. - * @param insertions - Statement or statements to be inserted - * - * @param callbackFunction {Function} called as callbackFunction(uri, success, errorbody) - * OR returns a promise - * - * @returns {*} + /** + * This high-level function updates the local store iff the web is changed successfully. + * Deletions, insertions may be undefined or single statements or lists or formulae (may contain bnodes which can be indirectly identified by a where clause). + * The `why` property of each statement must be the same and give the web document to be updated. + * @param deletions - Statement or statements to be deleted. + * @param insertions - Statement or statements to be inserted. + * @param callback - called as callbackFunction(uri, success, errorbody) + * OR returns a promise */ - update (deletions, insertions, callbackFunction, secondTry) { - if (!callbackFunction) { + update( + deletions: ReadonlyArray, + insertions: ReadonlyArray, + callback?: ( + uri: string | undefined | null, + success: boolean, + errorBody?: string, + response?: Response | Error + ) => void, + secondTry?: boolean + ): void | Promise { + if (!callback) { var thisUpdater = this return new Promise(function (resolve, reject) { // Promise version thisUpdater.update(deletions, insertions, function (uri, ok, errorBody) { @@ -694,9 +723,9 @@ export default class UpdateManager { throw new Error('Type Error ' + (typeof is) + ': ' + is) } if (ds.length === 0 && is.length === 0) { - return callbackFunction(null, true) // success -- nothing needed to be done. + return callback(null, true) // success -- nothing needed to be done. } - var doc = ds.length ? ds[0].why : is[0].why + var doc = ds.length ? ds[0].graph : is[0].graph if (!doc) { let message = 'Error patching: statement does not specify which document to patch:' + ds[0] + ', ' + is[0] console.log(message) @@ -709,10 +738,10 @@ export default class UpdateManager { var verbs = ['insert', 'delete'] var clauses = { 'delete': ds, 'insert': is } verbs.map(function (verb) { - clauses[verb].map(function (st) { - if (!doc.equals(st.why)) { + clauses[verb].map(function (st: TFQuad) { + if (!doc.equals(st.graph)) { throw new Error('update: destination ' + doc + - ' inconsistent with delete quad ' + st.why) + ' inconsistent with delete quad ' + st.graph) } props.map(function (prop) { if (typeof st[prop] === 'undefined') { @@ -722,7 +751,7 @@ export default class UpdateManager { }) }) - var protocol = this.editable(doc.uri, kb) + var protocol = this.editable(doc.value, kb) if (protocol === false) { throw new Error('Update: Can\'t make changes in uneditable ' + doc) } @@ -730,15 +759,15 @@ export default class UpdateManager { if (secondTry) { throw new Error('Update: Loaded ' + doc + "but stil can't figure out what editing protcol it supports.") } - console.log(`Update: have not loaded ${doc} before: loading now...`) - this.store.fetcher.load(doc).then(response => { - this.update(deletions, insertions, callbackFunction, true) // secondTry + console.log(`Update: have not loaded ${doc} before: loading now...`); + (this.store.fetcher.load(doc) as Promise).then(response => { + this.update(deletions, insertions, callback, true) }, err => { throw new Error(`Update: Can't read ${doc} before patching: ${err}`) }) return - } else if (protocol.indexOf('SPARQL') >= 0) { - var bnodes = [] + } else if ((protocol as string).indexOf('SPARQL') >= 0) { + var bnodes: TFBlankNode[] = [] if (ds.length) bnodes = this.statementArrayBnodes(ds) if (is.length) bnodes = bnodes.concat(this.statementArrayBnodes(is)) var context = this.bnodeContext(bnodes, doc) @@ -784,11 +813,11 @@ export default class UpdateManager { console.log('upstream count up to : ' + control.upstreamCount) } - this.fire(doc.uri, query, (uri, success, body, response) => { - response.elapsedTimeMs = Date.now() - startTime + this.fire(doc.value, query, (uri, success, body, response) => { + (response as any).elapsedTimeMs = Date.now() - startTime console.log(' UpdateManager: Return ' + - (success ? 'success ' : 'FAILURE ') + response.status + - ' elapsed ' + response.elapsedTimeMs + 'ms') + (success ? 'success ' : 'FAILURE ') + (response as Response).status + + ' elapsed ' + (response as any).elapsedTimeMs + 'ms') if (success) { try { kb.remove(ds) @@ -801,7 +830,7 @@ export default class UpdateManager { } } - callbackFunction(uri, success, body, response) + callback(uri, success, body, response) control.pendingUpstream -= 1 // When upstream patches have been sent, reload state if downstream waiting if (control.pendingUpstream === 0 && control.downstreamAction) { @@ -811,15 +840,15 @@ export default class UpdateManager { downstreamAction(doc) } }) - } else if (protocol.indexOf('DAV') >= 0) { - this.updateDav(doc, ds, is, callbackFunction) + } else if ((protocol as string).indexOf('DAV') >= 0) { + this.updateDav(doc, ds, is, callback) } else { - if (protocol.indexOf('LOCALFILE') >= 0) { + if ((protocol as string).indexOf('LOCALFILE') >= 0) { try { - this.updateLocalFile(doc, ds, is, callbackFunction) + this.updateLocalFile(doc, ds, is, callback) } catch (e) { - callbackFunction(doc.uri, false, - 'Exception trying to write back file <' + doc.uri + '>\n' + callback(doc.value, false, + 'Exception trying to write back file <' + doc.value + '>\n' // + tabulator.Util.stackString(e)) ) } @@ -828,12 +857,17 @@ export default class UpdateManager { } } } catch (e) { - callbackFunction(undefined, false, 'Exception in update: ' + e + '\n' + + callback(undefined, false, 'Exception in update: ' + e + '\n' + Util.stackString(e)) } } - updateDav (doc, ds, is, callbackFunction) { + updateDav ( + doc: TFSubject, + ds, + is, + callbackFunction + ): null | Promise { let kb = this.store // The code below is derived from Kenny's UpdateCenter.js var request = kb.any(doc, this.ns.link('request')) @@ -841,11 +875,11 @@ export default class UpdateManager { throw new Error('No record of our HTTP GET request for document: ' + doc) } // should not happen - var response = kb.any(request, this.ns.link('response')) + var response = kb.any(request as TFNamedNode, this.ns.link('response')) as TFSubject if (!response) { return null // throw "No record HTTP GET response for document: "+doc } - var contentType = kb.the(response, this.ns.httph('content-type')).value + var contentType = (kb.the(response, this.ns.httph('content-type'))as TFTerm).value // prepare contents of revised document let newSts = kb.statementsMatching(undefined, undefined, undefined, doc).slice() // copy! @@ -856,7 +890,7 @@ export default class UpdateManager { newSts.push(is[i]) } - const documentString = this.serialize(doc.uri, newSts, contentType) + const documentString = this.serialize(doc.value, newSts, contentType) // Write the new version back var candidateTarget = kb.the(response, this.ns.httph('content-location')) @@ -884,10 +918,10 @@ export default class UpdateManager { kb.add(is[i].subject, is[i].predicate, is[i].object, doc) } - callbackFunction(doc.uri, response.ok, response.responseText, response) + callbackFunction(doc.value, response.ok, response.responseText, response) }) .catch(err => { - callbackFunction(doc.uri, false, err.message, err) + callbackFunction(doc.value, false, err.message, err) }) } @@ -899,7 +933,7 @@ export default class UpdateManager { * @param is * @param callbackFunction */ - updateLocalFile (doc, ds, is, callbackFunction) { + updateLocalFile (doc: TFNamedNode, ds, is, callbackFunction): void { const kb = this.store console.log('Writing back to local file\n') // See http://simon-jung.blogspot.com/2007/10/firefox-extension-file-io.html @@ -913,29 +947,31 @@ export default class UpdateManager { newSts.push(is[ i ]) } // serialize to the appropriate format - var dot = doc.uri.lastIndexOf('.') + var dot = doc.value.lastIndexOf('.') if (dot < 1) { - throw new Error('Rewriting file: No filename extension: ' + doc.uri) + throw new Error('Rewriting file: No filename extension: ' + doc.value) } - var ext = doc.uri.slice(dot + 1) + var ext = doc.value.slice(dot + 1) let contentType = Fetcher.CONTENT_TYPE_BY_EXT[ ext ] if (!contentType) { throw new Error('File extension .' + ext + ' not supported for data write') } - const documentString = this.serialize(doc.uri, newSts, contentType) + const documentString = this.serialize(doc.value, newSts, contentType) // Write the new version back // create component for file writing console.log('Writing back: <<<' + documentString + '>>>') - var filename = doc.uri.slice(7) // chop off file:// leaving /path + var filename = doc.value.slice(7) // chop off file:// leaving /path // console.log("Writeback: Filename: "+filename+"\n") + // @ts-ignore Where does Component come from? Perhaps deprecated? var file = Components.classes[ '@mozilla.org/file/local;1' ] + // @ts-ignore Where does Component come from? Perhaps deprecated? .createInstance(Components.interfaces.nsILocalFile) file.initWithPath(filename) if (!file.exists()) { - throw new Error('Rewriting file <' + doc.uri + + throw new Error('Rewriting file <' + doc.value + '> but it does not exist!') } // { @@ -943,7 +979,9 @@ export default class UpdateManager { // } // create file output stream and use write/create/truncate mode // 0x02 writing, 0x08 create file, 0x20 truncate length if exist + // @ts-ignore Where does Component come from? Perhaps deprecated? var stream = Components.classes[ '@mozilla.org/network/file-output-stream;1' ] + // @ts-ignore Where does Component come from? Perhaps deprecated? .createInstance(Components.interfaces.nsIFileOutputStream) // Various JS systems object to 0666 in struct mode as dangerous @@ -959,19 +997,15 @@ export default class UpdateManager { for (let i = 0; i < is.length; i++) { kb.add(is[ i ].subject, is[ i ].predicate, is[ i ].object, doc) } - callbackFunction(doc.uri, true, '') // success! + callbackFunction(doc.value, true, '') // success! } /** - * @param uri {string} - * @param data {string|Array} - * @param contentType {string} - * * @throws {Error} On unsupported content type * * @returns {string} */ - serialize (uri, data, contentType) { + serialize (uri: string, data: string | TFQuad[], contentType: string): string { const kb = this.store let documentString @@ -1003,35 +1037,31 @@ export default class UpdateManager { } /** - * This is suitable for an initial creation of a document - * - * @param doc {Node} - * @param data {string|Array} - * @param contentType {string} - * @param callbackFunction {Function} callbackFunction(uri, ok, message, response) - * - * @throws {Error} On unsupported content type (via serialize()) - * - * @returns {Promise} + * This is suitable for an initial creation of a document. */ - put (doc, data, contentType, callbackFunction) { + put( + doc: NamedNode, + data: string | TFQuad[], + contentType: string, + callback: (uri: string, ok: boolean, errorMessage?: string, response?: unknown) => void, + ): Promise { const kb = this.store - let documentString + let documentString: string return Promise.resolve() .then(() => { - documentString = this.serialize(doc.uri, data, contentType) + documentString = this.serialize(doc.value, data, contentType) return kb.fetcher - .webOperation('PUT', doc.uri, { contentType, body: documentString }) + .webOperation('PUT', doc.value, { contentType, body: documentString }) }) .then(response => { if (!response.ok) { - return callbackFunction(doc.uri, response.ok, response.error, response) + return callback(doc.value, response.ok, response.error, response) } - delete kb.fetcher.nonexistent[doc.uri] - delete kb.fetcher.requested[doc.uri] // @@ could this mess with the requested state machine? if a fetch is in progress + delete kb.fetcher.nonexistent[doc.value] + delete kb.fetcher.requested[doc.value] // @@ could this mess with the requested state machine? if a fetch is in progress if (typeof data !== 'string') { data.map((st) => { @@ -1039,10 +1069,10 @@ export default class UpdateManager { }) } - callbackFunction(doc.uri, response.ok, '', response) + callback(doc.value, response.ok, '', response) }) .catch(err => { - callbackFunction(doc.uri, false, err.message) + callback(doc.value, false, err.message) }) } @@ -1058,17 +1088,27 @@ export default class UpdateManager { * @param doc {NamedNode} * @param callbackFunction */ - reload (kb, doc, callbackFunction) { + reload ( + kb: IndexedFormula, + doc: docReloadType, + callbackFunction: (ok: boolean, message?: string, response?: Error | Response) => {} | void + ): void { var startTime = Date.now() // force sets no-cache and - const options = { force: true, noMeta: true, clearPreviousData: true } + const options = { + force: true, + noMeta: true, + clearPreviousData: true, + }; - kb.fetcher.nowOrWhenFetched(doc.uri, options, function (ok, body, response) { + (kb as any).fetcher.nowOrWhenFetched(doc.value, options, function (ok: boolean, body: Body, response: Response) { if (!ok) { console.log(' ERROR reloading data: ' + body) callbackFunction(false, 'Error reloading data: ' + body, response) + //@ts-ignore Where does onErrorWasCalled come from? } else if (response.onErrorWasCalled || response.status !== 200) { console.log(' Non-HTTP error reloading data! onErrorWasCalled=' + + //@ts-ignore Where does onErrorWasCalled come from? response.onErrorWasCalled + ' status: ' + response.status) callbackFunction(false, 'Non-HTTP error reloading data: ' + body, response) } else { @@ -1089,3 +1129,8 @@ export default class UpdateManager { }) } } + +interface docReloadType extends TFNamedNode { + reloadTimeCount?: number + reloadTimeTotal?: number +} diff --git a/src/uri.js b/src/uri.ts similarity index 75% rename from src/uri.js rename to src/uri.ts index 40fe7923b..18945e3d5 100644 --- a/src/uri.js +++ b/src/uri.ts @@ -12,8 +12,12 @@ var alert = alert || console.log import NamedNode from './named-node' -export function docpart (uri) { - var i +/** + * Gets the document part of an URI + * @param uri The URI + */ +export function docpart(uri: string): string { + var i: number i = uri.indexOf('#') if (i < 0) { return uri @@ -22,11 +26,19 @@ export function docpart (uri) { } } -export function document (x) { - return new NamedNode(docpart(x.uri)) +/** + * Gets the document part of an URI as a named node + * @param x - The URI + */ +export function document(x: string): NamedNode { + return new NamedNode(docpart(x)) } -export function hostpart (u) { +/** + * Gets the hostname in an URI + * @param u The URI + */ +export function hostpart(u: string): string { var m = /[^\/]*\/\/([^\/]*)\//.exec(u) if (m) { return m[1] @@ -35,7 +47,12 @@ export function hostpart (u) { } } -export function join (given, base) { +/** + * Joins an URI with a base + * @param given - The relative part + * @param base - The base URI + */ +export function join(given: string, base: string): string { var baseColon, baseScheme, baseSingle var colon, lastSlash, path var baseHash = base.indexOf('#') @@ -103,9 +120,12 @@ export function join (given, base) { return base.slice(0, baseSingle) + path } -export function protocol (uri) { - var i - i = uri.indexOf(':') +/** + * Gets the protocol part of an URI + * @param uri The URI + */ +export function protocol(uri: string): string | null { + const i = uri.indexOf(':') if (i < 0) { return null } else { @@ -113,8 +133,25 @@ export function protocol (uri) { } } -export function refTo (base, uri) { - var c, i, k, l, len, len1, n, o, p, q, ref, ref1, s +/** + * Gets a relative uri + * @param base The base URI + * @param uri The absolute URI + */ +export function refTo(base: string, uri: string): string { + var c: string, + i: number, + k: number, + l: number, + len: number, + len1: number, + n: number, + o: number, + p: number, + q: number, + ref: string, + ref1: number, + s: string var commonHost = new RegExp('^[-_a-zA-Z0-9.]+:(//[^/]*)?/[^/]*$') if (!base) { return uri @@ -123,7 +160,7 @@ export function refTo (base, uri) { return '' } for (i = o = 0, len = uri.length; o < len; i = ++o) { - c = uri[i] + const c = uri[i] if (c !== base[i]) { break } diff --git a/src/util.js b/src/util.js index 08de267bd..eb9b543cb 100644 --- a/src/util.js +++ b/src/util.js @@ -20,117 +20,13 @@ export function linkRelationProperty(relation){ return new NamedNode('http://www.w3.org/ns/iana/link-relations/relation#' + relation.trim()) } -export const appliedFactoryMethods = [ - 'blankNode', - 'defaultGraph', - 'literal', - 'namedNode', - 'quad', - 'variable', - 'supports', -] - -/** - * Loads ontologies of the data we load (this is the callback from the kb to - * the fetcher). - */ -export function AJAR_handleNewTerm (kb, p, requestedBy) { - var sf = null - if (typeof kb.fetcher !== 'undefined') { - sf = kb.fetcher - } else { - return - } - if (p.termType !== 'NamedNode') return - var docuri = docpart(p.uri) - var fixuri - if (p.uri.indexOf('#') < 0) { // No hash - // @@ major hack for dbpedia Categories, which spread indefinitely - if (string_startswith(p.uri, 'http://dbpedia.org/resource/Category:')) return - - /* - if (string_startswith(p.uri, 'http://xmlns.com/foaf/0.1/')) { - fixuri = "http://dig.csail.mit.edu/2005/ajar/ajaw/test/foaf" - // should give HTTP 303 to ontology -- now is :-) - } else - */ - if (string_startswith(p.uri, - 'http://purl.org/dc/elements/1.1/') || - string_startswith(p.uri, 'http://purl.org/dc/terms/')) { - fixuri = 'http://dublincore.org/2005/06/13/dcq' - // dc fetched multiple times - } else if (string_startswith(p.uri, 'http://xmlns.com/wot/0.1/')) { - fixuri = 'http://xmlns.com/wot/0.1/index.rdf' - } else if (string_startswith(p.uri, 'http://web.resource.org/cc/')) { - // log.warn("creative commons links to html instead of rdf. doesn't seem to content-negotiate.") - fixuri = 'http://web.resource.org/cc/schema.rdf' - } - } - if (fixuri) { - docuri = fixuri - } - if (sf && sf.getState(docuri) !== 'unrequested') return - - if (fixuri) { // only give warning once: else happens too often - log.warn('Assuming server still broken, faking redirect of <' + p.uri + - '> to <' + docuri + '>') - } - - return sf.fetch(docuri, { referringTerm: requestedBy }) -} - -const rdf = { - first: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', - rest: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', - nil: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil' -} - -/** - * Expands an array of Terms to a set of statements representing the rdf:list. - * @param rdfFactory - The factory to use - * @param {NamedNode|BlankNode} subject - The iri of the first list item. - * @param {Term[]} data - The terms to expand into the list. - * @return {Statement[]} - The {data} as a set of statements. - */ -export function arrayToStatements(rdfFactory, subject, data) { - const statements = [] - - data.reduce((id, listObj, i, listData) => { - statements.push(rdfFactory.quad(id, rdfFactory.namedNode(rdf.first), listData[i])) - - let nextNode - if (i < listData.length - 1) { - nextNode = rdfFactory.blankNode() - statements.push(rdfFactory.quad(id, rdfFactory.namedNode(rdf.rest), nextNode)) - } else { - statements.push(rdfFactory.quad(id, rdfFactory.namedNode(rdf.rest), rdfFactory.namedNode(rdf.nil))) - } - - return nextNode - }, subject) - - return statements -} - -export function ArrayIndexOf (arr, item, i) { - i || (i = 0) - var length = arr.length - if (i < 0) i = length + i - for (; i < length; i++) { - if (arr[i] === item) { - return i - } - } - return -1 -} - /** * Adds callback functionality to an object. * Callback functions are indexed by a 'hook' string. * They return true if they want to be called again. * @method callbackify * @param obj {Object} - * @param callbacks {Array} + * @param callbacks {Array} */ export function callbackify (obj, callbacks) { obj.callbacks = {} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 000000000..ca82425cc --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,114 @@ +import Fetcher from './fetcher' +import log from './log' +import { TFDataFactory, TFLiteral, TFQuad, TFSubject, TFTerm } from './types' +import { docpart } from './uri' +import { string_startswith } from './util' + +/** RDF/JS Taskforce Typeguards */ + +/** + * Loads ontologies of the data we load (this is the callback from the kb to + * the fetcher). + */ +export function AJAR_handleNewTerm (kb: { fetcher: Fetcher }, p, requestedBy) { + var sf: Fetcher | null = null + if (typeof kb.fetcher !== 'undefined') { + sf = kb.fetcher + } else { + return + } + if (p.termType !== 'NamedNode') return + var docuri = docpart(p.uri) + var fixuri + if (p.uri.indexOf('#') < 0) { // No hash + // @@ major hack for dbpedia Categories, which spread indefinitely + if (string_startswith(p.uri, 'http://dbpedia.org/resource/Category:')) return + + /* + if (string_startswith(p.uri, 'http://xmlns.com/foaf/0.1/')) { + fixuri = "http://dig.csail.mit.edu/2005/ajar/ajaw/test/foaf" + // should give HTTP 303 to ontology -- now is :-) + } else + */ + if (string_startswith(p.uri, + 'http://purl.org/dc/elements/1.1/') || + string_startswith(p.uri, 'http://purl.org/dc/terms/')) { + fixuri = 'http://dublincore.org/2005/06/13/dcq' + // dc fetched multiple times + } else if (string_startswith(p.uri, 'http://xmlns.com/wot/0.1/')) { + fixuri = 'http://xmlns.com/wot/0.1/index.rdf' + } else if (string_startswith(p.uri, 'http://web.resource.org/cc/')) { + // log.warn("creative commons links to html instead of rdf. doesn't seem to content-negotiate.") + fixuri = 'http://web.resource.org/cc/schema.rdf' + } + } + if (fixuri) { + docuri = fixuri + } + if (sf && (sf as Fetcher).getState(docuri) !== 'unrequested') return + + if (fixuri) { // only give warning once: else happens too often + log.warn('Assuming server still broken, faking redirect of <' + p.uri + + '> to <' + docuri + '>') + } + + return (sf as any).fetch(docuri, { referringTerm: requestedBy }) +} + +export const appliedFactoryMethods = [ + 'blankNode', + 'defaultGraph', + 'literal', + 'namedNode', + 'quad', + 'variable', + 'supports', +] + +const rdf = { + first: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', + rest: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', + nil: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil' +} + +/** + * Expands an array of Terms to a set of statements representing the rdf:list. + * @param rdfFactory - The factory to use + * @param subject - The iri of the first list item. + * @param data - The terms to expand into the list. + * @return The {data} as a set of statements. + */ +export function arrayToStatements( + rdfFactory: TFDataFactory, + subject: TFSubject, + data: TFTerm[] +): TFQuad[] { + const statements: TFQuad[] = [] + + data.reduce((id, _listObj, i, listData) => { + statements.push(rdfFactory.quad(id, rdfFactory.namedNode(rdf.first), listData[i] as TFLiteral)) + + let nextNode + if (i < listData.length - 1) { + nextNode = rdfFactory.blankNode() + statements.push(rdfFactory.quad(id, rdfFactory.namedNode(rdf.rest), nextNode)) + } else { + statements.push(rdfFactory.quad(id, rdfFactory.namedNode(rdf.rest), rdfFactory.namedNode(rdf.nil))) + } + + return nextNode + }, subject) + + return statements +} + +export function ArrayIndexOf (arr, item, i: number = 0) { + var length = arr.length + if (i < 0) i = length + i + for (; i < length; i++) { + if (arr[i] === item) { + return i + } + } + return -1 +} diff --git a/src/utils/terms.ts b/src/utils/terms.ts index 448f70a8d..80aee4cd2 100644 --- a/src/utils/terms.ts +++ b/src/utils/terms.ts @@ -1,24 +1,35 @@ -import { TFTerm } from "../types"; +import { + ObjectType, + TermType, + TFBlankNode, + TFGraph, + TFLiteral, + TFNamedNode, + TFObject, + TFPredicate, + TFQuad, + TFSubject, + TFTerm +} from '../types' +import Collection from '../collection' +import IndexedFormula from '../store' +import Statement from '../statement' +import NamedNode from '../named-node' -export function isTerm(obj) { - return typeof obj === 'object' - && obj !== null - && 'termType' in obj - && 'value' in obj -} - -export function isStatement(obj) { +export function isStatement(obj): obj is Statement { return typeof obj === 'object' && obj !== null && 'subject' in obj } -export function isStore(obj) { +/** TypeGuard for RDF/JS TaskForce Stores */ +export function isStore(obj): obj is IndexedFormula { return typeof obj === 'object' && obj !== null && 'statements' in obj } -export function isNamedNode(obj) { - return isTerm(obj) && obj.termType === 'NamedNode' +export function isNamedNode(obj): obj is NamedNode { + return isTFTerm(obj) && obj.termType === 'NamedNode' } +/** Retrieve the value of a term, or self if already a string. */ export function termValue(node: TFTerm | string): string { if (typeof node === 'string') { return node @@ -26,3 +37,85 @@ export function termValue(node: TFTerm | string): string { return node.value } + +/** TypeGuard for RDFLib Collections */ +export function isCollection(obj: any): obj is Collection { + return isTFTerm(obj) + && (obj as TFTerm).termType === TermType.Collection +} + +/** TypeGuard for valid RDFlib Object types, also allows Collections */ +export function isRDFObject (obj: any): obj is ObjectType { + return obj && Object.prototype.hasOwnProperty.call(obj, 'termType') && ( + obj.termType === TermType.NamedNode || + obj.termType === TermType.Variable || + obj.termType === TermType.BlankNode || + obj.termType === TermType.Collection || + obj.termType === TermType.Literal + ) +} + +/** TypeGuard for RDF/JS TaskForce Terms */ +export function isTFTerm (obj: any): obj is TFTerm { + return typeof obj === 'object' + && obj !== null + && 'termType' in obj + && 'value' in obj +} + +/** TypeGuard for RDF/JS TaskForce Literals */ +export function isTFLiteral (value: any): value is TFLiteral { + return (value as TFTerm).termType === TermType.Literal +} + +/** TypeGuard for RDF/JS TaskForce Quads */ +export function isTFStatement (obj: any): obj is TFQuad { + return typeof obj === "object" && obj !== null && 'subject' in obj +} + +/** TypeGuard for RDF/JS TaskForce NamedNodes */ +export function isTFNamedNode (obj: any): obj is TFNamedNode { + return isTFTerm(obj) && 'termType' in obj && obj.termType === 'NamedNode' +} + +/** TypeGuard for RDF/JS TaskForce BlankNodes */ +export function isTFBlankNode (obj: any): obj is TFBlankNode { + return isTFTerm(obj) && 'termType' in obj && obj.termType === 'BlankNode' +} + +/** TypeGuard for valid RDFJS Taskforce Subject types */ +export function isTFSubject (obj: any): obj is TFSubject { + return isTFTerm(obj) && ( + obj.termType === TermType.NamedNode || + obj.termType === TermType.Variable || + obj.termType === TermType.BlankNode + ) +} + +/** TypeGuard for valid RDFJS Taskforce Predicate types */ +export function isTFPredicate (obj: any): obj is TFPredicate { + return isTFTerm(obj) && ( + obj.termType === TermType.NamedNode || + obj.termType === TermType.Variable + ) +} + +/** TypeGuard for valid RDFJS Taskforce Object types */ +export function isTFObject (obj: any): obj is TFObject { + return isTFTerm(obj) && ( + obj.termType === TermType.NamedNode || + obj.termType === TermType.Variable || + obj.termType === TermType.BlankNode || + obj.termType === TermType.Literal + ) +} + +/** TypeGuard for valid RDFJS Graph types */ +export function isTFGraph (obj: any): obj is TFGraph { + return isTFTerm(obj) && ( + obj.termType === TermType.NamedNode || + obj.termType === TermType.Variable || + obj.termType === TermType.BlankNode || + obj.termType === TermType.DefaultGraph + ) +} diff --git a/src/variable.ts b/src/variable.ts index d38d854cc..7f7704325 100644 --- a/src/variable.ts +++ b/src/variable.ts @@ -1,25 +1,23 @@ -'use strict' import ClassOrder from './class-order' import Node from './node-internal' -import { TermType } from "./types"; +import { TermType, TFVariable, VariableTermType } from './types' import * as Uri from './uri' /** - * Variables are placeholders used in patterns to be matched. - * In cwm they are symbols which are the formula's list of quantified variables. - * In sparql they are not visibly URIs. Here we compromise, by having - * a common special base URI for variables. Their names are uris, - * but the ? notation has an implicit base uri of 'varid:' - * @class Variable - */ -export default class Variable extends Node { +* Variables are placeholders used in patterns to be matched. +* In cwm they are symbols which are the formula's list of quantified variables. +* In sparql they are not visibly URIs. Here we compromise, by having +* a common special base URI for variables. Their names are uris, +* but the ? notation has an implicit base uri of 'varid:' +*/ +export default class Variable extends Node implements TFVariable { static termType = TermType.Variable /** The base string for a variable's name */ base = 'varid:' classOrder = ClassOrder.Variable isVar = 1 - termType = TermType.Variable + termType: VariableTermType = TermType.Variable /** The unique identifier of this variable */ uri: string @@ -29,6 +27,7 @@ export default class Variable extends Node { */ constructor (name = '') { super(name) + this.base = 'varid:' this.uri = Uri.join(name, this.base) } diff --git a/src/wip.md b/src/wip.md new file mode 100644 index 000000000..209422fb9 --- /dev/null +++ b/src/wip.md @@ -0,0 +1,111 @@ +Builds upon the approved #363 PR for [typescript migration](https://github.com/linkeddata/rdflib.js/issues/355): + +## Changes included in PR #363 + +- Converted some of the most fundamental classes to typescript, including `Node`, `Literal`, `BlankNode`, `NamedNode`, `Collection`, `Statement`. +- Introduced a `.types` file for shared types. +- Included a temporary `types-temp.ts` file in project root as a reference file for documentation and keeping track of the ts migration process. +- The `.isVar` method is set to boolean values, instead of `0` or `1`. This seemed reasonable, as it's only used for boolean type checks, and the existing types already define it as a boolean value. Timbl confirmed that `isVar` is only used for boolean operations. +- JSDoc is replaced with Typedoc. Combined with types and comments from `@types/rdflib`, this makes the documentation far more complete. +- I used many of the types and comments from `@types/rdflib` by [Cénotélie](https://github.com/cenotelie/). Added credits in `package.json`, discussed this with Cénotélie. + +## New changes + +- Migrated `formula`, `variable`, `store`, `update-manager`, `data-factory`, `default-graph`, `namespace`, `parse`,`serialize`, `parse`, `uri` and `utils` tot ts. +- Added `fork-ts-checker-webpack-plugin`, which enables errors in the log when ts errors occur. +- Added and implemented RDF/JS Taskforce (TF) types, included these in the `types.ts` file. I tried implementing the TF types in the major classes, but some of the incompatibilities make it difficult. Many available methods on rdfjs instances (e.g. `.toNt()` in NamedNode), are missing in TF classes. To improve TF comatibility, we should minimize using rdflib specific functions. This would for example enable using Forumla methods on RDFExt nodes. We should use the Taskforce types (TFTerm, TFQuad) as much as possible, instead of rdflib types (Node, Statement). +- Added typeguards, e.g. `isTFNamedNode` and `isTFPredicate` in `Utils`, and used these at various locations. +- Use enums for `termType` and `contentType`, without breaking compatibility with regular strings. +- `Formula` Constructor arguments are optional - since some functions initialize empty Formulas. +- In `Formula.fromNT()` `return this.literal(str, lang || dt)` seemed wrong, converted it to +- The various `fromValue` methods conflict with the base Node class, type wise. Since they don't use `this`, I feel like they should be converted to functions. + +## Compatibility with RDFJS taskforce and external datafactories + +- Variables (from rdfjs taskforce) make typings a lot more complex (many methods would require explicit type checks, e.g. you can't serialize a variable to N-Triples), so I disabled them. +- Switched internal calls from `sameTerm` to `equals` in order to comply to TF spec, so that these functions also work with external datafactories. Alias still exists, so nothing changes externally. +- Switched internal calls from `.why` to `graph`. Alias still exists, so nothing changes externally. +- Calls to `kb.sym` have been replaced with `kb.rdfFactory.namedNode`, which makes all these functions more compatible with external datafactories. Added a deprecation warning to `.sym`. + +## Minor fixes + +- Removed the last conditional of `Formula.holds()`, since it did not make sense +- Removed some unreachable code, unused variables and functions that didn't do anything such as `Node.substitute()`. +- Removed the `justOne` argument from `formula.statementsMatching`, since it was unused. +- The `uri.document` function called `.uri` on a string, I removed that. +- Transformed inline comments to JSDoc, moved them to type declarations instead of constructor. +- Some types are set to any, because I didn't fully understand them. I've added TODO comments for these. +- Removed the fourth argument from the `parser.parse` function in `fetcher.parse`, since the function only takes three. +- Removed the `response` argument from `fetcher.parse`, `XHTMLHandler.parse`, `RDFXMLHander.parse`, `XMLHandler`, since it was not used. +- `Fetcher.failfetch` added strings as objects to the store. Changed that to literals. +- Internal calls to `NamedNode.uri` are changed to `.value` to comply with TF spec. This enables these functions to work with external datafactories. +- Removed unused second argument from `Fetcher.cleanupFetchRequest` +- Created one huge `Options` type for Fetcher. Not sure if this is the way to go. +- In `Node.toJS`, the boolean only returned true if the `xsd:boolean` value is `'1'`, now it it should also work for `'true'`. +- Converted `kb.add(someString)` to `kb.add(new Namednode(somestring))` to enhance compatibility with other datafactories. This happens in `Fetcher` and +- `Fetcher.refreshIfExpired` passed an array of headers, but it needs only one string. +- `Fethcer` uses `Headers` a lot. I've changed empty objects to empty `new Headers` instances, which enhances compatibility with default `Fetch` behavior. +- `Serializer.tripleCallback` had an unused third argument. +- `UpdateManager.update` checked an undefined `secondTry` variable. Since later in the same function, `.update` is called with a 4th argument, I assume this is `secondTry`. I've added it as an optional argument. Perhaps this is +- `Formula.add()` now uses `this.rdfFactory.defaultGraph` instead of the non-existent `this.defaultGraph` +- `IndexedFormula.replaceWith` now passes a Node instead of a string to `.add` in the `if (big.value)` block + +## Possible bugs discovered, which are not fixed by this PR + +- `Formula.substitute` uses `this.add(Statments[])`, which will crash. I think it should be removed, since `IndexedFormula.substitute` is used all the time anyway. +- The `Formula.serialize` function calls `serialize.ts` with only one argument, so without a store. I think this will crash every time. Also`Formula.serialize` uses `this.namespaces`, but this is only defined in `IndexedFormula`. Is it rotten code and should it be removed? +- `IndexedFormula.add()` accepts many types of inputs, but this will lead to invalid statements (e.g. a Literal as a Subject). I suggest we make this more strict and throw more errors on wrong inputs. Relates to #362. We could still make the allowed inputs bigger by allowing other types with some explicit behavior, e.g. in subject arguments, create `NamedNodes` from `URL` objects and `strings` that look like URLs . In any case, I thinkg the `Node.fromValue` behavior is too unpredictable for `store.add`. For now, I've updated the docs to match its behavior. +- The types for `Node.fromValue` and `Literal.fromValue` show how unpredictable these methods are. I suggest we make them more strict (also relates to #362), so they either return a `TFTerm` (`node`) or throw an error - they should not return `undefined` or `null`. Also, I think they should be converted to functions in `Utils`: this would fix the circular dependency issue (why we need `node_internal`) and it would fix the type issues in `Literal.fromValue` (which tends to give issues since it's signature does not correctly extend from `Node.fromValue`) +- In `Fetcher.addtype`, the final logic will allways return `true`, since `redirection` is a `NamedNode`. Should it call `.value`? +- Various `Hanlder.parse()` functions in `Fetcher` return either a `Response` or a `Promise`. This seems like weird behavior - should it not always return an array? +- The `defaultGraph` iri is set to `chrome:theSession`, but this errors in Firefox. I suggest we change it to something else. See #370. +- The `Parse.executeErrorCallback` conditional logic is always `true`. +- I've added many `// @ts-ignore` comments. Ideally, these should be resolved by fixing the underlying type issues. +- `UpdateManager.update_statement` seems to refer to the wrong `this`. It calls `this.anonimize`, but it is not available in that scope. +- `UpdateManager.updateLocalFile` uses `Component`, but this is not defined anywhere. Is this deprecated? +- `Data-factory-internal.id()` returns `string | undefined`, I feel like undefined should not be possible - it should throw an error. This would resolve the type incompatibility on line 146. +- `IndexedFormula.copy` runs `.copy` on a Collection, but that method is not available there. +- `IndexedFormula.predicateCallback` is checked, but never used in this codebase. + + +## Other things I noticed + +- Literals can apparently be `null` or `undefined`, when nodes are created using the `.fromValue` method. This causes faulty behavior. This happens in the `new Statement()` constructor as well. See #362. +- The `IndexedFormula.add()` method has logic for Statement array inputs and store inputs, but this behavior is not documented. It also refers to `this.fetcher` and `this.defaultGraph`, which both should not be available. I've added types that accept these arrays. +- The filenames of major classes differ from their default exports, e.g. `store.ts` is called `IndexedFormula`. +- Aliases (e.g. `IndexedFormula.match` for `IndexefFormula.statementsMatching`) introduce complexity, documentation and type duplication. I suggest adding deprecation warnings. +- The various calling methods of `Fetcher.nowOrWhenFetched` are quite dynamic. A simpler, stricter input type might be preferable. +- The Variable type (or `TFVariable`) really messes with some assumptions. I feel like they should not be valid in regular quads, since it's impossible to serialize them decently. If I'd add it to the accepted types, we'd require a lot of typeguards in functions. +- `Fetcher` `StatusValues` can be many types in RDFlib: string, number, true, undefined... This breaks compatibility with extending `Response` types, since these only use numbers. I feel we should only use the `499` browser error and add the text message to the `requestbody`. I've created a type for the internal `InternalResponse`; it shows how it differs from a regular `Response`. The `.responseText` property, for example, is used frequently in Fetcher, but +- The `IndexedFormula` and `Formula` methods have incompatible types, such as in `compareTerm`, `variable` and `add`. I've added `//@ts-ignore` lines with comments. +- The fourth `reponse` argument in `.parse()` methods in `Handler` classes was unused (except in N3Handler), so I removed it everywhere it was unused. +- `Serializer`'s fourth `options` argument is undocumented, and I couldn't find out how it worked. +- `Fetcher` `saveResponseMetadata` creates literals +- Many functions in `Fetcher` assume that specific `Opts` are defined. I've included all these in a single `Options` type and added documentation for the props I understood. I've also created an `AutoInitOptions` type, which sets auto-initialized. I extended Options in each function where specific opts seemed to be required. I'm not entirely confident about the types I've set. I feel like the truly required items should never be `Opts`, since they aren't optional. Refactoring this requires a seperate PR. +- `Fetcher.load` allows arrays as inputs. This causes the output types to be more unpredictable. `Promise | Result[]`. I suggest splitting this in two functions, e.g. add `loadMany` +- `Utils.callbackify` seems to be used only in `Fetcher`. +- `UpdateManager.editable` has a confusing return type (`string | boolean | undefined`). I suggest we refactor it to always return one of multiple pre-defined strings,. +- The `optional` argument in `formula.js` does not seem to be documented, used or tested - should it be removed? + +## Need review + +- Some of the `Formula` and `IndexedFormula` functions (e.g. `anyStatementMatching`) might have too strict types - perhaps Collections are allowed in some of them. +- `IndexedFormula.declareExistential` & `newExistential` have different assumptions on types - should they be blanknodes or namednodes? +- `IndexedFormula.check` passes a single statement to `checkStatementList`, which expects an array +- I've added many type assertions (e.g. `as TFObject`), but Ideally, these do not exist. Ultimately, these should be replaced by TypeGuards that work on runtime/ +- The `data-factory-types` are quite complex. This is a result of the differences between the RDF//JS Taskforce spec and rdflib itself. Perhaps there is an easier / cleaner way to setup the types (without heavy use of generics), but I'm afraid I can't think of one. + +## Some thoughts on simplifying language + +Getting started with Linked Data or RDF can be difficult, since it introduces quite a few new concepts. +Since this library is powerful and generic, it might be one of the first and most important RDF tools that a developer will use. +Therefore, we should try to use consistent langauge and keep synonyms to a minimum. + +- The name `Node` and `Term` seem to refer to the same concept. Both are used in this repo. I think Term is slightly more suited, partially because it complies to the TF spec, but also because it seems more sementically correct. A `Literal`, for example, is not really a node in the mathematical sense, it's more of an edge, since it cannot connect to other nodes. +- `Statement`, `Triple` and `Quad` refer to the same concept, at least technically. Maybe we could pick one. I suggest `Statement`, because it covers both triples and quads. +- The concept `graph` is referred to as `why`, `doc` and `graph` in the code and API. I think this might be confusing - should we just call it `graph` everywhere? +- the `IndexedFormula` default export name is different from the `store` filename. It might be easier to just call it `store` everywhere, including where it's called `kb`. + +## Probably OK, but I don't get it: + +- I'm a bit embarassed about this, but even after rewriting so much of the code I still don't understand all methods. E.g. `Forumla.transitiveClosure()` diff --git a/tests/unit/fetcher-test.js b/tests/unit/fetcher-test.js index 182c0942b..677d46fa8 100644 --- a/tests/unit/fetcher-test.js +++ b/tests/unit/fetcher-test.js @@ -70,6 +70,7 @@ describe('Fetcher', () => { status: 200, }) const options = { + req: store.rdfFactory.blankNode(), resource: store.rdfFactory.namedNode('https://example.com/resource/1') } diff --git a/tests/unit/parse-test.js b/tests/unit/parse-test.js index 9ffb399bd..5b0d73b7c 100644 --- a/tests/unit/parse-test.js +++ b/tests/unit/parse-test.js @@ -4,7 +4,6 @@ import { expect } from 'chai' import parse from '../../src/parse' import CanonicalDataFactory from '../../src/data-factory-internal' import DataFactory from '../../src/data-factory' -import Node from '../../src/node' import defaultXSD from '../../src/xsd' describe('Parse', () => { diff --git a/tests/unit/typings-test.ts b/tests/unit/typings-test.ts new file mode 100644 index 000000000..38244c15b --- /dev/null +++ b/tests/unit/typings-test.ts @@ -0,0 +1,18 @@ + +import Statement from '../../src/statement' +import Literal from '../../src/literal' +import { expect } from 'chai' + +/** + * These test do some assertions, but their main test is that they don't create compiler errors. + */ +describe('typings', () => { + it('allows calling Literal.equal with non-literal', () => { + const test = (): Statement => undefined as unknown as Statement + + const b = test() + const c = new Literal("") + + expect(c.equals(b.object)).to.be.false() + }) +}) diff --git a/tests/unit/util-test.js b/tests/unit/util-test.js index 4c54f5ad4..1f1c23ffb 100644 --- a/tests/unit/util-test.js +++ b/tests/unit/util-test.js @@ -5,11 +5,11 @@ import CanonicalDataFactory from '../../src/data-factory-internal' import Literal from '../../src/literal' import NamedNode from '../../src/named-node' import Statement from '../../src/statement' -import { arrayToStatements } from '../../src/util' -import { isNamedNode, isStatement, isTerm } from '../../src/utils/terms' +import { arrayToStatements } from '../../src/utils' +import { isNamedNode, isStatement, isTFTerm } from '../../src/utils/terms' describe('util', () => { - describe('isTerm', () => { + describe('isTFTerm', () => { it('handles undefined', () => { expect(isNamedNode(undefined)).to.be.false() }) @@ -26,7 +26,7 @@ describe('util', () => { }) it ('handles literals', () => { - expect(isTerm(new Literal('test'))).to.be.true() + expect(isTFTerm(new Literal('test'))).to.be.true() }); }) diff --git a/tsconfig.json b/tsconfig.json index 4b78c20c4..0ce1aedf8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,13 +3,15 @@ "allowSyntheticDefaultImports": true, "checkJs": false, "allowJs": true, - "target": "es6", + "target": "es5", "module": "es6", "strict": true, "noImplicitAny": false, "lib": [ - "es2019" - ] + "es2019", + "dom" + ], + "outDir": "./temp_ts_output" }, "include": [ "src/**/*" diff --git a/typedoc.js b/typedoc.js new file mode 100644 index 000000000..c3592bae5 --- /dev/null +++ b/typedoc.js @@ -0,0 +1,18 @@ +module.exports = { + src: ['./src'], + mode: "file", + out: "doc", + tsconfig: "tsconfig.json", + theme: "default", + hideGenerator: true, + ignoreCompilerErrors: true, + excludePrivate: true, + excludeNotExported: "true", + target: "ES6", + moduleResolution: "node", + preserveConstEnums: "true", + stripInternal: "true", + suppressExcessPropertyErrors: "true", + suppressImplicitAnyIndexErrors: "true", + module: "commonjs" +} diff --git a/types-temp.ts b/types-temp.ts new file mode 100644 index 000000000..bee81ed0e --- /dev/null +++ b/types-temp.ts @@ -0,0 +1,293 @@ +// This is a Temporary file to help with the migration to typescript. +// See issue: https://github.com/linkeddata/rdflib.js/issues/355 +// Migrate these types and comments to the according files, then remove them from this list. +// Don't import types from this file. +// When you do want to use a type from this file, move it to `./types.ts` +// And import it here. + +import { + Bindings, + ValueType, +} from './src/types' +import { NamedNode } from './src' + +// Type definitions for rdflib 0.20 +// Project: http://github.com/linkeddata/rdflib.js +// Definitions by: Cénotélie +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +// TypeScript Version: 3.0 +// Acknowledgements: This work has been financed by Logilab SA, FRANCE, logilab.fr + +export namespace log { + /** + * Logs a debug event + * @param x The event + */ + function debug(x: any): void; + /** + * Logs a warning event + * @param x The event + */ + function warn(x: any): void; + /** + * Logs an information event + * @param x The event + */ + function info(x: any): void; + /** + * Logs an error event + * @param x The event + */ + function error(x: any): void; + /** + * Logs a success event + * @param x The event + */ + function success(x: any): void; + /** + * Logs a message event + * @param x The event + */ + function msg(x: any): void; +} + +export namespace convert { + /** + * Converts an n3 string to JSON + * @param n3String The n3 string + * @param jsonCallback Callback when the operation terminated + */ + function convertToJson( + n3String: string, + jsonCallback: (err: string, jsonString: string) => void + ): void; + /** + * Converts an n3 string to n-quads + * @param n3String The n3 string + * @param nquadCallback Callback when the operation terminated + */ + function convertToNQuads( + n3String: string, + nquadCallback: (err: string, nquadString: string) => void + ): void; +} + +export class Query { + pat: IndexedFormula; + name: string; + id?: string; + constructor(name: string, id?: any); + } + +export namespace Util { + /** + * Gets a named node for a media type + * @param mediaType A media type + */ + function mediaTypeClass(mediaType: string): NamedNode; + /** + * Gets a named node from the name of a relation + * @param relation The name of a relation + */ + function linkRelationProperty(relation: string): NamedNode; + /** + * Loads ontologies of the data we load (this is the callback from the kb to + * the fetcher). Exports as `AJAR_handleNewTerm` + * @param kb The store + * @param p A property + * @param requestedBy + */ + function AJAR_handleNewTerm( + kb: Formula, + p: NamedNode, + requestedBy: string + ): Promise; +} +/** +* A datatype-specific handler for fetching data +*/ +export interface Handler { + response: any; + dom: any; +} +export interface FetchOptions { + fetch?: typeof fetch; + /** + * The resource which referred to this (for tracking bad links). + */ + referringTerm?: NamedNode; + /** + * Provided content type (for writes). + */ + contentType?: string; + /** + * Override the incoming header to force the data to be treated as this content-type (for reads). + */ + forceContentType?: string; + /** + * Load the data even if loaded before. Also sets the `Cache-Control:` header to `no-cache`. + */ + force?: boolean; + /** + * Original uri to preserve through proxying etc (`xhr.original`). + */ + baseUri?: Node | string; + /** + * Whether this request is a retry via a proxy (generally done from an error handler). + */ + proxyUsed?: boolean; + /** + * Flag for XHR/CORS etc + */ + withCredentials?: boolean; + /** + * Before we parse new data, clear old, but only on status 200 responses. + */ + clearPreviousData?: boolean; + /** + * Prevents the addition of various metadata triples (about the fetch request) to the store. + */ + noMeta?: boolean; + noRDFa?: boolean; +} +/** +* Responsible for fetching RDF data +*/ +export class Fetcher { + store: any; + timeout: number; + appNode: BlankNode; + requested: { + [uri: string]: any; + }; + timeouts: any; + redirectedTo: any; + constructor(store: any, options: any); + static HANDLERS: { + RDFXMLHandler: Handler; + XHTMLHandler: Handler; + XMLHandler: Handler; + HTMLHandler: Handler; + TextHandler: Handler; + N3Handler: Handler; + }; + static CONTENT_TYPE_BY_EXT: { + [ext: string]: string; + }; + /** + * Loads a web resource or resources into the store. + * @param uri Resource to load, provided either as a NamedNode object or a plain URL. If multiple resources are passed as an array, they will be fetched in parallel. + */ + load: (uri: ReadonlyArray | ReadonlyArray | NamedNode | string, options?: FetchOptions) => Promise; +} +/** +* Gets a node for the specified input +* @param value An input value +*/ +export function term(value: ValueType): Node | Collection | ValueType; +/** +* Gets a namespace +* @param nsuri The URI for the namespace +*/ +export function Namespace(nsuri: string): (ln: string) => NamedNode; +/** +* Transforms an NTriples string format into a Node. +* The bnode bit should not be used on program-external values; designed +* for internal work such as storing a bnode id in an HTML attribute. +* This will only parse the strings generated by the vaious toNT() methods. +* @param str A string representation +*/ +export function fromNT(str: string): Node; +/** +* Creates a new fetcher +* @param store The store to use +* @param options The options +*/ +export function fetcher(store: Formula, options: any): Fetcher; +/** +* Creates a new graph (store) +*/ +export function graph(): IndexedFormula; +/** +* Creates a new literal node +* @param val The lexical value +* @param lang The language +* @param dt The datatype +*/ +export function lit(val: string, lang: string, dt: NamedNode): Literal; +/** +* Creates a new statement +* @param subject The subject +* @param predicate The predicate +* @param object The object +* @param graph The containing graph +*/ +export function st( + subject: Node | Date | string, + predicate: Node, + object: Node | Date | string, + graph: Node +): Statement; +/** +* Creates a new named node +* @param value The new named node +*/ +export function sym(value: string): NamedNode; +/** +* Creates a new variable +* @param name The name for the variable +*/ +export function variable(name: string): Variable; +/** +* Creates a new blank node +* @param value The blank node's identifier +*/ +export function blankNode(value: string): BlankNode; +/** +* Gets the default graph +*/ +export function defaultGraph(): DefaultGraph; +/** +* Creates a new literal node +* @param value The lexical value +* @param languageOrDatatype Either the language or the datatype +*/ +export function literal( + value: string, + languageOrDatatype: string | NamedNode +): Literal; +/** +* Creates a new named node +* @param value The new named node +*/ +export function namedNode(value: string): NamedNode; + +/** +* Creates a new statement +* @param subject The subject +* @param predicate The predicate +* @param object The object +*/ +export function triple(subject: Node, predicate: Node, object: Node): Statement; +/** +* Parse a string and put the result into the graph kb. +* Normal method is sync. +* Unfortunately jsdonld is currently written to need to be called async. +* Hence the mess below with executeCallback. +* @param str The input string to parse +* @param kb The store to use +* @param base The base URI to use +* @param contentType The content type for the input +* @param callback The callback to call when the data has been loaded +*/ +export function parse( + str: string, + kb: Formula, + base: string, + contentType: string, + callback: (error: any, kb: Formula) => void +): void; +/** +* Get the next available unique identifier +*/ +export let NextId: number; diff --git a/webpack.config.js b/webpack.config.js index 5198dab44..35324cd7a 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,5 +1,6 @@ const path = require('path') const WrapperPlugin = require('wrapper-webpack-plugin'); +const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); module.exports = (env, args) => { return { @@ -26,6 +27,7 @@ module.exports = (env, args) => { } ] }, + resolve: { extensions: ['.js', '.ts'] }, plugins: [ new WrapperPlugin({ // Fall back to window.fetch when solid-auth-client is not present, @@ -37,6 +39,8 @@ module.exports = (env, args) => { window.solid.auth = { fetch: (a, b) => window.fetch(a, b) } }` }), + // Comment out the next line if you want to disable type checking. + new ForkTsCheckerWebpackPlugin() ], externals: { '@trust/webcrypto': 'crypto',