├── .eslintrc ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── jsconfig.json ├── logo.png ├── package-lock.json ├── package.json ├── src ├── get │ ├── index.ts │ ├── leveliterator.ts │ └── sortingiterator.ts ├── index.ts ├── quadstore.ts ├── scope │ └── index.ts ├── serialization │ ├── fpstring.ts │ ├── index.ts │ ├── patterns.ts │ ├── quads.ts │ ├── terms.ts │ ├── utils.ts │ └── xsd.ts ├── types │ └── index.ts └── utils │ ├── comparators.ts │ ├── constants.ts │ ├── consumeinbatches.ts │ ├── consumeonebyone.ts │ ├── stuff.ts │ └── uid.ts ├── test ├── backends │ ├── browserlevel.ts │ ├── classiclevel.ts │ └── memorylevel.ts ├── browser.ts ├── browser │ ├── index.html │ ├── index.ts │ ├── mocha-10.0.0.js │ └── webpack.config.cjs ├── node.ts ├── others │ ├── consumeinbatches.ts │ ├── consumeonebyone.ts │ ├── fpstring.ts │ ├── others.ts │ └── typings.ts ├── package.json ├── quadstore │ ├── del.ts │ ├── get.ts │ ├── import.ts │ ├── match.ts │ ├── patch.ts │ ├── prewrite.ts │ ├── put.ts │ ├── quadstore.ts │ ├── ranges.ts │ ├── remove.ts │ ├── removematches.ts │ ├── scope.ts │ └── serialization.ts ├── tsconfig.json └── utils │ ├── expect.ts │ ├── stuff.js │ └── stuff.ts └── tsconfig.json /.eslintrc: -------------------------------------------------------------------------------- 1 | // Use this file as a starting point for your project's .eslintrc. 2 | // Copy this file, and add rule overrides as needed. 3 | { 4 | "extends": [ 5 | "airbnb" 6 | ], 7 | "parserOptions": { 8 | "sourceType": "script", 9 | "ecmaFeatures": { 10 | "modules": false, 11 | "impliedStrict": false 12 | } 13 | }, 14 | "env": { 15 | "es6": true, 16 | "node": true, 17 | "mocha": true 18 | }, 19 | "rules": { 20 | "strict": ["error", "global"], 21 | "no-underscore-dangle": "off", 22 | "prefer-template": "off", 23 | "no-param-reassign": "off", 24 | "max-len": "off", 25 | "comma-dangle": "off", 26 | "consistent-return": "off", 27 | "padded-blocks": "off", 28 | "no-restricted-syntax": "off" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | test: 14 | 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest] 18 | node-version: [18.x, 20.x, 22.x] 19 | arch: [x64] 20 | 21 | runs-on: ${{ matrix.os }} 22 | name: ${{ matrix.os }} / Node ${{ matrix.node-version }} ${{ matrix.arch }} 23 | 24 | steps: 25 | - uses: actions/checkout@v2 26 | - name: Use Node.js ${{ matrix.node-version }} 27 | uses: actions/setup-node@v2 28 | with: 29 | node-version: ${{ matrix.node-version }} 30 | - run: npm ci 31 | - run: npm run build 32 | - run: npm test 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/linux,macos,windows,sublimetext,webstorm,visualstudiocode,node 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=linux,macos,windows,sublimetext,webstorm,visualstudiocode,node 4 | 5 | ### Linux ### 6 | *~ 7 | 8 | # temporary files which can be created if a process still has a handle open of a deleted file 9 | .fuse_hidden* 10 | 11 | # KDE directory preferences 12 | .directory 13 | 14 | # Linux trash folder which might appear on any partition or disk 15 | .Trash-* 16 | 17 | # .nfs files are created when an open file is removed but is still being accessed 18 | .nfs* 19 | 20 | ### macOS ### 21 | # General 22 | .DS_Store 23 | .AppleDouble 24 | .LSOverride 25 | 26 | # Icon must end with two \r 27 | Icon 28 | 29 | # Thumbnails 30 | ._* 31 | 32 | # Files that might appear in the root of a volume 33 | .DocumentRevisions-V100 34 | .fseventsd 35 | .Spotlight-V100 36 | .TemporaryItems 37 | .Trashes 38 | .VolumeIcon.icns 39 | .com.apple.timemachine.donotpresent 40 | 41 | # Directories potentially created on remote AFP share 42 | .AppleDB 43 | .AppleDesktop 44 | Network Trash Folder 45 | Temporary Items 46 | .apdisk 47 | 48 | ### Node ### 49 | # Logs 50 | logs 51 | *.log 52 | npm-debug.log* 53 | yarn-debug.log* 54 | yarn-error.log* 55 | lerna-debug.log* 56 | 57 | # Diagnostic reports (https://nodejs.org/api/report.html) 58 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 59 | 60 | # Runtime data 61 | pids 62 | *.pid 63 | *.seed 64 | *.pid.lock 65 | 66 | # Directory for instrumented libs generated by jscoverage/JSCover 67 | lib-cov 68 | 69 | # Coverage directory used by tools like istanbul 70 | coverage 71 | *.lcov 72 | 73 | # nyc test coverage 74 | .nyc_output 75 | 76 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 77 | .grunt 78 | 79 | # Bower dependency directory (https://bower.io/) 80 | bower_components 81 | 82 | # node-waf configuration 83 | .lock-wscript 84 | 85 | # Compiled binary addons (https://nodejs.org/api/addons.html) 86 | build/Release 87 | 88 | # Dependency directories 89 | node_modules/ 90 | jspm_packages/ 91 | 92 | # TypeScript v1 declaration files 93 | typings/ 94 | 95 | # TypeScript cache 96 | *.tsbuildinfo 97 | 98 | # Optional npm cache directory 99 | .npm 100 | 101 | # Optional eslint cache 102 | .eslintcache 103 | 104 | # Microbundle cache 105 | .rpt2_cache/ 106 | .rts2_cache_cjs/ 107 | .rts2_cache_es/ 108 | .rts2_cache_umd/ 109 | 110 | # Optional REPL history 111 | .node_repl_history 112 | 113 | # Output of 'npm pack' 114 | *.tgz 115 | 116 | # Yarn Integrity file 117 | .yarn-integrity 118 | 119 | # dotenv environment variables file 120 | .env 121 | .env.test 122 | 123 | # parcel-bundler cache (https://parceljs.org/) 124 | .cache 125 | 126 | # Next.js build output 127 | .next 128 | 129 | # Nuxt.js build / generate output 130 | .nuxt 131 | dist 132 | 133 | # Gatsby files 134 | .cache/ 135 | # Comment in the public line in if your project uses Gatsby and not Next.js 136 | # https://nextjs.org/blog/next-9-1#public-directory-support 137 | # public 138 | 139 | # vuepress build output 140 | .vuepress/dist 141 | 142 | # Serverless directories 143 | .serverless/ 144 | 145 | # FuseBox cache 146 | .fusebox/ 147 | 148 | # DynamoDB Local files 149 | .dynamodb/ 150 | 151 | # TernJS port file 152 | .tern-port 153 | 154 | # Stores VSCode versions used for testing VSCode extensions 155 | .vscode-test 156 | 157 | ### SublimeText ### 158 | # Cache files for Sublime Text 159 | *.tmlanguage.cache 160 | *.tmPreferences.cache 161 | *.stTheme.cache 162 | 163 | # Workspace files are user-specific 164 | *.sublime-workspace 165 | 166 | # Project files should be checked into the repository, unless a significant 167 | # proportion of contributors will probably not be using Sublime Text 168 | # *.sublime-project 169 | 170 | # SFTP configuration file 171 | sftp-config.json 172 | 173 | # Package control specific files 174 | Package Control.last-run 175 | Package Control.ca-list 176 | Package Control.ca-bundle 177 | Package Control.system-ca-bundle 178 | Package Control.cache/ 179 | Package Control.ca-certs/ 180 | Package Control.merged-ca-bundle 181 | Package Control.user-ca-bundle 182 | oscrypto-ca-bundle.crt 183 | bh_unicode_properties.cache 184 | 185 | # Sublime-github package stores a github token in this file 186 | # https://packagecontrol.io/packages/sublime-github 187 | GitHub.sublime-settings 188 | 189 | ### VisualStudioCode ### 190 | .vscode/* 191 | !.vscode/settings.json 192 | !.vscode/tasks.json 193 | !.vscode/launch.json 194 | !.vscode/extensions.json 195 | *.code-workspace 196 | 197 | ### VisualStudioCode Patch ### 198 | # Ignore all local history of files 199 | .history 200 | 201 | ### WebStorm ### 202 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 203 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 204 | 205 | # User-specific stuff 206 | .idea/**/workspace.xml 207 | .idea/**/tasks.xml 208 | .idea/**/usage.statistics.xml 209 | .idea/**/dictionaries 210 | .idea/**/shelf 211 | 212 | # Generated files 213 | .idea/**/contentModel.xml 214 | 215 | # Sensitive or high-churn files 216 | .idea/**/dataSources/ 217 | .idea/**/dataSources.ids 218 | .idea/**/dataSources.local.xml 219 | .idea/**/sqlDataSources.xml 220 | .idea/**/dynamic.xml 221 | .idea/**/uiDesigner.xml 222 | .idea/**/dbnavigator.xml 223 | 224 | # Gradle 225 | .idea/**/gradle.xml 226 | .idea/**/libraries 227 | 228 | # Gradle and Maven with auto-import 229 | # When using Gradle or Maven with auto-import, you should exclude module files, 230 | # since they will be recreated, and may cause churn. Uncomment if using 231 | # auto-import. 232 | # .idea/artifacts 233 | # .idea/compiler.xml 234 | # .idea/jarRepositories.xml 235 | # .idea/modules.xml 236 | # .idea/*.iml 237 | # .idea/modules 238 | # *.iml 239 | # *.ipr 240 | 241 | # CMake 242 | cmake-build-*/ 243 | 244 | # Mongo Explorer plugin 245 | .idea/**/mongoSettings.xml 246 | 247 | # File-based project format 248 | *.iws 249 | 250 | # IntelliJ 251 | out/ 252 | 253 | # mpeltonen/sbt-idea plugin 254 | .idea_modules/ 255 | 256 | # JIRA plugin 257 | atlassian-ide-plugin.xml 258 | 259 | # Cursive Clojure plugin 260 | .idea/replstate.xml 261 | 262 | # Crashlytics plugin (for Android Studio and IntelliJ) 263 | com_crashlytics_export_strings.xml 264 | crashlytics.properties 265 | crashlytics-build.properties 266 | fabric.properties 267 | 268 | # Editor-based Rest Client 269 | .idea/httpRequests 270 | 271 | # Android studio 3.1+ serialized cache file 272 | .idea/caches/build_file_checksums.ser 273 | 274 | ### WebStorm Patch ### 275 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 276 | 277 | # *.iml 278 | # modules.xml 279 | # .idea/misc.xml 280 | # *.ipr 281 | 282 | # Sonarlint plugin 283 | .idea/**/sonarlint/ 284 | 285 | # SonarQube Plugin 286 | .idea/**/sonarIssues.xml 287 | 288 | # Markdown Navigator plugin 289 | .idea/**/markdown-navigator.xml 290 | .idea/**/markdown-navigator-enh.xml 291 | .idea/**/markdown-navigator/ 292 | 293 | # Cache file creation bug 294 | # See https://youtrack.jetbrains.com/issue/JBR-2257 295 | .idea/$CACHE_FILE$ 296 | 297 | ### Windows ### 298 | # Windows thumbnail cache files 299 | Thumbs.db 300 | Thumbs.db:encryptable 301 | ehthumbs.db 302 | ehthumbs_vista.db 303 | 304 | # Dump file 305 | *.stackdump 306 | 307 | # Folder config file 308 | [Dd]esktop.ini 309 | 310 | # Recycle Bin used on file shares 311 | $RECYCLE.BIN/ 312 | 313 | # Windows Installer files 314 | *.cab 315 | *.msi 316 | *.msix 317 | *.msm 318 | *.msp 319 | 320 | # Windows shortcuts 321 | *.lnk 322 | 323 | # End of https://www.toptal.com/developers/gitignore/api/linux,macos,windows,sublimetext,webstorm,visualstudiocode,node 324 | 325 | # 326 | # Custom entries 327 | # 328 | 329 | # Dist directories built via the Typescript compiler 330 | dist/esm 331 | dist/cjs 332 | 333 | # Optional rdf-test-suite cache directory 334 | .rdf-test-suite-cache 335 | 336 | test/**/*.d.ts 337 | test/**/*.js.map 338 | test/backends/*.js 339 | test/browser/index.js 340 | test/browser/index.bundle.js 341 | test/others/*.js 342 | test/quadstore/*.js 343 | test/utils/*.js 344 | test/*.js 345 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # CHANGELOG 3 | 4 | ## 15.3.0 5 | 6 | - *[new]* added support for (de)serializing NaN literals (`xsd:float`, 7 | `xsd:double` but really any numeric type that can't be parsed into an 8 | actual number). 9 | (https://github.com/quadstorejs/quadstore/issues/176) 10 | (https://github.com/quadstorejs/quadstore/pull/177) 11 | 12 | ## 15.2.0 13 | 14 | - *[internal]* reduced duplication, looping and unnecessary allocations when 15 | reading from `AbstractIterator`. 16 | (https://github.com/quadstorejs/quadstore/pull/175) 17 | 18 | ## 15.1.0 19 | 20 | - *[internal]* updates to [abstract-level@3.x][15_1_0_1] 21 | 22 | [15_1_0_1]: https://github.com/Level/abstract-level/blob/main/CHANGELOG.md#310---2025-04-12 23 | 24 | ## 15.0.0 25 | 26 | - **[breaking]** drops CommonJS build, becoming an ESM-only package. 27 | 28 | ## 14.0.0 29 | 30 | - **[breaking]** updates to [abstract-level@2.x][14_1], drops support for the 31 | [`@nxtedition/rocksdb`][14_2] backend 32 | (https://github.com/quadstorejs/quadstore/issues/168) 33 | 34 | [14_1]: https://github.com/Level/abstract-level/blob/main/UPGRADING.md#200 35 | [14_2]: https://www.npmjs.com/package/@nxtedition/rocksdb 36 | 37 | ## 13.0.0 38 | 39 | - **[breaking]** switches to using radix 36 rather than radix 10 to encode 40 | the lengths of terms and quads in support of much longer literal terms. 41 | (https://github.com/quadstorejs/quadstore/pull/166) 42 | (https://github.com/quadstorejs/quadstore/issues/158) 43 | - *[internal]* adds [`@nxtedition/rocksdb`](https://www.npmjs.com/package/@nxtedition/rocksdb) 44 | to the list of backends covered by the test suite 45 | (https://github.com/quadstorejs/quadstore/issues/164) 46 | 47 | ## 12.1.0 48 | 49 | - *[fix]* fixes broken literal equality and matching 50 | (https://github.com/quadstorejs/quadstore/pull/161) 51 | (https://github.com/quadstorejs/quadstore/issues/160) 52 | Thanks [@Peeja](https://github.com/Peeja)! 53 | 54 | ## 12.0.2 55 | 56 | - *[internal]* updated dependencies to latest versions 57 | 58 | ## 12.0.0 59 | 60 | - **[breaking]** uses a simpler and faster (de)serialization technique 61 | that operates solely on keys, with no need for values 62 | (https://github.com/quadstorejs/quadstore/issues/157) 63 | - *[internal]* avoids repeated serializations of the same term 64 | (https://github.com/quadstorejs/quadstore/issues/156) 65 | - *[internal]* updated dependencies to latest versions 66 | 67 | ## 11.0.7 68 | 69 | - *[internal]* updated dependencies to latest versions 70 | 71 | ## 11.0.5 72 | 73 | - *[internal]* ported test suite to TypeScript and made it run both 74 | server-side (`mocha` using the `MemoryLevel` and `ClassicLevel` 75 | backends) and browser-side (`mocha` within a webpage loaded via 76 | `puppeteer` using the `MemoryLevel` and `BrowserLevel` backends) 77 | (https://github.com/quadstorejs/quadstore/issues/150) 78 | 79 | ## 11.0.3 80 | 81 | - *[fix]* fixes breaking serialization of terms that serialize to strings 82 | longer than 127 chars 83 | (https://github.com/quadstorejs/quadstore/issues/152) 84 | - *[internal]* replaces `asynciterator`'s `TransformIterator` with `wrap()` 85 | following upstream performance work 86 | (https://github.com/RubenVerborgh/AsyncIterator/issues/44) 87 | 88 | ## 11.0.0 89 | 90 | - **[breaking]** upgraded to the new generation of `*-level` packages 91 | (https://github.com/Level/community#how-do-i-upgrade-to-abstract-level) 92 | - **[breaking]** uses `Uint16Array` instead of `Buffer` for value 93 | (de)serialization 94 | - *[new]* adds support for ES modules through separate CJS and ESM builds 95 | (https://github.com/quadstorejs/quadstore/issues/138) 96 | - *[new]* adds support for Deno 97 | (https://github.com/quadstorejs/quadstore/issues/139) 98 | - *[internal]* moves performance tests to dedicated repository 99 | (https://github.com/quadstorejs/quadstore-perf) 100 | - *[internal]* upgrades to newer versions of `asynciterator` where 101 | we've done a lot of work on optimizing synchronous transforms 102 | (https://github.com/RubenVerborgh/AsyncIterator/issues/44#issuecomment-1201438495) 103 | 104 | ## 10.0.0 105 | 106 | - **[breaking]** drops all SPARQL-related features in favor of 107 | `quadstore-comunica`, for greater separation of concerns 108 | - **[breaking]** stops shipping `rdf-data-factory` as the default 109 | implementation of RDF/JS's `DataFactory` interface 110 | - *[fix]* throws when `backend` is not an instance of `AbstractLevelDOWN` 111 | (https://github.com/quadstorejs/quadstore/issues/140) 112 | - *[fix]* lands upstream fix to variable selection in SPARQL OPTIONAL clauses 113 | (https://github.com/quadstorejs/quadstore/issues/142) 114 | - *[internal]* replaces `immutable` and `decimal.js` with smaller 115 | alternatives in `quadstore-comunica`, dropping ~60 kB from the 116 | final bundle size 117 | (https://github.com/quadstorejs/quadstore/issues/143) 118 | - *[docs]* adds basic example to `README` 119 | (https://github.com/quadstorejs/quadstore/issues/145) 120 | 121 | ## 9.1.0 122 | 123 | - *[fix]* fixes missing dependencies used for type declarations 124 | (https://github.com/quadstorejs/quadstore/issues/136) 125 | 126 | ## 9.0.0 127 | 128 | - **[breaking]** removes support for DEFAULT vs. UNION default graph modes 129 | - *[fix]* fixes breaking blank node correlations 130 | (https://github.com/quadstorejs/quadstore/issues/134) 131 | - *[fix]* fixes repeated calls to `AbstractIterator#end()` 132 | - *[internal]* fixes duplicated typings for the comunica engine 133 | (https://github.com/quadstorejs/quadstore/issues/129) 134 | - *[internal]* offloads SPARQL UPDATE queries to Comunica 135 | - *[internal]* brings SPARQL spec compliance tests close to 100% passing 136 | 137 | ## 8.0.0 138 | 139 | - **[breaking]** deep revision of the serialization mechanism to remove 140 | duplicated quad serializations across indexes 141 | - **[breaking]** removes support for custom `separator` and `boundary` 142 | - **[breaking]** an instance of Comunica's `ActorInitSparql` must now be passed 143 | to the `Quadstore` constructor via the `opts` argument, adding support for 144 | Comunica configurations other than `quadstore-comunica` 145 | (https://github.com/quadstorejs/quadstore/issues/122) 146 | - *[fix]* fixes deep equality checks for literal terms in tests 147 | - *[fix]* re-enables the symmetric join actor in Comunica 148 | (https://github.com/joachimvh/asyncjoin/issues/7) 149 | - *[internal]* takes comparators out of the Quadstore class 150 | 151 | ## v7.3.1 152 | 153 | - *[fix]* fixes broken `JOIN` SPARQL queries when `approximateSize()` rounds to 0 154 | (https://github.com/quadstorejs/quadstore/pull/127) 155 | - *[fix]* fixes broken SPARQL queries due to Comunica operating in generalized 156 | RDF mode which can lead to literals being passed as the `subject` arg to 157 | `match()` 158 | (https://github.com/quadstorejs/quadstore/pull/127) 159 | 160 | ## v7.3.0 161 | 162 | - *[new]* quad scoping support via `initScope()`, `loadScope()`, 163 | `deleteScope()` and `deleteAllScopes()` methods 164 | (https://github.com/quadstorejs/quadstore/issues/124) 165 | - *[new]* added [`rocksdb`](https://github.com/level/rocksdb) to the list of 166 | tested backends 167 | 168 | ## v7.2.1 169 | 170 | - *[fix]* fixes broken browser builds due to naming collisions between nested 171 | webpack bundles 172 | (https://github.com/quadstorejs/quadstore-comunica/blob/5cfc803cb0864f089b07d3cf9850c0e377373e58/README.md#build) 173 | 174 | ## v7.2.0 175 | 176 | - *[fix]* fixes race condition within the `AsyncIterator` wrapper around `AbstractLevelIterator` 177 | (https://github.com/quadstorejs/quadstore/pull/125) 178 | - *[internal]* updates to `quadstore-comunica@0.2.0` (non-minified bundle) 179 | - *[internal]* updates third-party dependencies to their latest versions 180 | 181 | ## v7.1.1 182 | 183 | - *[fix]* fixes unsupported `DESCRIBE` SPARQL queries 184 | - *[fix]* fixes unsupported `ORDER BY` SPARQL expressions 185 | 186 | ## v7.1.0 187 | 188 | - *[new]* `preWrite` hook to support atomic writes of quads plus custom 189 | key-value pairs 190 | (https://github.com/quadstorejs/quadstore/pull/120) 191 | - *[fix]* prefix-based compaction/expansion of literal datatype IRIs 192 | (https://github.com/quadstorejs/quadstore/issues/118) 193 | - *[fix]* quadstore can now be bundles using browserify without the 194 | `ignoreMissing` configuration param 195 | (https://github.com/quadstorejs/quadstore/issues/117) 196 | - *[fix]* dropped indirect dev dependency on `@comunica/actor-init-sparql` 197 | (https://github.com/quadstorejs/quadstore/issues/116) 198 | 199 | ## v7.0.1 200 | 201 | - *[new]* added support for range queries 202 | - *[new]* added support for user-defined indexes 203 | - *[new]* added support for SPARQL queries via 204 | [quadstore-comunica](https://github.com/quadstorejs/quadstore-comunica) 205 | - **[breaking]** moved to using `master` as the development branch 206 | - **[breaking]** dropped support for matching terms in `del()` and `patch()` 207 | methods 208 | - **[breaking]** refactored `del()`, `put()` and `patch()` into single-quad and 209 | multi-quad variants (`multiDel()`, `multiPut()`, `multiPatch()`) 210 | - **[breaking]** refactored all APIs to return results wrapped in results objects 211 | - **[breaking]** refactored all streaming APIs to use `AsyncIterator` and 212 | asynchronous entrypoints with the exception of `RDF/JS` methods 213 | - **[breaking]** dropped support for the non-RDF API 214 | - **[breaking]** dropped support for previous implementation of custom indexes 215 | - **[breaking]** dropped support for callbacks 216 | - **[breaking]** refactored constructors to only use a single `opts` argument, 217 | passing the leveldb backend instance via the `opts.backend` property 218 | - **[breaking]** dropped support for the `ready` event, replaced by the 219 | `.open()` and `.close()` methods 220 | - *[internal]* dropped a few dependencies by pulling in the relevant code 221 | - *[internal]* added index-specific test suites 222 | - *[internal]* refactored folder structure 223 | - *[internal]* ported the whole project to Typescript 224 | 225 | ## v6.0.1 226 | 227 | - **[breaking]** drops pre-assembled browser bundle. 228 | - *[internal]* adds node.js 12 to Travis config. 229 | 230 | ## v5.2.0 231 | 232 | - *[internal]* updates outdated dependencies. 233 | 234 | ## v5.1.0 235 | 236 | - *[internal]* switches to `readable-stream` module as a substitute for the 237 | native `stream` module. 238 | 239 | ## v5.0.2 240 | 241 | - **[breaking]** extracts sparql and http support into separate packages 242 | 243 | ## v4.0.1 244 | 245 | - **[breaking]** updates leveldown, levelup 246 | - **[breaking]** changes the way `LevelDB`'s backends are passed to constructors 247 | - **[breaking]** makes `.sparql()` method `async` 248 | - *[internal]* updates `N3` to RDF/JS version 249 | - *[internal]* switches to the `@comunica-/actor-init-sparql-rdfjs` SPARQL engine 250 | - *[internal]* consolidates dependencies 251 | 252 | ## v3.0.0 253 | 254 | - deprecates `.query()` 255 | - adds support for SPARQL 256 | - adds HTTP API w/ LDF endpoint 257 | - improves test suite 258 | 259 | ## v2.1.1 260 | 261 | - new, (hopefully) cleaner API 262 | - `getStream()`, `putStream()` and `delStream()` methods 263 | - `query.get()`, `query().del()` methods 264 | - less code duplication 265 | - `master` branch now tracking NPM 266 | - work now happening in `devel` branch 267 | 268 | ## v1.0.0 269 | 270 | - default `contextKey` value changed from `context` to `graph` 271 | 272 | ## v0.2.1 273 | 274 | - replaces AsyncIterator instances w/ native stream.(Readable|Writable) instances 275 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | # MIT License 3 | 4 | Copyright © 2017 - 2023 Jacopo Scazzosi 5 | Copyright © 2017 - 2023 Matteo Murgida 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs" 5 | }, 6 | "exclude": [ 7 | "node_modules" 8 | ] 9 | } -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quadstorejs/quadstore/5d80da6f1915e7fde10b569799038641c1f5eda6/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quadstore", 3 | "version": "15.4.1", 4 | "description": "Quadstore is a LevelDB-backed RDF graph database / triplestore for JavaScript runtimes (browsers, Node.js, Deno, Bun, ...) that implements the RDF/JS interfaces and supports SPARQL queries and querying across named graphs.", 5 | "keywords": [ 6 | "node", 7 | "deno", 8 | "bun", 9 | "browser", 10 | "triplestore", 11 | "quadstore", 12 | "graph", 13 | "rdf", 14 | "store", 15 | "level", 16 | "leveldb", 17 | "database", 18 | "sparql", 19 | "rdfjs", 20 | "RDF/JS", 21 | "triple", 22 | "quad" 23 | ], 24 | "type": "module", 25 | "main": "./dist/index.js", 26 | "types": "./dist/index.d.ts", 27 | "scripts": { 28 | "clean": "rm -rf dist", 29 | "watch": "npm run clean && tsc --watch", 30 | "test:build": "cd test && tsc && cd browser && webpack -c webpack.config.cjs", 31 | "test:node": "cd test && mocha node.js --reporter spec", 32 | "test:browser": "cd test && node browser.js", 33 | "test": "npm run test:build && npm run test:node && npm run test:browser", 34 | "build": "npm run clean && tsc -p tsconfig.json", 35 | "publish:alpha": "npm run build && npm publish --tag alpha", 36 | "publish:beta": "npm run build && npm publish --tag beta" 37 | }, 38 | "files": [ 39 | "dist/**/*.js", 40 | "dist/**/*.d.ts" 41 | ], 42 | "repository": { 43 | "type": "git", 44 | "url": "https://github.com/quadstorejs/quadstore.git" 45 | }, 46 | "devDependencies": { 47 | "@types/chai": "^5.2.2", 48 | "@types/mocha": "^10.0.10", 49 | "@types/n3": "^1.26.0", 50 | "@types/node": "^24.8.1", 51 | "@types/node-static": "^0.7.11", 52 | "browser-level": "^3.0.0", 53 | "chai": "^6.2.0", 54 | "classic-level": "^3.0.0", 55 | "expect.js": "^0.3.1", 56 | "memory-level": "^3.1.0", 57 | "mocha": "^11.7.4", 58 | "n3": "^1.26.0", 59 | "node-static": "^0.7.11", 60 | "puppeteer": "^24.25.0", 61 | "rdf-data-factory": "^2.0.2", 62 | "typescript": "^5.9.3", 63 | "webpack": "^5.102.1", 64 | "webpack-cli": "^6.0.1" 65 | }, 66 | "dependencies": { 67 | "@rdfjs/types": "^2.0.1", 68 | "abstract-level": "^3.1.1", 69 | "asynciterator": "^3.10.0", 70 | "js-sorted-set": "^0.7.0" 71 | }, 72 | "author": "Jacopo Scazzosi ", 73 | "contributors": [ 74 | "Jacopo Scazzosi ", 75 | "Matteo Murgida " 76 | ], 77 | "engineStrict": true, 78 | "engines": { 79 | "node": ">=18.0.0" 80 | }, 81 | "license": "MIT", 82 | "homepage": "https://github.com/quadstorejs/quadstore", 83 | "bugs": "https://github.com/quadstorejs/quadstore/issues" 84 | } 85 | -------------------------------------------------------------------------------- /src/get/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import type { Quadstore } from '../quadstore.js'; 3 | import type { AsyncIterator } from 'asynciterator'; 4 | import type { 5 | ApproximateSizeResult, 6 | GetOpts, 7 | InternalIndex, 8 | Pattern, 9 | Prefixes, 10 | Quad, 11 | QuadStreamResultWithInternals, 12 | TermName, 13 | } from '../types/index.js'; 14 | import type { AbstractIteratorOptions } from 'abstract-level'; 15 | 16 | import { isPromise } from 'asynciterator'; 17 | import { ResultType, LevelQuery } from '../types/index.js'; 18 | import { arrStartsWith } from '../utils/stuff.js'; 19 | import { emptyObject, separator } from '../utils/constants.js'; 20 | import {quadReader, twoStepsQuadWriter, writePattern} from '../serialization/index.js'; 21 | import { SortingIterator } from './sortingiterator.js'; 22 | import { AbstractLevel } from 'abstract-level'; 23 | import { LevelIterator } from './leveliterator.js'; 24 | 25 | const SORTING_KEY = Symbol(); 26 | 27 | interface SortableQuad extends Quad { 28 | [SORTING_KEY]: string; 29 | } 30 | 31 | const compareSortableQuadsReverse = (left: SortableQuad, right: SortableQuad) => { 32 | return left[SORTING_KEY] > right[SORTING_KEY] ? -1 : 1; 33 | }; 34 | 35 | const compareSortableQuads = (left: SortableQuad, right: SortableQuad) => { 36 | return left[SORTING_KEY] > right[SORTING_KEY] ? 1 : -1; 37 | }; 38 | 39 | const emitSortableQuad = (item: SortableQuad): Quad => item; 40 | 41 | const getLevelQueryForIndex = (pattern: Pattern, index: InternalIndex, prefixes: Prefixes, opts: GetOpts): LevelQuery|null => { 42 | const indexQuery = writePattern(pattern, index, prefixes); 43 | if (indexQuery === null) { 44 | return null; 45 | } 46 | const levelOpts: AbstractIteratorOptions = { 47 | [indexQuery.gte ? 'gte' : 'gt']: indexQuery.gt, 48 | [indexQuery.lte ? 'lte' : 'lt']: indexQuery.lt, 49 | keys: true, 50 | values: false, 51 | keyEncoding: 'utf8', 52 | }; 53 | if (typeof opts.limit === 'number') { 54 | levelOpts.limit = opts.limit; 55 | } 56 | if (typeof opts.reverse === 'boolean') { 57 | levelOpts.reverse = opts.reverse; 58 | } 59 | return { level: levelOpts, order: indexQuery.order, index: indexQuery.index }; 60 | }; 61 | 62 | const getLevelQuery = (pattern: Pattern, indexes: InternalIndex[], prefixes: Prefixes, opts: GetOpts): LevelQuery|null => { 63 | for (let i = 0, index; i < indexes.length; i += 1) { 64 | index = indexes[i]; 65 | const levelQuery = getLevelQueryForIndex(pattern, index, prefixes, opts); 66 | if (levelQuery !== null && (!opts.order || arrStartsWith(levelQuery.order, opts.order))) { 67 | return levelQuery; 68 | } 69 | } 70 | return null; 71 | }; 72 | 73 | export const getStream = async (store: Quadstore, pattern: Pattern, opts: GetOpts): Promise => { 74 | const { dataFactory, prefixes, indexes } = store; 75 | 76 | const levelQueryFull = getLevelQuery(pattern, indexes, prefixes, opts); 77 | 78 | if (levelQueryFull !== null) { 79 | const { index, level, order } = levelQueryFull; 80 | let iterator: AsyncIterator = new LevelIterator( 81 | store.db.iterator(level), 82 | ([key]) => quadReader.read(key, index.prefix.length, index.terms, dataFactory, prefixes), 83 | opts.maxBufferSize, 84 | ); 85 | return { type: ResultType.QUADS, order, iterator, index: index.terms, resorted: false }; 86 | } 87 | 88 | const levelQueryNoOpts = getLevelQuery(pattern, indexes, prefixes, emptyObject); 89 | 90 | if (levelQueryNoOpts !== null) { 91 | const { index, level, order } = levelQueryNoOpts; 92 | let iterator: AsyncIterator = new LevelIterator( 93 | store.db.iterator(level), 94 | ([key]) => quadReader.read(key, index.prefix.length, index.terms, dataFactory, prefixes), 95 | opts.maxBufferSize, 96 | ); 97 | if (typeof opts.order !== 'undefined' && !arrStartsWith(opts.order, order)) { 98 | const digest = (item: Quad): SortableQuad => { 99 | (item as SortableQuad)[SORTING_KEY] = twoStepsQuadWriter.ingest(item, prefixes).write('', opts.order) + separator; 100 | return (item as SortableQuad); 101 | }; 102 | const compare = opts.reverse === true ? compareSortableQuadsReverse : compareSortableQuads; 103 | iterator = new SortingIterator(iterator, compare, digest, emitSortableQuad); 104 | if (typeof opts.limit !== 'undefined') { 105 | const onEndOrError = function (this: AsyncIterator) { 106 | this.removeListener('end', onEndOrError); 107 | this.removeListener('error', onEndOrError); 108 | this.destroy(); 109 | }; 110 | iterator = iterator.take(opts.limit) 111 | .on('end', onEndOrError) 112 | .on('error', onEndOrError); 113 | } 114 | } 115 | return {type: ResultType.QUADS, order: opts.order || order, iterator, index: index.terms, resorted: true }; 116 | } 117 | 118 | throw new Error(`No index compatible with pattern ${JSON.stringify(pattern)} and options ${JSON.stringify(opts)}`); 119 | }; 120 | 121 | interface AbstractLevelWithApproxSize extends AbstractLevel { 122 | approximateSize: (start: string, end: string) => Promise; 123 | } 124 | 125 | export const getApproximateSize = async (store: Quadstore, pattern: Pattern, opts: GetOpts): Promise => { 126 | if (!(store.db as AbstractLevelWithApproxSize).approximateSize) { 127 | return { type: ResultType.APPROXIMATE_SIZE, approximateSize: Infinity }; 128 | } 129 | const { indexes, prefixes } = store; 130 | const levelQuery = getLevelQuery(pattern, indexes, prefixes, opts); 131 | if (levelQuery === null) { 132 | throw new Error(`No index compatible with pattern ${JSON.stringify(pattern)} and options ${JSON.stringify(opts)}`); 133 | } 134 | const { level } = levelQuery; 135 | const start = level.gte || level.gt; 136 | const end = level.lte || level.lt; 137 | const approximateSize = await (store.db as AbstractLevelWithApproxSize).approximateSize(start, end); 138 | return { 139 | type: ResultType.APPROXIMATE_SIZE, 140 | approximateSize: Math.max(1, approximateSize), 141 | }; 142 | }; 143 | -------------------------------------------------------------------------------- /src/get/leveliterator.ts: -------------------------------------------------------------------------------- 1 | 2 | import type { AbstractIterator } from 'abstract-level'; 3 | 4 | import { AsyncIterator } from 'asynciterator'; 5 | 6 | export type MappingFn = (val: I) => O; 7 | 8 | export class LevelIterator extends AsyncIterator { 9 | 10 | #source: AbstractIterator; 11 | #sourceEnded: boolean; 12 | #mapper: MappingFn<[K, V], T>; 13 | #bufsize: number; 14 | #nextbuf: [K, V][] | null; 15 | #currbuf: [K, V][] | null; 16 | #curridx: number; 17 | #loading: boolean; 18 | 19 | constructor(source: AbstractIterator, mapper: MappingFn<[K, V], T>, maxBufferSize: number = 256) { 20 | super(); 21 | this.#source = source; 22 | this.#sourceEnded = false; 23 | this.#mapper = mapper; 24 | this.#bufsize = maxBufferSize; 25 | this.#currbuf = null; 26 | this.#nextbuf = null; 27 | this.#curridx = 0; 28 | this.#loading = false; 29 | this.readable = false; 30 | queueMicrotask(this.#loadNextBuffer); 31 | } 32 | 33 | read(): T | null { 34 | let item: T | null = null; 35 | if (!this.#currbuf) { 36 | this.#currbuf = this.#nextbuf; 37 | this.#nextbuf = null; 38 | this.#curridx = 0; 39 | this.#loadNextBuffer(); 40 | } 41 | if (this.#currbuf) { 42 | if (this.#curridx < this.#currbuf.length) { 43 | item = this.#mapper(this.#currbuf[this.#curridx++]); 44 | } 45 | if (this.#curridx === this.#currbuf.length) { 46 | this.#currbuf = null; 47 | } 48 | } 49 | if (item === null) { 50 | this.readable = false; 51 | if (this.#sourceEnded) { 52 | this.close(); 53 | } 54 | } 55 | return item; 56 | } 57 | 58 | #loadNextBuffer = () => { 59 | if (!this.#loading && !this.#nextbuf) { 60 | this.#loading = true; 61 | this.#source.nextv(this.#bufsize) 62 | .then(this.#onNextBuffer) 63 | .catch(this.#onNextBufferError); 64 | } 65 | } 66 | 67 | #onNextBuffer = (entries: [K, V][]) => { 68 | this.#loading = false; 69 | if (entries.length) { 70 | this.readable = true; 71 | this.#nextbuf = entries; 72 | } else { 73 | this.#nextbuf = null; 74 | this.#sourceEnded = true; 75 | if (!this.#currbuf) { 76 | this.close(); 77 | } 78 | } 79 | } 80 | 81 | #onNextBufferError = (err: Error) => { 82 | this.#loading = false; 83 | this.#sourceEnded = true; 84 | if (!this.#currbuf) { 85 | this.close(); 86 | } 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/get/sortingiterator.ts: -------------------------------------------------------------------------------- 1 | 2 | // @ts-ignore 3 | import SortedSet from 'js-sorted-set'; 4 | import { AsyncIterator } from 'asynciterator'; 5 | 6 | /** 7 | * Buffers all items emitted from `source` and sorts them according to 8 | * `compare`. 9 | */ 10 | export class SortingIterator extends AsyncIterator { 11 | 12 | /** 13 | * 14 | * @param source 15 | * @param compare 16 | * @param digest 17 | * @param emit 18 | */ 19 | public constructor( 20 | source: AsyncIterator, 21 | compare: (left: Int, right: Int) => number, 22 | digest: (item: In) => Int, 23 | emit: (item: Int) => Out, 24 | ) { 25 | 26 | super(); 27 | 28 | let iterator: any; 29 | 30 | const startBuffering = () => { 31 | const set = new SortedSet({ comparator: compare }); 32 | const cleanup = () => { 33 | source.removeListener('data', onData); 34 | source.removeListener('error', onError); 35 | source.removeListener('end', onEnd); 36 | source.destroy(); 37 | }; 38 | const onData = (item: In) => { 39 | set.insert(digest(item)); 40 | }; 41 | const onError = (err: Error) => { 42 | cleanup(); 43 | this.emit('error', err); 44 | }; 45 | const onEnd = () => { 46 | cleanup(); 47 | iterator = set.beginIterator(); 48 | this.readable = true; 49 | }; 50 | source.on('data', onData); 51 | source.on('error', onError); 52 | source.on('end', onEnd); 53 | }; 54 | 55 | this.read = (): Out | null => { 56 | if (iterator) { 57 | const value = iterator.value(); 58 | if (value === null) { 59 | this.close(); 60 | return null; 61 | } 62 | iterator = iterator.next(); 63 | return emit(value); 64 | } 65 | this.readable = false; 66 | return null; 67 | }; 68 | 69 | queueMicrotask(startBuffering); 70 | 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './types/index.js'; 3 | export { Quadstore } from './quadstore.js'; 4 | export { getTermComparator, getQuadComparator } from './utils/comparators.js'; 5 | -------------------------------------------------------------------------------- /src/quadstore.ts: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | import type { 5 | DataFactory, 6 | Quad, 7 | Quad_Graph, 8 | Quad_Object, 9 | Quad_Predicate, 10 | Quad_Subject, 11 | Store, 12 | Stream, 13 | } from '@rdfjs/types'; 14 | import type { 15 | DelStreamOpts, 16 | BatchOpts, 17 | DelOpts, 18 | PutOpts, 19 | PatchOpts, 20 | GetOpts, 21 | InternalIndex, 22 | PutStreamOpts, 23 | Pattern, 24 | StoreOpts, 25 | VoidResult, 26 | StreamLike, 27 | TermName, 28 | Prefixes, 29 | QuadArrayResultWithInternals, 30 | QuadStreamResultWithInternals, 31 | } from './types/index.js'; 32 | import { ResultType } from './types/index.js'; 33 | import type { 34 | AbstractChainedBatch, 35 | AbstractLevel, 36 | } from 'abstract-level'; 37 | import { EventEmitter } from 'events'; 38 | import { 39 | AsyncIterator, 40 | EmptyIterator, 41 | wrap, 42 | } from 'asynciterator'; 43 | import { 44 | streamToArray, 45 | ensureAbstractLevel, 46 | } from './utils/stuff.js'; 47 | import { 48 | emptyObject, 49 | defaultIndexes, 50 | separator, 51 | levelPutOpts, 52 | levelDelOpts, emptyValue, 53 | } from './utils/constants.js'; 54 | import { consumeOneByOne } from './utils/consumeonebyone.js'; 55 | import { consumeInBatches } from './utils/consumeinbatches.js'; 56 | import { uid } from './utils/uid.js'; 57 | import { getApproximateSize, getStream } from './get/index.js'; 58 | import { Scope } from './scope/index.js'; 59 | import {twoStepsQuadWriter} from './serialization/quads.js'; 60 | 61 | export class Quadstore implements Store { 62 | 63 | readonly db: AbstractLevel; 64 | 65 | readonly indexes: InternalIndex[]; 66 | readonly id: string; 67 | 68 | readonly prefixes: Prefixes; 69 | 70 | readonly dataFactory: DataFactory; 71 | 72 | constructor(opts: StoreOpts) { 73 | ensureAbstractLevel(opts.backend, '"opts.backend"'); 74 | this.dataFactory = opts.dataFactory; 75 | this.db = opts.backend; 76 | this.indexes = []; 77 | this.id = uid(); 78 | (opts.indexes || defaultIndexes) 79 | .forEach((index: TermName[]) => this._addIndex(index)); 80 | this.prefixes = opts.prefixes || { 81 | expandTerm: term => term, 82 | compactIri: iri => iri, 83 | }; 84 | } 85 | 86 | protected ensureReady() { 87 | if (this.db.status !== 'open') { 88 | throw new Error(`Store is not ready (status: "${this.db.status}"). Did you call store.open()?`); 89 | } 90 | } 91 | 92 | async open() { 93 | if (this.db.status !== 'open') { 94 | await this.db.open(); 95 | } 96 | } 97 | 98 | async close() { 99 | if (this.db.status !== 'closed') { 100 | await this.db.close(); 101 | } 102 | } 103 | 104 | toString() { 105 | return this.toJSON(); 106 | } 107 | 108 | toJSON() { 109 | return `[object ${this.constructor.name}::${this.id}]`; 110 | } 111 | 112 | _addIndex(terms: TermName[]): void { 113 | const name = terms.map(t => t.charAt(0).toUpperCase()).join(''); 114 | this.indexes.push({ 115 | terms, 116 | prefix: name + separator, 117 | }); 118 | } 119 | 120 | async clear(): Promise { 121 | await this.db.clear(); 122 | } 123 | 124 | match(subject?: Quad_Subject, predicate?: Quad_Predicate, object?: Quad_Object, graph?: Quad_Graph, opts: GetOpts = emptyObject): AsyncIterator { 125 | // This is required due to the fact that Comunica may invoke the `.match()` 126 | // method in generalized RDF mode, under which the subject may be a literal 127 | // term. 128 | // @ts-ignore 129 | if (subject && subject.termType === 'Literal') { 130 | return new EmptyIterator(); 131 | } 132 | const pattern: Pattern = { subject, predicate, object, graph }; 133 | return wrap(this.getStream(pattern, opts).then(results => results.iterator)); 134 | } 135 | 136 | async countQuads(subject?: Quad_Subject, predicate?: Quad_Predicate, object?: Quad_Object, graph?: Quad_Graph, opts: GetOpts = emptyObject): Promise { 137 | // This is required due to the fact that Comunica may invoke the `.match()` 138 | // method in generalized RDF mode, under which the subject may be a literal 139 | // term. 140 | // @ts-ignore 141 | if (subject && subject.termType === 'Literal') { 142 | return 0; 143 | } 144 | const pattern: Pattern = { subject, predicate, object, graph }; 145 | const results = await this.getApproximateSize(pattern, opts); 146 | return results.approximateSize; 147 | } 148 | 149 | import(source: StreamLike): EventEmitter { 150 | const emitter = new EventEmitter(); 151 | this.putStream(source, {}) 152 | .then(() => { emitter.emit('end'); }) 153 | .catch((err) => { emitter.emit('error', err); }); 154 | return emitter; 155 | } 156 | 157 | remove(source: StreamLike): EventEmitter { 158 | const emitter = new EventEmitter(); 159 | this.delStream(source, {}) 160 | .then(() => emitter.emit('end')) 161 | .catch((err) => emitter.emit('error', err)); 162 | return emitter; 163 | } 164 | 165 | removeMatches(subject?: Quad_Subject, predicate?: Quad_Predicate, object?: Quad_Object, graph?: Quad_Graph, opts: GetOpts = emptyObject) { 166 | const source = this.match(subject, predicate, object, graph, opts); 167 | return this.remove(source); 168 | } 169 | 170 | deleteGraph(graph: Quad_Graph) { 171 | return this.removeMatches(undefined, undefined, undefined, graph); 172 | } 173 | 174 | async getApproximateSize(pattern: Pattern, opts: GetOpts = emptyObject) { 175 | this.ensureReady(); 176 | return await getApproximateSize(this, pattern, opts); 177 | } 178 | 179 | private _batchPut(quad: Quad, batch: AbstractChainedBatch): AbstractChainedBatch { 180 | const { indexes, prefixes } = this; 181 | twoStepsQuadWriter.ingest(quad, prefixes); 182 | for (let i = 0, il = indexes.length, index; i < il; i += 1) { 183 | index = indexes[i]; 184 | const key = twoStepsQuadWriter.write(index.prefix, index.terms); 185 | batch = batch.put(key, emptyValue, levelPutOpts); 186 | } 187 | return batch; 188 | } 189 | 190 | async put(quad: Quad, opts: PutOpts = emptyObject): Promise { 191 | this.ensureReady(); 192 | const { indexes, db } = this; 193 | let batch = db.batch(); 194 | if (opts.scope) { 195 | quad = opts.scope.parseQuad(quad, batch); 196 | } 197 | this._batchPut(quad, batch); 198 | await this.writeBatch(batch, opts); 199 | return { type: ResultType.VOID }; 200 | } 201 | 202 | async multiPut(quads: Quad[], opts: PutOpts = emptyObject): Promise { 203 | this.ensureReady(); 204 | const { indexes, db } = this; 205 | let batch = db.batch(); 206 | for (let q = 0, ql = quads.length, quad; q < ql; q += 1) { 207 | quad = quads[q]; 208 | if (opts.scope) { 209 | quad = opts.scope.parseQuad(quad, batch); 210 | } 211 | this._batchPut(quad, batch); 212 | } 213 | await this.writeBatch(batch, opts); 214 | return { type: ResultType.VOID }; 215 | } 216 | 217 | private _batchDel(quad: Quad, batch: AbstractChainedBatch): AbstractChainedBatch { 218 | const { indexes, prefixes } = this; 219 | twoStepsQuadWriter.ingest(quad, prefixes); 220 | for (let i = 0, il = indexes.length, index; i < il; i += 1) { 221 | index = indexes[i]; 222 | const key = twoStepsQuadWriter.write(index.prefix, index.terms); 223 | batch = batch.del(key, levelDelOpts); 224 | } 225 | return batch; 226 | } 227 | 228 | async del(quad: Quad, opts: DelOpts = emptyObject): Promise { 229 | this.ensureReady(); 230 | const batch = this.db.batch(); 231 | this._batchDel(quad, batch); 232 | await this.writeBatch(batch, opts); 233 | return { type: ResultType.VOID }; 234 | } 235 | 236 | async multiDel(quads: Quad[], opts: DelOpts = emptyObject): Promise { 237 | this.ensureReady(); 238 | const batch = this.db.batch(); 239 | for (let q = 0, ql = quads.length, quad; q < ql; q += 1) { 240 | quad = quads[q]; 241 | this._batchDel(quad, batch); 242 | } 243 | await this.writeBatch(batch, opts); 244 | return { type: ResultType.VOID }; 245 | } 246 | 247 | async patch(oldQuad: Quad, newQuad: Quad, opts: PatchOpts = emptyObject): Promise { 248 | this.ensureReady(); 249 | const { indexes, db } = this; 250 | const batch = db.batch(); 251 | this._batchDel(oldQuad, batch); 252 | this._batchPut(newQuad, batch); 253 | await this.writeBatch(batch, opts); 254 | return { type: ResultType.VOID }; 255 | } 256 | 257 | async multiPatch(oldQuads: Quad[], newQuads: Quad[], opts: PatchOpts = emptyObject): Promise { 258 | this.ensureReady(); 259 | const { indexes, db } = this; 260 | let batch = db.batch(); 261 | for (let oq = 0, oql = oldQuads.length, oldQuad; oq < oql; oq += 1) { 262 | oldQuad = oldQuads[oq]; 263 | this._batchDel(oldQuad, batch); 264 | } 265 | for (let nq = 0, nql = newQuads.length, newQuad; nq < nql; nq += 1) { 266 | newQuad = newQuads[nq]; 267 | this._batchPut(newQuad, batch); 268 | } 269 | await this.writeBatch(batch, opts); 270 | return { type: ResultType.VOID }; 271 | } 272 | 273 | private async writeBatch(batch: AbstractChainedBatch, opts: BatchOpts) { 274 | if (opts.preWrite) { 275 | await opts.preWrite(batch); 276 | } 277 | await batch.write(); 278 | } 279 | 280 | async get(pattern: Pattern, opts: GetOpts = emptyObject): Promise { 281 | this.ensureReady(); 282 | const results = await this.getStream(pattern, opts); 283 | const items: Quad[] = await streamToArray(results.iterator); 284 | return { 285 | items, 286 | type: results.type, 287 | order: results.order, 288 | index: results.index, 289 | resorted: results.resorted, 290 | }; 291 | } 292 | 293 | async getStream(pattern: Pattern, opts: GetOpts = emptyObject): Promise { 294 | this.ensureReady(); 295 | return await getStream(this, pattern, opts); 296 | } 297 | 298 | async putStream(source: StreamLike, opts: PutStreamOpts = emptyObject): Promise { 299 | this.ensureReady(); 300 | const batchSize = opts.batchSize || 1; 301 | if (batchSize === 1) { 302 | await consumeOneByOne(source, quad => this.put(quad, opts)); 303 | } else { 304 | await consumeInBatches(source, batchSize, quads => this.multiPut(quads, opts)); 305 | } 306 | return { type: ResultType.VOID }; 307 | } 308 | 309 | async delStream(source: StreamLike, opts: DelStreamOpts = emptyObject): Promise { 310 | this.ensureReady(); 311 | const batchSize = opts.batchSize || 1; 312 | if (batchSize === 1) { 313 | await consumeOneByOne(source, quad => this.del(quad)); 314 | } else { 315 | await consumeInBatches(source, batchSize, quads => this.multiDel(quads)); 316 | } 317 | return { type: ResultType.VOID }; 318 | } 319 | 320 | async initScope(): Promise { 321 | await this.ensureReady(); 322 | return await Scope.init(this); 323 | } 324 | 325 | async loadScope(scopeId: string): Promise { 326 | await this.ensureReady(); 327 | return await Scope.load(this, scopeId); 328 | } 329 | 330 | async deleteScope(scopeId: string): Promise { 331 | await this.ensureReady(); 332 | await Scope.delete(this, scopeId); 333 | } 334 | 335 | async deleteAllScopes(): Promise { 336 | await this.ensureReady(); 337 | await Scope.delete(this); 338 | } 339 | 340 | } 341 | -------------------------------------------------------------------------------- /src/scope/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import type { DataFactory, Quad, BlankNode, Quad_Subject, Quad_Object, Quad_Graph } from '@rdfjs/types'; 3 | import type { AbstractChainedBatch } from 'abstract-level'; 4 | import type { Quadstore } from '../quadstore.js'; 5 | 6 | import { consumeOneByOne } from '../utils/consumeonebyone.js'; 7 | import { uid } from '../utils/uid.js'; 8 | import { separator, boundary } from '../utils/constants.js'; 9 | import { LevelIterator } from '../get/leveliterator.js'; 10 | 11 | export type ScopeLabelMapping = [string, string]; 12 | 13 | export class Scope { 14 | 15 | readonly id: string; 16 | 17 | readonly blankNodes: Map; 18 | readonly factory: DataFactory; 19 | 20 | static async init(store: Quadstore): Promise { 21 | return new Scope(store.dataFactory, uid(), new Map()); 22 | } 23 | 24 | static async load(store: Quadstore, scopeId: string): Promise { 25 | const levelOpts = Scope.getLevelIteratorOpts(false, true, scopeId); 26 | const iterator = new LevelIterator( 27 | store.db.iterator(levelOpts), 28 | ([key, value]) => JSON.parse(value) as ScopeLabelMapping, 29 | ); 30 | const blankNodes: Map = new Map(); 31 | const { dataFactory: factory } = store; 32 | await consumeOneByOne(iterator, (mapping) => { 33 | blankNodes.set(mapping[0], factory.blankNode(mapping[1])); 34 | }); 35 | return new Scope(factory, scopeId, blankNodes); 36 | } 37 | 38 | static async delete(store: Quadstore, scopeId?: string): Promise { 39 | const batch = store.db.batch(); 40 | const levelOpts = Scope.getLevelIteratorOpts(true, false, scopeId); 41 | const iterator = new LevelIterator( 42 | store.db.iterator(levelOpts), 43 | ([key, value]) => key, 44 | ); 45 | await consumeOneByOne(iterator, (key: string) => { 46 | batch.del(key); 47 | }); 48 | await batch.write(); 49 | } 50 | 51 | static getLevelIteratorOpts(keys: boolean, values: boolean, scopeId?: string) { 52 | const gte = scopeId 53 | ? `SCOPE${separator}${scopeId}${separator}` 54 | : `SCOPE${separator}`; 55 | return { 56 | keys, 57 | values, 58 | keyEncoding: 'utf8', 59 | valueEncoding: 'utf8', 60 | gte, 61 | lte: `${gte}${boundary}`, 62 | }; 63 | } 64 | 65 | static addMappingToLevelBatch(scopeId: string, batch: AbstractChainedBatch, originalLabel: string, randomLabel: string) { 66 | batch.put(`SCOPE${separator}${scopeId}${separator}${originalLabel}`, JSON.stringify([originalLabel, randomLabel])); 67 | } 68 | 69 | constructor(factory: DataFactory, id: string, blankNodes: Map) { 70 | this.blankNodes = blankNodes; 71 | this.factory = factory; 72 | this.id = id; 73 | } 74 | 75 | private parseBlankNode(node: BlankNode, batch: AbstractChainedBatch): BlankNode { 76 | let cachedNode = this.blankNodes.get(node.value); 77 | if (!cachedNode) { 78 | cachedNode = this.factory.blankNode(uid()); 79 | this.blankNodes.set(node.value, cachedNode); 80 | Scope.addMappingToLevelBatch(this.id, batch, node.value, cachedNode.value); 81 | } 82 | return cachedNode; 83 | } 84 | 85 | private parseSubject(node: Quad_Subject, batch: AbstractChainedBatch): Quad_Subject { 86 | switch (node.termType) { 87 | case 'BlankNode': 88 | return this.parseBlankNode(node, batch); 89 | default: 90 | return node; 91 | } 92 | } 93 | 94 | private parseObject(node: Quad_Object, batch: AbstractChainedBatch): Quad_Object { 95 | switch (node.termType) { 96 | case 'BlankNode': 97 | return this.parseBlankNode(node, batch); 98 | default: 99 | return node; 100 | } 101 | } 102 | 103 | private parseGraph(node: Quad_Graph, batch: AbstractChainedBatch): Quad_Graph { 104 | switch (node.termType) { 105 | case 'BlankNode': 106 | return this.parseBlankNode(node, batch); 107 | default: 108 | return node; 109 | } 110 | } 111 | 112 | parseQuad(quad: Quad, batch: AbstractChainedBatch): Quad { 113 | return this.factory.quad( 114 | this.parseSubject(quad.subject, batch), 115 | quad.predicate, 116 | this.parseObject(quad.object, batch), 117 | this.parseGraph(quad.graph, batch), 118 | ); 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /src/serialization/fpstring.ts: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * License: MIT 4 | * Author: Jacopo Scazzosi 5 | * 6 | * The content of this file is an implementation of an encoding 7 | * technique for floating point numbers. Given a set of floating 8 | * point values, their encoded strings are such that their 9 | * lexicographical ordering follows the numerical ordering of the 10 | * original values. 11 | * 12 | * The specific encoding technique is based on the internet draft 13 | * "Directory string representation for floating point values" by 14 | * Doug Wood of Tivoli Systems Inc., submitted in Dec, 1999. 15 | * 16 | * https://tools.ietf.org/html/draft-wood-ldapext-float-00 17 | * 18 | * The resulting strings follow the pattern described in the draft: 19 | * 20 | * +-+-+---+-+------------------+ 21 | * | | |Exp| | Mantissa | 22 | * +-+-+---+-+------------------+ 23 | * |c| |nnn| |n.nnnnnnnnnnnnnnnn| 24 | * +-+-+---+-+------------------+ 25 | * 26 | * The first character identifies which case the original number 27 | * belongs to, which determines whether the exponent and mantissa 28 | * have been flipped and/or inverted: 29 | * 30 | * +------+------------+-----------------------------------------+--------------------+------------+ 31 | * | CASE | RANGE | MANTISSA AND EXPONENT SIGNS | FLIPS | INVERSIONS | 32 | * +------+------------+-----------------------------------------+--------------------+------------+ 33 | * | 0 | -Infinity | | | | 34 | * +------+------------+-----------------------------------------+--------------------+------------+ 35 | * | 1 | X <= -1 | negative mantissa and positive exponent | mantissa, exponent | | 36 | * +------+------------+-----------------------------------------+--------------------+------------+ 37 | * | 2 | 0 < X < -1 | negative mantissa and negative exponent | mantissa | exponent | 38 | * +------+------------+-----------------------------------------+--------------------+------------+ 39 | * | 3 | X = 0 | | | | 40 | * +------+------------+-----------------------------------------+--------------------+------------+ 41 | * | 4 | 0 < X < 1 | positive mantissa and negative exponent | exponent | exponent | 42 | * +------+------------+-----------------------------------------+--------------------+------------+ 43 | * | 5 | X >= 1 | positive mantissa and positive exponent | mantissa, exponent | | 44 | * +------+------------+-----------------------------------------+--------------------+------------+ 45 | * | 6 | Infinity | | | | 46 | * +------+------------+-----------------------------------------+--------------------+------------+ 47 | * | 7 | NaN | | | | 48 | * +------+------------+-----------------------------------------+--------------------+------------+ 49 | * 50 | */ 51 | 52 | 53 | const join = (encodingCase: number, exponent: number, mantissa: number): string => { 54 | let r = '' + encodingCase; 55 | if (exponent < 10) { 56 | r += '00' + exponent; 57 | } else if (exponent < 100) { 58 | r += '0' + exponent; 59 | } else { 60 | r += exponent; 61 | } 62 | r += mantissa.toFixed(17); 63 | return r; 64 | }; 65 | 66 | const ZERO = join(3, 0, 0); 67 | const NEG_INF = join(0, 0, 0); 68 | const POS_INF = join(6, 0, 0); 69 | const NAN = join(7, 0, 0); 70 | 71 | export const encode = (stringOrNumber: string|number): string => { 72 | 73 | let mantissa: number = typeof stringOrNumber !== 'number' 74 | ? parseFloat(stringOrNumber) 75 | : stringOrNumber; 76 | 77 | if (Number.isNaN(mantissa)) { 78 | return NAN; 79 | } 80 | 81 | if (mantissa === -Infinity) { 82 | return NEG_INF; 83 | } 84 | 85 | if (mantissa === Infinity) { 86 | return POS_INF; 87 | } 88 | 89 | if (mantissa === 0) { 90 | return ZERO; 91 | } 92 | 93 | let exponent = 0 94 | let sign = 0 95 | 96 | if (mantissa < 0) { 97 | sign = 1; 98 | mantissa *= -1; 99 | } 100 | 101 | while (mantissa > 10) { 102 | mantissa /= 10; 103 | exponent += 1; 104 | } 105 | 106 | while (mantissa < 1) { 107 | mantissa *= 10; 108 | exponent -= 1; 109 | } 110 | 111 | if (sign === 1) { 112 | if (exponent >= 0) { 113 | return join(1, 999 - exponent, 10 - mantissa); 114 | } else { 115 | return join(2, exponent * -1, 10 - mantissa); 116 | } 117 | } else { 118 | if (exponent < 0) { 119 | return join(4, 999 + exponent, mantissa); 120 | } else { 121 | return join(5, exponent, mantissa); 122 | } 123 | } 124 | 125 | }; 126 | -------------------------------------------------------------------------------- /src/serialization/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export { twoStepsQuadWriter, quadReader } from './quads.js'; 3 | export { writePattern } from './patterns.js'; 4 | -------------------------------------------------------------------------------- /src/serialization/patterns.ts: -------------------------------------------------------------------------------- 1 | 2 | import type { Literal } from '@rdfjs/types'; 3 | import type {InternalIndex, Pattern, Prefixes, IndexQuery, SerializedTerm} from '../types/index.js'; 4 | 5 | import * as xsd from './xsd.js'; 6 | import { encode } from './fpstring.js'; 7 | import { separator, boundary } from '../utils/constants.js'; 8 | import { blankNodeWriter, defaultGraphWriter, genericLiteralWriter, langStringLiteralWriter, namedNodeWriter, 9 | numericLiteralWriter, stringLiteralWriter } from './terms.js'; 10 | 11 | const serialized: SerializedTerm = { 12 | type: '', 13 | value: '', 14 | lengths: '', 15 | }; 16 | 17 | const patternLiteralWriter = { 18 | write(term: Literal, prefixes: Prefixes) { 19 | if (term.language) { 20 | langStringLiteralWriter.write(term, serialized, prefixes); 21 | return; 22 | } 23 | if (term.datatype) { 24 | switch (term.datatype.value) { 25 | case xsd.string: 26 | stringLiteralWriter.write(term, serialized, prefixes); 27 | return; 28 | case xsd.integer: 29 | case xsd.double: 30 | case xsd.decimal: 31 | case xsd.nonPositiveInteger: 32 | case xsd.negativeInteger: 33 | case xsd.long: 34 | case xsd.int: 35 | case xsd.short: 36 | case xsd.byte: 37 | case xsd.nonNegativeInteger: 38 | case xsd.unsignedLong: 39 | case xsd.unsignedInt: 40 | case xsd.unsignedShort: 41 | case xsd.unsignedByte: 42 | case xsd.positiveInteger: 43 | numericLiteralWriter.write(term, serialized, prefixes, true, encode(term.value)); 44 | return; 45 | case xsd.dateTime: 46 | numericLiteralWriter.write(term, serialized, prefixes, true, encode(new Date(term.value).valueOf())); 47 | return; 48 | default: 49 | genericLiteralWriter.write(term, serialized, prefixes); 50 | return; 51 | } 52 | } 53 | stringLiteralWriter.write(term, serialized, prefixes); 54 | return; 55 | } 56 | }; 57 | 58 | export const writePattern = (pattern: Pattern, index: InternalIndex, prefixes: Prefixes): IndexQuery|null => { 59 | let gt = index.prefix; 60 | let lt = index.prefix; 61 | let gte = true; 62 | let lte = true; 63 | let didRange = false; 64 | let didLiteral = false; 65 | let remaining = Object.entries(pattern).filter(([termName, term]) => term).length; 66 | if (remaining === 0) { 67 | lt += boundary; 68 | return { gt, lt, gte, lte, order: index.terms, index }; 69 | } 70 | let t = 0; 71 | for (; t < index.terms.length && remaining > 0; t += 1) { 72 | const term = pattern[index.terms[t]]; 73 | if (!term) { 74 | return null; 75 | } 76 | if (didRange || didLiteral) { 77 | return null; 78 | } 79 | switch (term.termType) { 80 | case 'Range': 81 | didRange = true; 82 | if (term.gt) { 83 | patternLiteralWriter.write(term.gt, prefixes); 84 | gt += serialized.value; 85 | gte = false; 86 | } else if (term.gte) { 87 | patternLiteralWriter.write(term.gte, prefixes); 88 | gt += serialized.value; 89 | gte = true; 90 | } 91 | if (term.lt) { 92 | patternLiteralWriter.write(term.lt, prefixes); 93 | lt += serialized.value; 94 | lte = false; 95 | } else if (term.lte) { 96 | patternLiteralWriter.write(term.lte, prefixes); 97 | lt += serialized.value; 98 | lte = true; 99 | } 100 | break; 101 | case 'Literal': 102 | didLiteral = true; 103 | patternLiteralWriter.write(term, prefixes); 104 | gt += serialized.value; 105 | gte = true; 106 | patternLiteralWriter.write(term, prefixes); 107 | lt += serialized.value; 108 | lte = true; 109 | break; 110 | case 'NamedNode': 111 | namedNodeWriter.write(term, serialized, prefixes); 112 | gt += serialized.value; 113 | gte = true; 114 | namedNodeWriter.write(term, serialized, prefixes); 115 | lt += serialized.value; 116 | lte = true; 117 | break; 118 | case 'BlankNode': 119 | blankNodeWriter.write(term, serialized, prefixes); 120 | gt += serialized.value; 121 | gte = true; 122 | blankNodeWriter.write(term, serialized, prefixes); 123 | lt += serialized.value; 124 | lte = true; 125 | break; 126 | case 'DefaultGraph': 127 | defaultGraphWriter.write(term, serialized, prefixes); 128 | gt += serialized.value; 129 | gte = true; 130 | defaultGraphWriter.write(term, serialized, prefixes); 131 | lt += serialized.value; 132 | lte = true; 133 | break; 134 | default: 135 | throw new Error(`Unsupported term type ${term.termType}`); 136 | } 137 | remaining -= 1; 138 | if (remaining > 0 && t < index.terms.length - 1) { 139 | gt += separator; 140 | lt += separator; 141 | } 142 | } 143 | if (lte) { 144 | if (didRange) { 145 | lt += boundary; 146 | } else { 147 | lt += separator + boundary; 148 | } 149 | } else { 150 | lt += separator; 151 | } 152 | if (gte) { 153 | if (!didRange && !didLiteral) { 154 | gt += separator; 155 | } 156 | } else { 157 | if (didRange || didLiteral) { 158 | gt += boundary; 159 | } else { 160 | gt += separator + boundary; 161 | } 162 | } 163 | return { gt, lt, gte, lte, order: index.terms.slice(didRange ? t - 1 : 1), index }; 164 | }; 165 | -------------------------------------------------------------------------------- /src/serialization/quads.ts: -------------------------------------------------------------------------------- 1 | 2 | import type {DataFactory, Quad_Graph, Quad_Object, Quad_Predicate, Quad_Subject, Term} from '@rdfjs/types'; 3 | import type {Prefixes, Quad, SerializedTerm, TermName} from '../types/index.js'; 4 | 5 | import { separator } from '../utils/constants.js'; 6 | import { termReader, termWriter } from './terms.js'; 7 | import { encodeQuadLength, decodeQuadLength, LENGTH_OF_ENCODED_QUAD_LENGTH } from './utils.js'; 8 | 9 | type TwoStepsQuadWriter = Record & { 10 | ingest(quad: Quad, prefixes: Prefixes): TwoStepsQuadWriter; 11 | write(prefix: string, termNames: TermName[]): string; 12 | }; 13 | 14 | export const twoStepsQuadWriter: TwoStepsQuadWriter = { 15 | subject: { type: '', value: '', lengths: '' }, 16 | predicate: { type: '', value: '', lengths: '' }, 17 | object: { type: '', value: '', lengths: '' }, 18 | graph: { type: '', value: '', lengths: '' }, 19 | ingest(quad: Quad, prefixes: Prefixes) { 20 | termWriter.write(quad.subject, this.subject, prefixes); 21 | termWriter.write(quad.predicate, this.predicate, prefixes); 22 | termWriter.write(quad.object, this.object, prefixes); 23 | termWriter.write(quad.graph, this.graph, prefixes); 24 | return this; 25 | }, 26 | write(prefix: string, termNames: TermName[]) { 27 | let key = prefix; 28 | let lengths = ''; 29 | for (let t = 0, term; t < termNames.length; t += 1) { 30 | term = this[termNames[t]]; 31 | key += term.value + separator; 32 | lengths += term.type + term.lengths; 33 | } 34 | return key + lengths + encodeQuadLength(lengths.length); 35 | }, 36 | }; 37 | 38 | type QuadReader = Record 39 | & { keyOffset: number; lengthsOffset: number; } 40 | & { read(key: string, keyOffset: number, termNames: TermName[], factory: DataFactory, prefixes: Prefixes): Quad; } 41 | ; 42 | 43 | export const quadReader: QuadReader = { 44 | subject: null, 45 | predicate: null, 46 | object: null, 47 | graph: null, 48 | keyOffset: 0, 49 | lengthsOffset: 0, 50 | read(key: string, keyOffset: number, termNames: TermName[], factory: DataFactory, prefixes: Prefixes): Quad { 51 | this.lengthsOffset = key.length - decodeQuadLength(key.slice(-LENGTH_OF_ENCODED_QUAD_LENGTH)) - LENGTH_OF_ENCODED_QUAD_LENGTH; 52 | this.keyOffset = keyOffset; 53 | for (let t = 0, termName; t < termNames.length; t += 1) { 54 | termName = termNames[t]; 55 | this[termName] = termReader.read(key, this, factory, prefixes); 56 | this.keyOffset += separator.length; 57 | } 58 | return factory.quad( 59 | this.subject! as Quad_Subject, 60 | this.predicate! as Quad_Predicate, 61 | this.object! as Quad_Object, 62 | this.graph! as Quad_Graph, 63 | ); 64 | }, 65 | }; 66 | -------------------------------------------------------------------------------- /src/serialization/terms.ts: -------------------------------------------------------------------------------- 1 | 2 | import type { Prefixes, ReadingState, SerializedTerm, TermReader, TermWriter } from '../types/index.js'; 3 | import type { BlankNode, DataFactory, DefaultGraph, Literal, NamedNode } from '@rdfjs/types'; 4 | 5 | import * as xsd from './xsd.js'; 6 | import { encodeTermLength, decodeTermLength, LENGTH_OF_ENCODED_TERM_LENGTH, sliceString } from './utils.js'; 7 | import {Term} from '@rdfjs/types'; 8 | import {separator} from '../utils/constants.js'; 9 | import {encode} from './fpstring.js'; 10 | 11 | export const namedNodeWriter: TermWriter = { 12 | write(node: NamedNode, serialized: SerializedTerm, prefixes: Prefixes) { 13 | const compactedIri = prefixes.compactIri(node.value); 14 | serialized.lengths = encodeTermLength(compactedIri.length); 15 | serialized.value = compactedIri; 16 | }, 17 | }; 18 | 19 | export const namedNodeReader: TermReader = { 20 | read(key: string, state: ReadingState, factory: DataFactory, prefixes: Prefixes): NamedNode { 21 | const { keyOffset, lengthsOffset } = state; 22 | const valueLen = decodeTermLength(sliceString(key, lengthsOffset, LENGTH_OF_ENCODED_TERM_LENGTH)); 23 | state.lengthsOffset += LENGTH_OF_ENCODED_TERM_LENGTH; 24 | state.keyOffset += valueLen; 25 | return factory.namedNode(prefixes.expandTerm(sliceString(key, keyOffset, valueLen))); 26 | }, 27 | }; 28 | 29 | export const blankNodeWriter: TermWriter = { 30 | write(node: BlankNode, serialized: SerializedTerm) { 31 | serialized.lengths = encodeTermLength(node.value.length); 32 | serialized.value = node.value; 33 | }, 34 | }; 35 | 36 | export const blankNodeReader: TermReader = { 37 | read(key: string, state: ReadingState, factory: DataFactory): BlankNode { 38 | const { keyOffset, lengthsOffset } = state; 39 | const valueLen = decodeTermLength(sliceString(key, lengthsOffset, LENGTH_OF_ENCODED_TERM_LENGTH)); 40 | state.lengthsOffset += LENGTH_OF_ENCODED_TERM_LENGTH; 41 | state.keyOffset += valueLen; 42 | return factory.blankNode(sliceString(key, keyOffset, valueLen)); 43 | }, 44 | }; 45 | 46 | export const genericLiteralWriter: TermWriter = { 47 | write(node: Literal, serialized: SerializedTerm) { 48 | serialized.lengths = encodeTermLength(node.value.length) + encodeTermLength(node.datatype.value.length); 49 | serialized.value = node.datatype.value + separator + node.value; 50 | }, 51 | }; 52 | 53 | export const genericLiteralReader: TermReader = { 54 | read(key: string, state: ReadingState, factory: DataFactory, prefixes: Prefixes): Literal { 55 | const { keyOffset, lengthsOffset } = state; 56 | const valueLen = decodeTermLength(sliceString(key, lengthsOffset, LENGTH_OF_ENCODED_TERM_LENGTH)); 57 | const datatypeValueLen = decodeTermLength(sliceString(key, lengthsOffset + LENGTH_OF_ENCODED_TERM_LENGTH, LENGTH_OF_ENCODED_TERM_LENGTH)); 58 | state.lengthsOffset += LENGTH_OF_ENCODED_TERM_LENGTH * 2; 59 | state.keyOffset += valueLen + datatypeValueLen + separator.length; 60 | return factory.literal( 61 | sliceString(key, keyOffset + datatypeValueLen + separator.length, valueLen), 62 | factory.namedNode(sliceString(key, keyOffset, datatypeValueLen)), 63 | ); 64 | }, 65 | } 66 | 67 | export const stringLiteralWriter: TermWriter = { 68 | write(node: Literal, serialized: SerializedTerm) { 69 | serialized.lengths = encodeTermLength(node.value.length); 70 | serialized.value = node.value; 71 | }, 72 | }; 73 | 74 | export const stringLiteralReader: TermReader = { 75 | read(key: string, state: ReadingState, factory: DataFactory): Literal { 76 | const { keyOffset, lengthsOffset } = state; 77 | const valueLen = decodeTermLength(sliceString(key, lengthsOffset, LENGTH_OF_ENCODED_TERM_LENGTH)); 78 | state.lengthsOffset += LENGTH_OF_ENCODED_TERM_LENGTH; 79 | state.keyOffset += valueLen; 80 | return factory.literal(sliceString(key, keyOffset, valueLen)); 81 | }, 82 | }; 83 | 84 | export const langStringLiteralWriter: TermWriter = { 85 | write(node: Literal, serialized: SerializedTerm) { 86 | serialized.lengths = encodeTermLength(node.value.length) + encodeTermLength(node.language.length); 87 | serialized.value = node.language + separator + node.value; 88 | }, 89 | }; 90 | 91 | export const langStringLiteralReader: TermReader = { 92 | read(key: string, state: ReadingState, factory: DataFactory, prefixes: Prefixes): Literal { 93 | const { keyOffset, lengthsOffset } = state; 94 | const valueLen = decodeTermLength(sliceString(key, lengthsOffset, LENGTH_OF_ENCODED_TERM_LENGTH)); 95 | const langCodeLen = decodeTermLength(sliceString(key, lengthsOffset + LENGTH_OF_ENCODED_TERM_LENGTH, LENGTH_OF_ENCODED_TERM_LENGTH)); 96 | state.lengthsOffset += LENGTH_OF_ENCODED_TERM_LENGTH * 2; 97 | state.keyOffset += valueLen + langCodeLen + separator.length; 98 | return factory.literal( 99 | sliceString(key, keyOffset + langCodeLen + separator.length, valueLen), 100 | sliceString(key, keyOffset, langCodeLen), 101 | ); 102 | }, 103 | } 104 | 105 | export const numericLiteralWriter: TermWriter = { 106 | write(node: Literal, serialized: SerializedTerm, prefixes: Prefixes, rangeMode: boolean, encodedValue: string) { 107 | serialized.lengths = encodeTermLength(node.value.length) + encodeTermLength(node.datatype.value.length) + encodeTermLength(encodedValue.length); 108 | if (!rangeMode) { 109 | serialized.value = encodedValue + separator + node.datatype.value + separator + node.value; 110 | } else { 111 | serialized.value = encodedValue; 112 | } 113 | }, 114 | }; 115 | 116 | export const numericLiteralReader: TermReader = { 117 | read(key: string, state: ReadingState, factory: DataFactory, prefixes: Prefixes): Literal { 118 | const { keyOffset, lengthsOffset } = state; 119 | const valueLen = decodeTermLength(sliceString(key, lengthsOffset, LENGTH_OF_ENCODED_TERM_LENGTH)); 120 | const datatypeValueLen = decodeTermLength(sliceString(key, lengthsOffset + LENGTH_OF_ENCODED_TERM_LENGTH, LENGTH_OF_ENCODED_TERM_LENGTH)); 121 | const numericValueLen = decodeTermLength(sliceString(key, lengthsOffset + (LENGTH_OF_ENCODED_TERM_LENGTH * 2), LENGTH_OF_ENCODED_TERM_LENGTH)); 122 | state.lengthsOffset += LENGTH_OF_ENCODED_TERM_LENGTH * 3; 123 | state.keyOffset += numericValueLen + datatypeValueLen + valueLen + (separator.length * 2); 124 | return factory.literal( 125 | sliceString(key, keyOffset + numericValueLen + separator.length + datatypeValueLen + separator.length, valueLen), 126 | factory.namedNode(sliceString(key, keyOffset + numericValueLen + separator.length, datatypeValueLen)), 127 | ); 128 | }, 129 | } 130 | 131 | export const defaultGraphWriter: TermWriter = { 132 | write(node: DefaultGraph, serialized: SerializedTerm) { 133 | serialized.value = 'dg'; 134 | serialized.lengths = '2'; 135 | }, 136 | }; 137 | 138 | export const defaultGraphReader: TermReader = { 139 | read(key: string, state: ReadingState, factory: DataFactory, prefixes: Prefixes): DefaultGraph { 140 | state.keyOffset += 2; 141 | state.lengthsOffset += 1; 142 | return factory.defaultGraph(); 143 | }, 144 | }; 145 | 146 | export const termWriter: TermWriter = { 147 | write(term: Term, serialized: SerializedTerm, prefixes: Prefixes) { 148 | switch (term.termType) { 149 | case 'NamedNode': 150 | serialized.type = '0'; 151 | namedNodeWriter.write(term, serialized, prefixes); 152 | break; 153 | case 'BlankNode': 154 | serialized.type = '1'; 155 | blankNodeWriter.write(term, serialized, prefixes); 156 | break; 157 | case 'DefaultGraph': 158 | serialized.type = '6'; 159 | defaultGraphWriter.write(term, serialized, prefixes); 160 | break; 161 | case 'Literal': 162 | if (term.language) { 163 | serialized.type = '4'; 164 | langStringLiteralWriter.write(term, serialized, prefixes); 165 | } else if (term.datatype) { 166 | switch (term.datatype.value) { 167 | case xsd.string: 168 | serialized.type = '3'; 169 | stringLiteralWriter.write(term, serialized, prefixes); 170 | break; 171 | case xsd.integer: 172 | case xsd.float: 173 | case xsd.double: 174 | case xsd.decimal: 175 | case xsd.nonPositiveInteger: 176 | case xsd.negativeInteger: 177 | case xsd.long: 178 | case xsd.int: 179 | case xsd.short: 180 | case xsd.byte: 181 | case xsd.nonNegativeInteger: 182 | case xsd.unsignedLong: 183 | case xsd.unsignedInt: 184 | case xsd.unsignedShort: 185 | case xsd.unsignedByte: 186 | case xsd.positiveInteger: 187 | serialized.type = '5'; 188 | numericLiteralWriter.write(term, serialized, prefixes, false, encode(term.value)); 189 | break; 190 | case xsd.dateTime: 191 | serialized.type = '7'; 192 | numericLiteralWriter.write(term, serialized, prefixes, false, encode(new Date(term.value).valueOf())); 193 | break; 194 | default: 195 | serialized.type = '2'; 196 | genericLiteralWriter.write(term, serialized, prefixes); 197 | } 198 | } else { 199 | serialized.type = '3'; 200 | stringLiteralWriter.write(term, serialized, prefixes); 201 | } 202 | } 203 | } 204 | }; 205 | 206 | export const termReader: TermReader = { 207 | read(key: string, state: ReadingState, factory: DataFactory, prefixes: Prefixes) { 208 | let termValue; 209 | const encodedTermType = key.charAt(state.lengthsOffset); 210 | state.lengthsOffset += 1; 211 | switch (encodedTermType) { 212 | case '0': 213 | termValue = namedNodeReader.read(key, state, factory, prefixes); 214 | break; 215 | case '1': 216 | termValue = blankNodeReader.read(key, state, factory, prefixes); 217 | break; 218 | case '2': 219 | termValue = genericLiteralReader.read(key, state, factory, prefixes); 220 | break; 221 | case '3': 222 | termValue = stringLiteralReader.read(key, state, factory, prefixes); 223 | break; 224 | case '4': 225 | termValue = langStringLiteralReader.read(key, state, factory, prefixes); 226 | break; 227 | case '5': 228 | termValue = numericLiteralReader.read(key, state, factory, prefixes); 229 | break; 230 | case '6': 231 | termValue = defaultGraphReader.read(key, state, factory, prefixes); 232 | break; 233 | case '7': 234 | termValue = numericLiteralReader.read(key, state, factory, prefixes); 235 | break; 236 | default: throw new Error(`Unexpected encoded term type "${encodedTermType}"`); 237 | } 238 | return termValue; 239 | } 240 | }; 241 | -------------------------------------------------------------------------------- /src/serialization/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | export const sliceString = (source: string, offset: number, length: number): string => { 3 | return source.slice(offset, offset + length); 4 | }; 5 | 6 | export const LENGTH_OF_ENCODED_TERM_LENGTH = 4; 7 | 8 | export const encodeTermLength = (val: number) => { 9 | if (val < 36) return `000${val.toString(36)}`; 10 | if (val < 1_296) return `00${val.toString(36)}`; 11 | if (val < 46_656) return `0${val.toString(36)}`; 12 | if (val < 1_679_616) return val.toString(36); 13 | throw new Error('term serialization exceeded maximum limit of 1_679_616 characters'); 14 | }; 15 | 16 | export const decodeTermLength = (str: string) => { 17 | return parseInt(str, 36); 18 | }; 19 | 20 | export const LENGTH_OF_ENCODED_QUAD_LENGTH = 5; 21 | 22 | export const encodeQuadLength = (val: number) => { 23 | if (val < 36) return `0000${val.toString(36)}`; 24 | if (val < 1_296) return `000${val.toString(36)}`; 25 | if (val < 46_656) return `00${val.toString(36)}`; 26 | if (val < 1_679_616) return `0${val.toString(36)}`; 27 | if (val < 60_466_176) return val.toString(36); 28 | throw new Error('quad serialization exceeded maximum limit of 60_466_176 characters'); 29 | }; 30 | 31 | export const decodeQuadLength = (str: string) => { 32 | return parseInt(str, 36); 33 | }; 34 | -------------------------------------------------------------------------------- /src/serialization/xsd.ts: -------------------------------------------------------------------------------- 1 | 2 | export const xsd = 'http://www.w3.org/2001/XMLSchema#'; 3 | export const rdf = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'; 4 | 5 | export const langString = `${rdf}langString`; 6 | 7 | export const string = `${xsd}string`; 8 | 9 | export const dateTime = `${xsd}dateTime`; 10 | 11 | export const boolean = `${xsd}boolean`; 12 | 13 | export const integer = `${xsd}integer`; 14 | export const decimal = `${xsd}decimal`; 15 | export const float = `${xsd}float`; 16 | export const double = `${xsd}double`; 17 | export const nonPositiveInteger = `${xsd}nonPositiveInteger`; 18 | export const negativeInteger = `${xsd}negativeInteger`; 19 | export const long = `${xsd}long`; 20 | export const int = `${xsd}int`; 21 | export const short = `${xsd}short`; 22 | export const byte = `${xsd}byte`; 23 | export const nonNegativeInteger = `${xsd}nonNegativeInteger`; 24 | export const unsignedLong = `${xsd}unsignedLong`; 25 | export const unsignedInt = `${xsd}unsignedInt`; 26 | export const unsignedShort = `${xsd}unsignedShort`; 27 | export const unsignedByte = `${xsd}unsignedByte`; 28 | export const positiveInteger = `${xsd}positiveInteger`; 29 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import type { AbstractChainedBatch, AbstractLevel } from 'abstract-level' 3 | import type { AsyncIterator } from 'asynciterator'; 4 | import type { Literal, DataFactory, Quad_Subject, Quad_Predicate, Quad_Object, Quad_Graph, Quad, Term } from '@rdfjs/types'; 5 | import type { Scope } from '../scope/index.js'; 6 | import type { AbstractIteratorOptions } from 'abstract-level'; 7 | import type { EventEmitter } from 'events'; 8 | 9 | export interface BatchOpts { 10 | /** 11 | * Factory for additional Key-Value Pair operations to be included atomically 12 | * in a batch. 13 | */ 14 | preWrite?: (batch: AbstractChainedBatch) => Promise | any; 15 | } 16 | 17 | export interface DelOpts extends BatchOpts { 18 | scope?: Scope, 19 | } 20 | 21 | export interface PutOpts extends BatchOpts { 22 | scope?: Scope, 23 | } 24 | 25 | export interface PatchOpts extends BatchOpts { 26 | } 27 | 28 | export type TermName = 'subject' | 'predicate' | 'object' | 'graph'; 29 | 30 | export enum ResultType { 31 | VOID = 'void', 32 | QUADS = 'quads', 33 | APPROXIMATE_SIZE = 'approximate_size', 34 | } 35 | 36 | export interface InternalIndex { 37 | terms: TermName[], 38 | prefix: string, 39 | } 40 | 41 | export interface ApproximateSizeResult { 42 | type: ResultType.APPROXIMATE_SIZE, 43 | approximateSize: number, 44 | } 45 | 46 | export interface GetOpts { 47 | limit?: number, 48 | order?: TermName[], 49 | reverse?: boolean, 50 | maxBufferSize?: number; 51 | } 52 | 53 | export interface PutStreamOpts { 54 | batchSize?: number, 55 | scope?: Scope, 56 | } 57 | 58 | export interface DelStreamOpts { 59 | batchSize?: number, 60 | } 61 | 62 | export { Quad }; 63 | 64 | export interface Range { 65 | termType: 'Range', 66 | lt?: Literal, 67 | lte?: Literal, 68 | gt?: Literal, 69 | gte?: Literal, 70 | } 71 | 72 | export interface Pattern { 73 | subject?: Quad_Subject, 74 | predicate?: Quad_Predicate, 75 | object?: Quad_Object|Range, 76 | graph?: Quad_Graph, 77 | } 78 | 79 | export interface QuadArrayResult { 80 | type: ResultType.QUADS, 81 | order: TermName[], 82 | items: Quad[], 83 | } 84 | 85 | export interface QuadArrayResultWithInternals extends QuadArrayResult { 86 | index: TermName[], 87 | resorted: boolean, 88 | } 89 | 90 | export interface QuadStreamResult { 91 | type: ResultType.QUADS, 92 | order: TermName[], 93 | iterator: AsyncIterator, 94 | } 95 | 96 | export interface QuadStreamResultWithInternals extends QuadStreamResult { 97 | index: TermName[], 98 | resorted: boolean, 99 | } 100 | 101 | 102 | export interface VoidResult { 103 | type: ResultType.VOID, 104 | } 105 | 106 | export interface Prefixes { 107 | expandTerm(term: string): string; 108 | compactIri(iri: string): string; 109 | } 110 | 111 | export interface StoreOpts { 112 | backend: AbstractLevel, 113 | prefixes?: Prefixes, 114 | indexes?: TermName[][], 115 | dataFactory: DataFactory, 116 | } 117 | 118 | export interface IndexQuery { 119 | gt: string; 120 | lt: string; 121 | gte: boolean; 122 | lte: boolean; 123 | order: TermName[]; 124 | index: InternalIndex; 125 | } 126 | 127 | export interface LevelQuery { 128 | level: AbstractIteratorOptions; 129 | order: TermName[]; 130 | index: InternalIndex; 131 | } 132 | 133 | export interface SerializedTerm { 134 | value: string; 135 | type: string; 136 | lengths: string; 137 | } 138 | 139 | export interface ReadingState { 140 | keyOffset: number; 141 | lengthsOffset: number; 142 | } 143 | 144 | export interface TermReader { 145 | read(key: string, state: ReadingState, factory: DataFactory, prefixes: Prefixes): T; 146 | } 147 | 148 | export type TermWriter = E extends 'T' 149 | ? { write(node: T, serialized: SerializedTerm, prefixes: Prefixes, rangeMode: boolean, encodedValue: string): void } 150 | : { write(node: T, serialized: SerializedTerm, prefixes: Prefixes): void } 151 | ; 152 | 153 | export interface StreamLike extends EventEmitter { 154 | read(): T | null; 155 | destroy?: () => void; 156 | readable?: boolean; 157 | on(event: 'readable', listener: () => void): this; 158 | on(event: 'end', listener: () => void): this; 159 | on(event: 'error', listener: (error: Error) => void): this; 160 | on(event: 'data', listener: (item: T) => void): this; 161 | } 162 | -------------------------------------------------------------------------------- /src/utils/comparators.ts: -------------------------------------------------------------------------------- 1 | 2 | import type { Quad, Term } from '@rdfjs/types'; 3 | import type { TermName } from '../types/index.js'; 4 | import { termNames } from './constants.js'; 5 | 6 | export const getTermComparator = (): (a: Term, b: Term) => (-1 | 0 | 1) => { 7 | return (a: Term, b: Term): -1|0|1 => { 8 | if (a.termType !== b.termType) { 9 | return a.termType < b.termType ? -1 : 1; 10 | } 11 | if (a.termType !== 'Literal' || b.termType !== 'Literal') { 12 | return a.value < b.value ? -1 : (a.value === b.value ? 0 : 1); 13 | } 14 | if (a.language || b.language) { 15 | if (!a.language) { 16 | return -1; 17 | } 18 | if (!b.language) { 19 | return 1; 20 | } 21 | return a.language < b.language ? -1 : (a.language === b.language ? 0 : 1); 22 | } 23 | if (a.datatype || b.datatype) { 24 | if (!a.datatype) { 25 | return -1; 26 | } 27 | if (!b.datatype) { 28 | return 1; 29 | } 30 | if (a.datatype.value !== b.datatype.value) { 31 | return a.datatype.value < b.datatype.value ? -1 : 1; 32 | } 33 | } 34 | return a.value < b.value ? -1 : (a.value === b.value ? 0 : 1); 35 | }; 36 | }; 37 | 38 | export const getQuadComparator = (_termNames: TermName[] = termNames): (a: Quad, b: Quad) => (-1 | 0 | 1) => { 39 | const termComparator = getTermComparator(); 40 | return (a: Quad, b: Quad) => { 41 | for (let i = 0, n = _termNames.length, r: -1|0|1; i < n; i += 1) { 42 | r = termComparator(a[_termNames[i]], b[_termNames[i]]); 43 | if (r !== 0) return r; 44 | } 45 | return 0; 46 | }; 47 | }; 48 | -------------------------------------------------------------------------------- /src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | 2 | import type { TermName } from '../types/index.js'; 3 | import type { AbstractChainedBatchPutOptions, AbstractChainedBatchDelOptions } from 'abstract-level'; 4 | 5 | export const emptyObject: { [key: string]: any } = {}; 6 | 7 | export const boundary = '\uDBFF\uDFFF'; 8 | export const separator = '\u0000\u0000'; 9 | 10 | export const termNames: TermName[] = [ 11 | 'subject', 12 | 'predicate', 13 | 'object', 14 | 'graph', 15 | ]; 16 | 17 | export const defaultIndexes: TermName[][] = [ 18 | ['subject', 'predicate', 'object', 'graph'], 19 | ['object', 'graph', 'subject', 'predicate'], 20 | ['graph', 'subject', 'predicate', 'object'], 21 | ['subject', 'object', 'predicate', 'graph'], 22 | ['predicate', 'object', 'graph', 'subject'], 23 | ['graph', 'predicate', 'object', 'subject'], 24 | ]; 25 | 26 | export const levelPutOpts: AbstractChainedBatchPutOptions = { 27 | keyEncoding: 'utf8', 28 | valueEncoding: 'view', 29 | }; 30 | 31 | export const levelDelOpts: AbstractChainedBatchDelOptions = { 32 | keyEncoding: 'utf8', 33 | }; 34 | 35 | export const emptyValue = new Uint8Array(0); 36 | -------------------------------------------------------------------------------- /src/utils/consumeinbatches.ts: -------------------------------------------------------------------------------- 1 | 2 | import { StreamLike } from '../types/index.js'; 3 | 4 | export const consumeInBatches = async (source: StreamLike, batchSize: number, onEachBatch: (items: T[]) => any): Promise => { 5 | return new Promise((resolve, reject) => { 6 | let bufpos = 0; 7 | let looping = false; 8 | let ended = false; 9 | let buffer = new Array(batchSize); 10 | const flushAndResolve = () => { 11 | cleanup(); 12 | if (bufpos > 0) { 13 | Promise.resolve(onEachBatch(buffer.slice(0, bufpos))) 14 | .then(resolve) 15 | .catch(onError); 16 | return; 17 | } 18 | resolve(); 19 | }; 20 | const onEnd = () => { 21 | ended = true; 22 | if (!looping) { 23 | flushAndResolve(); 24 | } 25 | }; 26 | const onError = (err: Error) => { 27 | cleanup(); 28 | reject(err); 29 | }; 30 | const onReadable = () => { 31 | if (!looping) { 32 | loop(); 33 | } 34 | }; 35 | let item: T | null = null; 36 | const loop = () => { 37 | looping = true; 38 | if (ended) { 39 | flushAndResolve(); 40 | return; 41 | } 42 | while (bufpos < batchSize && (item = source.read()) !== null) { 43 | buffer[bufpos++] = item; 44 | } 45 | if (item === null) { 46 | looping = false; 47 | return; 48 | } 49 | if (bufpos === batchSize) { 50 | Promise.resolve(onEachBatch(buffer.slice())) 51 | .then(loop) 52 | .catch(onError); 53 | bufpos = 0; 54 | } 55 | }; 56 | const cleanup = () => { 57 | source.removeListener('end', onEnd); 58 | source.removeListener('error', onError); 59 | source.removeListener('readable', onReadable); 60 | source.destroy?.(); 61 | }; 62 | source.on('end', onEnd); 63 | source.on('error', onError); 64 | source.on('readable', onReadable); 65 | if ('readable' in source && source.readable) { 66 | loop(); 67 | } 68 | }); 69 | }; 70 | -------------------------------------------------------------------------------- /src/utils/consumeonebyone.ts: -------------------------------------------------------------------------------- 1 | 2 | import { StreamLike } from '../types/index.js'; 3 | 4 | export const consumeOneByOne = async (source: StreamLike, onEachItem: (item: T) => any) => { 5 | return new Promise((resolve, reject) => { 6 | let item; 7 | let ended = false; 8 | let looping = false; 9 | const loop = () => { 10 | looping = true; 11 | if ((item = source.read()) !== null) { 12 | Promise.resolve(onEachItem(item)) 13 | .then(loop) 14 | .catch(onError); 15 | return; 16 | } 17 | looping = false; 18 | if (ended) { 19 | resolve(); 20 | } 21 | }; 22 | const onError = (err: Error) => { 23 | reject(err); 24 | cleanup(); 25 | }; 26 | const onEnd = () => { 27 | ended = true; 28 | if (!looping) { 29 | resolve(); 30 | } 31 | cleanup(); 32 | }; 33 | const onReadable = () => { 34 | if (!looping) { 35 | loop(); 36 | } 37 | }; 38 | const cleanup = () => { 39 | source.removeListener('end', onEnd); 40 | source.removeListener('error', onError); 41 | source.removeListener('readable', onReadable); 42 | source.destroy?.(); 43 | }; 44 | source.on('end', onEnd); 45 | source.on('error', onError); 46 | source.on('readable', onReadable); 47 | // readable might be undefined in older versions of userland readable-stream 48 | if ('readable' in source && source.readable) { 49 | loop(); 50 | } 51 | }); 52 | }; 53 | -------------------------------------------------------------------------------- /src/utils/stuff.ts: -------------------------------------------------------------------------------- 1 | 2 | import type { EventEmitter } from 'events'; 3 | import type { AbstractLevel } from 'abstract-level'; 4 | import type { TermName, StreamLike } from '../types/index.js'; 5 | 6 | export const isObject = (o: any): boolean => { 7 | return typeof(o) === 'object' && o !== null; 8 | }; 9 | 10 | export const isAbstractLevel = (o: any): o is AbstractLevel => { 11 | return isObject(o) 12 | && typeof(o.open) === 'function' 13 | && typeof(o.batch) === 'function' 14 | ; 15 | }; 16 | 17 | export const ensureAbstractLevel = (o: any, key: string) => { 18 | if (!isAbstractLevel(o)) { 19 | throw new Error(`${key} is not an AbstractLevel instance`); 20 | } 21 | }; 22 | 23 | export const streamToArray = (source: StreamLike): Promise => { 24 | return new Promise((resolve, reject) => { 25 | const chunks: T[] = []; 26 | const onData = (chunk: T) => { 27 | chunks.push(chunk); 28 | }; 29 | const cleanup = () => { 30 | source.removeListener('data', onData); 31 | source.removeListener('error', onError); 32 | source.destroy?.(); 33 | }; 34 | const onEnd = () => { 35 | cleanup(); 36 | resolve(chunks); 37 | }; 38 | const onError = (err: Error) => { 39 | cleanup(); 40 | reject(err); 41 | }; 42 | source.on('error', onError); 43 | source.on('end', onEnd); 44 | source.on('data', onData); 45 | }); 46 | } 47 | 48 | export const resolveOnEvent = (emitter: EventEmitter, event: string, rejectOnError?: boolean): Promise => { 49 | return new Promise((resolve, reject) => { 50 | const onceEvent = (arg: any) => { 51 | emitter.removeListener('error', onceError); 52 | resolve(arg); 53 | }; 54 | const onceError = (err: Error) => { 55 | emitter.removeListener(event, onceEvent); 56 | reject(err); 57 | }; 58 | emitter.once(event, onceEvent); 59 | if (rejectOnError) { 60 | emitter.once('error', onceError); 61 | } 62 | }); 63 | } 64 | 65 | export const waitForEvent = resolveOnEvent; 66 | 67 | export const arrStartsWith = (arr: TermName[], prefix: TermName[]): boolean => { 68 | for (let i = 0; i < prefix.length; i += 1) { 69 | if (prefix[i] !== arr[i]) { 70 | return false; 71 | } 72 | } 73 | return true; 74 | }; 75 | -------------------------------------------------------------------------------- /src/utils/uid.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Port of Hexoid 4 | * https://github.com/lukeed/hexoid/commit/029de61ca54d58c47d34a08f4e35a55e0f9c807f 5 | * Released under the MIT license by Luke Edwards (@lukeed) 6 | * https://lukeed.com/ 7 | */ 8 | 9 | let IDX: number = 256; 10 | const HEX: string[] = []; 11 | 12 | while (IDX--) { 13 | HEX[IDX] = (IDX + 256).toString(16).substring(1); 14 | } 15 | 16 | export const createUid = (len: number): () => string => { 17 | len = len || 16; 18 | let str: string = ''; 19 | let num: number = 0; 20 | return () => { 21 | if (!str || num === 256) { 22 | str = ''; 23 | num = (1 + len) / 2 | 0; 24 | while (num--) { 25 | str += HEX[256 * Math.random() | 0]; 26 | } 27 | str = str.substring(num = 0, len - 2); 28 | } 29 | return str + HEX[num++]; 30 | }; 31 | }; 32 | 33 | export const uid = createUid(11); 34 | -------------------------------------------------------------------------------- /test/backends/browserlevel.ts: -------------------------------------------------------------------------------- 1 | 2 | import { BrowserLevel } from 'browser-level'; 3 | import { DataFactory } from 'rdf-data-factory'; 4 | import { uid } from '../../dist/utils/uid.js'; 5 | import { runQuadstoreTests } from '../quadstore/quadstore.js'; 6 | 7 | export const runBrowserLevelTests = () => { 8 | 9 | describe('BrowserLevel backend', () => { 10 | 11 | beforeEach(async function () { 12 | this.db = new BrowserLevel(`quadstore-${uid()}`); 13 | this.indexes = null; 14 | this.dataFactory = new DataFactory(); 15 | this.prefixes = { 16 | expandTerm: (term: string) => term, 17 | compactIri: (iri: string) => iri, 18 | }; 19 | }); 20 | 21 | runQuadstoreTests(); 22 | 23 | }); 24 | 25 | }; 26 | -------------------------------------------------------------------------------- /test/backends/classiclevel.ts: -------------------------------------------------------------------------------- 1 | 2 | import os from 'os'; 3 | import fs from 'fs/promises'; 4 | import path from 'path'; 5 | import { ClassicLevel } from 'classic-level'; 6 | import { DataFactory } from 'rdf-data-factory'; 7 | import { uid } from '../../dist/utils/uid.js'; 8 | import { runQuadstoreTests } from '../quadstore/quadstore.js'; 9 | 10 | export const runClassicLevelTests = () => { 11 | 12 | describe('ClassicLevel backend', () => { 13 | 14 | beforeEach(async function () { 15 | this.location = path.join(os.tmpdir(), `quadstore-${uid()}`); 16 | this.db = new ClassicLevel(this.location); 17 | this.indexes = null; 18 | this.dataFactory = new DataFactory(); 19 | this.prefixes = { 20 | expandTerm: (term: string) => term, 21 | compactIri: (iri: string) => iri, 22 | }; 23 | }); 24 | 25 | afterEach(async function () { 26 | await fs.rm(this.location, { recursive: true, force: true, maxRetries: 3, retryDelay: 500 }); 27 | }); 28 | 29 | runQuadstoreTests(); 30 | 31 | }); 32 | 33 | }; 34 | -------------------------------------------------------------------------------- /test/backends/memorylevel.ts: -------------------------------------------------------------------------------- 1 | 2 | import { MemoryLevel } from 'memory-level'; 3 | import { DataFactory } from 'rdf-data-factory'; 4 | import { runQuadstoreTests } from '../quadstore/quadstore.js'; 5 | 6 | export const runMemoryLevelTests = () => { 7 | 8 | describe('MemoryLevel backend', () => { 9 | 10 | beforeEach(async function () { 11 | this.db = new MemoryLevel(); 12 | this.indexes = null; 13 | this.dataFactory = new DataFactory(); 14 | this.prefixes = { 15 | expandTerm: (term: string) => term, 16 | compactIri: (iri: string) => iri, 17 | }; 18 | }); 19 | 20 | runQuadstoreTests(); 21 | 22 | }); 23 | 24 | }; 25 | -------------------------------------------------------------------------------- /test/browser.ts: -------------------------------------------------------------------------------- 1 | 2 | import url from 'url' 3 | import path from 'path'; 4 | import http from 'http'; 5 | import puppeteer from 'puppeteer'; 6 | import nodeStatic from 'node-static'; 7 | 8 | const __filename = url.fileURLToPath(import.meta.url); 9 | const __dirname = path.dirname(__filename); 10 | 11 | const fileServer = new nodeStatic.Server(path.resolve(__dirname, 'browser')); 12 | const httpServer = http.createServer(); 13 | 14 | httpServer.on('request', (req, res) => { 15 | req.once('end', () => fileServer.serve(req, res)).resume(); 16 | }); 17 | 18 | let exitCode = 0; 19 | 20 | httpServer.once('listening', () => { 21 | (async () => { 22 | const browser = await puppeteer.launch({ 23 | headless: true, 24 | args: ['--no-sandbox', '--disable-setuid-sandbox'], 25 | }); 26 | const page = await browser.newPage(); 27 | page.on('console', async (evt) => { 28 | const [msg, ...args] = await Promise.all(evt.args().map(arg => arg.jsonValue())); 29 | console.log(msg, ...args); 30 | }); 31 | await page.goto('http://127.0.0.1:8080/index.html'); 32 | await page.waitForFunction('window.__TEST_RESULTS__'); 33 | const { failures, total }: { failures: number, total: number } 34 | = await page.evaluate('window.__TEST_RESULTS__') as any; 35 | if (failures > 0 || total === 0) { 36 | exitCode = 1; 37 | } 38 | await browser.close(); 39 | httpServer.close(() => { 40 | process.exit(exitCode); 41 | }); 42 | })().catch((err) => { 43 | console.error(err); 44 | process.exit(1); 45 | }); 46 | }); 47 | 48 | httpServer.listen(8080, '127.0.0.1'); 49 | -------------------------------------------------------------------------------- /test/browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mocha Tests 6 | 7 | 8 | 9 |
10 | 11 | 19 | 20 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /test/browser/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import { runMemoryLevelTests } from '../backends/memorylevel.js'; 3 | import { runBrowserLevelTests } from '../backends/browserlevel.js'; 4 | import { runOtherTests } from '../others/others.js'; 5 | import { runTypingsTests } from '../others/typings.js'; 6 | 7 | runOtherTests(); 8 | runTypingsTests(false); 9 | runMemoryLevelTests(); 10 | runBrowserLevelTests(); 11 | -------------------------------------------------------------------------------- /test/browser/webpack.config.cjs: -------------------------------------------------------------------------------- 1 | 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | mode: 'production', 6 | entry: path.resolve(__dirname, 'index.js'), 7 | output: { 8 | path: path.resolve(__dirname, '.'), 9 | filename: 'index.bundle.js', 10 | libraryTarget: 'module', 11 | chunkFormat: false, 12 | }, 13 | experiments: { 14 | outputModule: true, 15 | }, 16 | target: 'web', 17 | optimization: { 18 | minimize: false, 19 | moduleIds: 'named', 20 | usedExports: true, 21 | concatenateModules: false 22 | }, 23 | resolve: { 24 | fallback: { 25 | stream: false, 26 | }, 27 | }, 28 | plugins: [] 29 | }; 30 | -------------------------------------------------------------------------------- /test/node.ts: -------------------------------------------------------------------------------- 1 | 2 | import { runMemoryLevelTests } from './backends/memorylevel.js'; 3 | import { runClassicLevelTests } from './backends/classiclevel.js'; 4 | import { runOtherTests } from './others/others.js'; 5 | import { runTypingsTests } from './others/typings.js'; 6 | 7 | runOtherTests(); 8 | runTypingsTests(true); 9 | runMemoryLevelTests(); 10 | runClassicLevelTests(); 11 | -------------------------------------------------------------------------------- /test/others/consumeinbatches.ts: -------------------------------------------------------------------------------- 1 | 2 | import {AsyncIterator, IntegerIterator} from 'asynciterator'; 3 | import { delayIterator } from '../utils/stuff.js'; 4 | import { consumeInBatches } from '../../dist/utils/consumeinbatches.js'; 5 | import { toStrictlyEqual, toBeFalse, toBeLessThanOrEqualTo } from '../utils/expect.js'; 6 | 7 | const createSourceIterator = () => new IntegerIterator({ start: 0, step: 1, end: 99 }); 8 | 9 | export const runConsumeInBatchesTests = () => { 10 | 11 | describe('consumeInBatches()', () => { 12 | 13 | let source: AsyncIterator; 14 | let batchSize: number; 15 | 16 | const runTests = () => { 17 | 18 | it('should work with a batchSize of 1', () => { 19 | batchSize = 1; 20 | }); 21 | 22 | it('should work with a batchSize equal to the total number of items', () => { 23 | batchSize = 100; 24 | }); 25 | 26 | it('should work with a batchSize that is a perfect divisor of the number of items', () => { 27 | batchSize = 10; 28 | }); 29 | 30 | it('should work with a batchSize that is not a perfect divisor of the number of items (1)', () => { 31 | batchSize = 13; 32 | }); 33 | 34 | it('should work with a batchSize that is not a perfect divisor of the number of items (2)', () => { 35 | batchSize = 67; 36 | }); 37 | 38 | }; 39 | 40 | afterEach(async () => { 41 | let itemValue = 0; 42 | let itemCount = 0; 43 | let batchCount = 0; 44 | let last = false; 45 | await consumeInBatches(source, batchSize, async (batch) => { 46 | await new Promise((resolve) => setTimeout(resolve, 1)); 47 | toBeFalse(last); 48 | toBeLessThanOrEqualTo(batch.length, batchSize); 49 | last = batch.length < batchSize; 50 | itemCount += batch.length; 51 | batchCount += 1; 52 | for (let i = 0; i < batch.length; i += 1) { 53 | toStrictlyEqual(batch[i], itemValue++); 54 | } 55 | }); 56 | toStrictlyEqual(itemCount, 100); 57 | toStrictlyEqual(batchCount, Math.ceil(100 / batchSize)); 58 | }); 59 | 60 | describe('with an IntegerIterator as the source', () => { 61 | beforeEach(() => { 62 | source = createSourceIterator(); 63 | }); 64 | runTests(); 65 | }); 66 | 67 | describe('with an asynchronous IntegerIterator as the source', () => { 68 | beforeEach(() => { 69 | source = delayIterator(createSourceIterator(), 2); 70 | }); 71 | runTests(); 72 | }); 73 | 74 | }); 75 | 76 | }; 77 | -------------------------------------------------------------------------------- /test/others/consumeonebyone.ts: -------------------------------------------------------------------------------- 1 | 2 | import {AsyncIterator, IntegerIterator} from 'asynciterator'; 3 | import { delayIterator } from '../utils/stuff.js'; 4 | import { consumeOneByOne } from '../../dist/utils/consumeonebyone.js'; 5 | import { toStrictlyEqual } from '../utils/expect.js'; 6 | 7 | const createSourceIterator = () => new IntegerIterator({ start: 0, step: 1, end: 99 }); 8 | 9 | export const runConsumeOneByOneTests = () => { 10 | 11 | describe('consumeOneByOne()', () => { 12 | 13 | let source: AsyncIterator; 14 | 15 | it('should consume an IntegerIterator', () => { 16 | source = createSourceIterator(); 17 | }); 18 | 19 | it('should consume an asynchronous IntegerIterator', () => { 20 | source = delayIterator(createSourceIterator()); 21 | }); 22 | 23 | afterEach(async () => { 24 | let count = 0; 25 | await consumeOneByOne(source, async (item) => { 26 | await new Promise((resolve) => setTimeout(resolve, 1)); 27 | toStrictlyEqual(item, count++); 28 | }); 29 | toStrictlyEqual(count, 100); 30 | }); 31 | 32 | }); 33 | 34 | }; 35 | -------------------------------------------------------------------------------- /test/others/fpstring.ts: -------------------------------------------------------------------------------- 1 | 2 | import { toStrictlyEqual } from '../utils/expect.js'; 3 | import { encode } from '../../dist/serialization/fpstring.js'; 4 | 5 | export const runFpstringTests = () => { 6 | 7 | describe('Floating-point serialization', () => { 8 | 9 | it('should produce strings whose lexicographical sorting matches the natural sorting of the original values', async () => { 10 | 11 | const values = [ 12 | -123123, 13 | -123.123, 14 | -9.1, 15 | -9, 16 | -2.123, 17 | -1.23, 18 | -1, 19 | -0.2123 20 | -0.123, 21 | -0.1, 22 | 0, 23 | 0.1, 24 | 0.123, 25 | 0.2123, 26 | 1, 27 | 1.23, 28 | 2.123, 29 | 9, 30 | 9.1, 31 | 123.123, 32 | 123123, 33 | ]; 34 | 35 | const shuffled = [ 36 | 0, 37 | 123.123, 38 | -123.123, 39 | 0.2123, 40 | -9.1, 41 | -1, 42 | 0.123, 43 | 9, 44 | -0.3353, 45 | 123123, 46 | -123123, 47 | -1.23, 48 | -0.1, 49 | 2.123, 50 | -9, 51 | -2.123, 52 | 9.1, 53 | 0.1, 54 | 1, 55 | 1.23 56 | ]; 57 | 58 | const pairs: [number, string][] = shuffled.map(n => [n, encode(n)]); 59 | pairs.sort((p1, p2) => p1[1] < p2[1] ? -1 : 1); 60 | 61 | pairs.forEach((p, i) => { 62 | toStrictlyEqual(p[0], values[i]); 63 | }); 64 | 65 | }); 66 | 67 | }); 68 | 69 | }; 70 | -------------------------------------------------------------------------------- /test/others/others.ts: -------------------------------------------------------------------------------- 1 | 2 | import { runFpstringTests } from './fpstring.js'; 3 | import { runConsumeOneByOneTests } from './consumeonebyone.js'; 4 | import { runConsumeInBatchesTests } from './consumeinbatches.js'; 5 | 6 | export const runOtherTests = () => { 7 | runFpstringTests(); 8 | runConsumeOneByOneTests(); 9 | runConsumeInBatchesTests(); 10 | }; 11 | -------------------------------------------------------------------------------- /test/others/typings.ts: -------------------------------------------------------------------------------- 1 | 2 | import type { Stream, Quad } from '@rdfjs/types'; 3 | import type { StreamLike } from '../../dist/types/index.js'; 4 | import type { AsyncIterator } from 'asynciterator'; 5 | 6 | import { Quadstore } from '../../dist/quadstore.js'; 7 | import { MemoryLevel } from 'memory-level'; 8 | import { DataFactory } from 'rdf-data-factory'; 9 | 10 | export const runTypingsTests = (isNode: boolean) => { 11 | 12 | describe('Typings', () => { 13 | 14 | const store = new Quadstore({ 15 | backend: new MemoryLevel(), 16 | dataFactory: new DataFactory(), 17 | }); 18 | 19 | describe('StreamLike', () => { 20 | 21 | it('should extend the RDF/JS Stream interface when using the RDF/JS Quad interface as the type parameter', () => { 22 | const t: Stream = ({} as StreamLike); 23 | }); 24 | 25 | it('should not extend the RDF/JS Stream interface when using anything else but the RDF/JS Quad interface as the type parameter', () => { 26 | const t: StreamLike<'foo'> extends Stream ? true : false = false; 27 | }); 28 | 29 | it('should be extended by the AsyncIterator interface', () => { 30 | const t: StreamLike<'foo'> = ({} as AsyncIterator<'foo'>); 31 | }); 32 | 33 | isNode && it('should be extended by Node\'s native Readable interface', async () => { 34 | const { Readable } = await import('stream'); 35 | const ta: StreamLike = new Readable(); 36 | const tq: StreamLike = new Readable(); 37 | }); 38 | 39 | }); 40 | 41 | describe('AsyncIterator', () => { 42 | 43 | it('should extend the RDF/JS Stream interface when using the RDF/JS Quad interface as the type parameter', () => { 44 | const t: Stream = ({} as AsyncIterator); 45 | }); 46 | 47 | }); 48 | 49 | describe('Quadstore.prototype.match()', () => { 50 | 51 | it('should return an AsyncIterator instance', () => { 52 | const t: AsyncIterator = store.match(); 53 | }); 54 | 55 | it('should return a StreamLike object', () => { 56 | const t: StreamLike = store.match(); 57 | }); 58 | 59 | it('should return an iterable object', () => { 60 | const t: AsyncIterable = store.match(); 61 | }); 62 | 63 | }); 64 | 65 | describe('Quadstore.prototype.getStream()', () => { 66 | 67 | it('should return an AsyncIterator instance', async () => { 68 | const t: AsyncIterator = (await store.getStream({})).iterator; 69 | }); 70 | 71 | it('should return a StreamLike object', async () => { 72 | const t: StreamLike = (await store.getStream({})).iterator; 73 | }); 74 | 75 | it('should return an iterable object', async () => { 76 | const t: AsyncIterable = (await store.getStream({})).iterator; 77 | }); 78 | 79 | }); 80 | 81 | describe('Quadstore.prototype.putStream()', () => { 82 | 83 | isNode && it('should accept an instance of Node\'s native Readable class', async () => { 84 | const { Readable } = await import('stream'); 85 | await store.putStream(new Readable({ 86 | read() { 87 | this.push(null); 88 | } 89 | })); 90 | }); 91 | 92 | }); 93 | 94 | describe('Quadstore.prototype.delStream()', () => { 95 | 96 | isNode && it('should accept an instance of Node\'s native Readable class', async () => { 97 | const { Readable } = await import('stream'); 98 | await store.delStream(new Readable({ 99 | read() { 100 | this.push(null); 101 | } 102 | })); 103 | }); 104 | 105 | }); 106 | 107 | }); 108 | 109 | }; 110 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { "type": "module" } 2 | -------------------------------------------------------------------------------- /test/quadstore/del.ts: -------------------------------------------------------------------------------- 1 | 2 | import { equalsQuadArray } from '../utils/expect.js'; 3 | 4 | export const runDelTests = () => { 5 | 6 | describe('Quadstore.prototype.del()', () => { 7 | 8 | it('should delete a quad correctly', async function () { 9 | const { dataFactory, store } = this; 10 | const quads = [ 11 | dataFactory.quad( 12 | dataFactory.namedNode('ex://s'), 13 | dataFactory.namedNode('ex://p'), 14 | dataFactory.namedNode('ex://o'), 15 | dataFactory.namedNode('ex://g'), 16 | ), 17 | dataFactory.quad( 18 | dataFactory.namedNode('ex://s2'), 19 | dataFactory.namedNode('ex://p2'), 20 | dataFactory.namedNode('ex://o2'), 21 | dataFactory.namedNode('ex://g2'), 22 | ), 23 | ] 24 | await store.multiPut(quads); 25 | const { items: quadsBefore } = await store.get({}); 26 | equalsQuadArray(quadsBefore, quads); 27 | await store.del(quadsBefore[0]); 28 | const { items: quadsAfter } = await store.get({}); 29 | equalsQuadArray(quadsAfter, [quads[1]]); 30 | }); 31 | 32 | }); 33 | 34 | describe('Quadstore.prototype.multiDel()', () => { 35 | 36 | it('should delete a quad correctly', async function () { 37 | const { dataFactory, store } = this; 38 | const quads = [ 39 | dataFactory.quad( 40 | dataFactory.namedNode('ex://s'), 41 | dataFactory.namedNode('ex://p'), 42 | dataFactory.namedNode('ex://o'), 43 | dataFactory.namedNode('ex://g'), 44 | ), 45 | dataFactory.quad( 46 | dataFactory.namedNode('ex://s2'), 47 | dataFactory.namedNode('ex://p2'), 48 | dataFactory.namedNode('ex://o2'), 49 | dataFactory.namedNode('ex://g2'), 50 | ), 51 | ] 52 | await store.multiPut(quads); 53 | const { items: quadsBefore } = await store.get({}); 54 | equalsQuadArray(quadsBefore, quads); 55 | await store.multiDel([quadsBefore[0]]); 56 | const { items: quadsAfter } = await store.get({}); 57 | equalsQuadArray(quadsAfter, [quads[1]]); 58 | }); 59 | 60 | }); 61 | 62 | }; 63 | -------------------------------------------------------------------------------- /test/quadstore/get.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as xsd from '../../dist/serialization/xsd.js'; 3 | import { equalsQuadArray, toBeFalse, toStrictlyEqual, toBeTrue, arrayToStartWith, arrayToHaveLength } from '../utils/expect.js'; 4 | 5 | export const runGetTests = () => { 6 | 7 | describe('Quadstore.prototype.get()', () => { 8 | 9 | beforeEach(async function () { 10 | const { dataFactory } = this; 11 | this.quads = [ 12 | dataFactory.quad( 13 | dataFactory.namedNode('ex://s'), 14 | dataFactory.namedNode('ex://p'), 15 | dataFactory.namedNode('ex://o'), 16 | dataFactory.namedNode('ex://c'), 17 | ), 18 | dataFactory.quad( 19 | dataFactory.namedNode('ex://s'), 20 | dataFactory.namedNode('ex://p2'), 21 | dataFactory.namedNode('ex://o2'), 22 | dataFactory.namedNode('ex://c2'), 23 | ), 24 | dataFactory.quad( 25 | dataFactory.namedNode('ex://s2'), 26 | dataFactory.namedNode('ex://p'), 27 | dataFactory.namedNode('ex://o'), 28 | dataFactory.namedNode('ex://c'), 29 | ), 30 | dataFactory.quad( 31 | dataFactory.namedNode('ex://s2'), 32 | dataFactory.namedNode('ex://p'), 33 | dataFactory.namedNode('ex://o2'), 34 | dataFactory.namedNode('ex://c'), 35 | ), 36 | dataFactory.quad( 37 | dataFactory.namedNode('ex://s2'), 38 | dataFactory.namedNode('ex://p2'), 39 | dataFactory.namedNode('ex://o2'), 40 | dataFactory.namedNode('ex://c2'), 41 | ), 42 | dataFactory.quad( 43 | dataFactory.namedNode('ex://s3'), 44 | dataFactory.namedNode('ex://p3'), 45 | dataFactory.literal('44', dataFactory.namedNode(xsd.integer)), 46 | dataFactory.namedNode('ex://c3'), 47 | ), 48 | dataFactory.quad( 49 | dataFactory.namedNode('ex://s4'), 50 | dataFactory.namedNode('ex://p4'), 51 | dataFactory.literal('Hello, World', 'en-us'), 52 | dataFactory.namedNode('ex://c4'), 53 | ), 54 | ]; 55 | await this.store.multiPut(this.quads); 56 | }); 57 | 58 | it('should match quads by subject', async function () { 59 | const { dataFactory, store } = this; 60 | const { items: quads } = await store.get({ 61 | subject: dataFactory.namedNode('ex://s'), 62 | }); 63 | equalsQuadArray(quads, this.quads.slice(0, 2)); 64 | }); 65 | 66 | it('should match quads by predicate', async function () { 67 | const { dataFactory, store } = this; 68 | const { items: quads } = await store.get({ 69 | predicate: dataFactory.namedNode('ex://p'), 70 | }); 71 | equalsQuadArray(quads, [this.quads[0], ...this.quads.slice(2, 4)]); 72 | }); 73 | 74 | it('should match quads by object', async function () { 75 | const { dataFactory, store } = this; 76 | const { items: quads } = await store.get({ 77 | object: dataFactory.namedNode('ex://o'), 78 | }); 79 | equalsQuadArray(quads, [this.quads[0], this.quads[2]]); 80 | }); 81 | 82 | it('should match quads by object where object is a numeric literal', async function () { 83 | const { dataFactory, store } = this; 84 | const { items: quads } = await store.get({ 85 | object: dataFactory.literal('44', dataFactory.namedNode(xsd.integer)), 86 | }); 87 | equalsQuadArray(quads, [this.quads[5]]); 88 | }); 89 | 90 | it('should match quads by object where object is a language-tagged string', async function () { 91 | const { dataFactory, store } = this; 92 | const { items: quads } = await store.get({ 93 | object: dataFactory.literal('Hello, World', 'en-us'), 94 | }); 95 | equalsQuadArray(quads, [this.quads[6]]); 96 | }); 97 | 98 | it('should match quads by graph', async function () { 99 | const { dataFactory, store } = this; 100 | const { items: quads } = await store.get({ 101 | graph: dataFactory.namedNode('ex://c') }); 102 | equalsQuadArray(quads, [this.quads[0], ...this.quads.slice(2, 4)]); 103 | }); 104 | 105 | it('should match quads by subject and predicate', async function () { 106 | const { dataFactory, store } = this; 107 | const { items: quads } = await store.get({ 108 | subject: dataFactory.namedNode('ex://s2'), 109 | predicate: dataFactory.namedNode('ex://p'), 110 | }); 111 | equalsQuadArray(quads, [this.quads[2], this.quads[3]]); 112 | }); 113 | 114 | it('should match quads by subject and object', async function () { 115 | const { dataFactory, store } = this; 116 | const { items: quads } = await store.get({ 117 | subject: dataFactory.namedNode('ex://s2'), 118 | object: dataFactory.namedNode('ex://o'), 119 | }); 120 | equalsQuadArray(quads, [this.quads[2]]); 121 | }); 122 | 123 | it('should match quads by subject and object where object is a numeric literal', async function () { 124 | const { dataFactory, store } = this; 125 | const { items: quads } = await store.get({ 126 | subject: dataFactory.namedNode('ex://s3'), 127 | object: dataFactory.literal('44', dataFactory.namedNode(xsd.integer)), 128 | }); 129 | equalsQuadArray(quads, [this.quads[5]]); 130 | }); 131 | 132 | it('should match quads by subject and graph', async function () { 133 | const { dataFactory, store } = this; 134 | const { items: quads } = await store.get({ 135 | subject: dataFactory.namedNode('ex://s2'), 136 | graph: dataFactory.namedNode('ex://c'), 137 | }); 138 | equalsQuadArray(quads, [this.quads[2], this.quads[3]]); 139 | }); 140 | 141 | it('should match quads by predicate and object', async function () { 142 | const { dataFactory, store } = this; 143 | const { items: quads } = await store.get({ 144 | predicate: dataFactory.namedNode('ex://p'), 145 | object: dataFactory.namedNode('ex://o'), 146 | }); 147 | equalsQuadArray(quads, [this.quads[0], this.quads[2]]); 148 | }); 149 | 150 | it('should match quads by predicate and object where object is a numeric literal', async function () { 151 | const { dataFactory, store } = this; 152 | const { items: quads } = await store.get({ 153 | predicate: dataFactory.namedNode('ex://p3'), 154 | object: dataFactory.literal('44', dataFactory.namedNode(xsd.integer)), 155 | }); 156 | equalsQuadArray(quads, [this.quads[5]]); 157 | }); 158 | 159 | it('should match quads by predicate and graph', async function () { 160 | const { dataFactory, store } = this; 161 | const { items: quads } = await store.get({ 162 | predicate: dataFactory.namedNode('ex://p'), 163 | graph: dataFactory.namedNode('ex://c'), 164 | }); 165 | equalsQuadArray(quads, [this.quads[0], ...this.quads.slice(2, 4)]); 166 | }); 167 | 168 | it('should match quads by object and graph', async function () { 169 | const { dataFactory, store } = this; 170 | const { items: quads } = await store.get({ 171 | object: dataFactory.namedNode('ex://o2'), 172 | graph: dataFactory.namedNode('ex://c2'), 173 | }); 174 | equalsQuadArray(quads, [this.quads[1], this.quads[4]]); 175 | }); 176 | 177 | it('should match quads by subject, predicate and object', async function () { 178 | const { dataFactory, store } = this; 179 | const { items: quads } = await store.get({ 180 | subject: dataFactory.namedNode('ex://s'), 181 | predicate: dataFactory.namedNode('ex://p2'), 182 | object: dataFactory.namedNode('ex://o2'), 183 | }); 184 | equalsQuadArray(quads, [this.quads[1]]); 185 | }); 186 | 187 | it('should match quads by subject, predicate and object where object is a numeric literal', async function () { 188 | const { dataFactory, store } = this; 189 | const { items: quads } = await store.get({ 190 | subject: dataFactory.namedNode('ex://s3'), 191 | predicate: dataFactory.namedNode('ex://p3'), 192 | object: dataFactory.literal('44', dataFactory.namedNode(xsd.integer)), 193 | }); 194 | equalsQuadArray(quads, [this.quads[5]]); 195 | }); 196 | 197 | it('should match quads by subject, predicate and graph', async function () { 198 | const { dataFactory, store } = this; 199 | const { items: quads } = await store.get({ 200 | subject: dataFactory.namedNode('ex://s'), 201 | predicate: dataFactory.namedNode('ex://p2'), 202 | graph: dataFactory.namedNode('ex://c2'), 203 | }); 204 | equalsQuadArray(quads, [this.quads[1]]); 205 | }); 206 | 207 | it('should match quads by subject, object and graph', async function () { 208 | const { dataFactory, store } = this; 209 | const { items: quads } = await store.get({ 210 | subject: dataFactory.namedNode('ex://s'), 211 | object: dataFactory.namedNode('ex://o2'), 212 | graph: dataFactory.namedNode('ex://c2'), 213 | }); 214 | equalsQuadArray(quads, [this.quads[1]]); 215 | }); 216 | 217 | it('should match quads by predicate, object and graph', async function () { 218 | const { dataFactory, store } = this; 219 | const { items: quads } = await store.get({ 220 | predicate: dataFactory.namedNode('ex://p2'), 221 | object: dataFactory.namedNode('ex://o2'), 222 | graph: dataFactory.namedNode('ex://c2'), 223 | }); 224 | equalsQuadArray(quads, [this.quads[1], this.quads[4]]); 225 | }); 226 | 227 | it('should match quads by predicate, object and graph where object is a numeric literal', async function () { 228 | const { dataFactory, store } = this; 229 | const { items: quads } = await store.get({ 230 | predicate: dataFactory.namedNode('ex://p3'), 231 | object: dataFactory.literal('44', dataFactory.namedNode(xsd.integer)), 232 | graph: dataFactory.namedNode('ex://c3'), 233 | }); 234 | equalsQuadArray(quads, [this.quads[5]]); 235 | }); 236 | 237 | it('should match quads by subject, predicate, object and graph', async function () { 238 | const { dataFactory, store } = this; 239 | const { items: quads } = await store.get({ 240 | subject: dataFactory.namedNode('ex://s2'), 241 | predicate: dataFactory.namedNode('ex://p2'), 242 | object: dataFactory.namedNode('ex://o2'), 243 | graph: dataFactory.namedNode('ex://c2'), 244 | }); 245 | equalsQuadArray(quads, [this.quads[4]]); 246 | }); 247 | 248 | it('should match quads by subject, predicate, object and graph where object is a numeric literal', async function () { 249 | const { dataFactory, store } = this; 250 | const { items: quads } = await store.get({ 251 | subject: dataFactory.namedNode('ex://s3'), 252 | predicate: dataFactory.namedNode('ex://p3'), 253 | object: dataFactory.literal('44', dataFactory.namedNode(xsd.integer)), 254 | graph: dataFactory.namedNode('ex://c3'), 255 | }); 256 | equalsQuadArray(quads, [this.quads[5]]); 257 | }); 258 | 259 | it('should match quads by subject, predicate, object and graph where object is a numeric literal', async function () { 260 | const { dataFactory, store } = this; 261 | const { items: quads } = await store.get({ 262 | subject: dataFactory.namedNode('ex://s3'), 263 | predicate: dataFactory.namedNode('ex://p3'), 264 | object: dataFactory.literal('44', dataFactory.namedNode(xsd.integer)), 265 | graph: dataFactory.namedNode('ex://c3'), 266 | }); 267 | equalsQuadArray(quads, [this.quads[5]]); 268 | }); 269 | 270 | it('should match zero quads when provided with an unknown subject', async function () { 271 | const { dataFactory, store } = this; 272 | const { items: quads } = await store.get({ 273 | subject: dataFactory.namedNode('ex://unknown'), 274 | }); 275 | equalsQuadArray(quads, []); 276 | }); 277 | 278 | it('should match zero quads when provided with an unknown subject and a known predicate', async function () { 279 | const { dataFactory, store } = this; 280 | const { items: quads } = await store.get({ 281 | subject: dataFactory.namedNode('ex://unknown'), 282 | predicate: dataFactory.namedNode('ex://p3'), 283 | }); 284 | equalsQuadArray(quads, []); 285 | }); 286 | 287 | }); 288 | 289 | describe('Quadstore.prototype.get() w/ order', () => { 290 | 291 | beforeEach(async function () { 292 | const { dataFactory } = this; 293 | const subject = dataFactory.namedNode('ex://s'); 294 | const graph = dataFactory.namedNode('ex://g'); 295 | const decimal = dataFactory.namedNode(xsd.decimal); 296 | for (let i = 10; i < 100; i += 1) { 297 | await this.store.put(dataFactory.quad( 298 | subject, 299 | dataFactory.namedNode(`ex://p${99 - i}`), 300 | dataFactory.literal(`${i}`, decimal), 301 | graph, 302 | )); 303 | } 304 | }); 305 | 306 | it('should produce the same results whether sorting in-memory or not', async function () { 307 | const { dataFactory, store } = this; 308 | const memResults = await store.get( 309 | { graph: dataFactory.namedNode('ex://g') }, 310 | { order: ['object'] }, 311 | ); 312 | toBeTrue(memResults.resorted); 313 | const idxResults = await store.get( 314 | { subject: dataFactory.namedNode('ex://s') }, 315 | { order: ['object'] }, 316 | ); 317 | toBeFalse(idxResults.resorted); 318 | arrayToStartWith(idxResults.order, ['object']); 319 | arrayToStartWith(memResults.order, ['object']); 320 | arrayToHaveLength(idxResults.items, 90); 321 | equalsQuadArray(idxResults.items, memResults.items); 322 | toStrictlyEqual(idxResults.items[0].object.value, '10'); 323 | toStrictlyEqual(idxResults.items[89].object.value, '99'); 324 | }); 325 | 326 | it('should produce the same results whether sorting in-memory or not, in reverse', async function () { 327 | const { dataFactory, store } = this; 328 | const memResults = await store.get( 329 | { graph: dataFactory.namedNode('ex://g') }, 330 | { order: ['object'], reverse: true }, 331 | ); 332 | toBeTrue(memResults.resorted); 333 | const idxResults = await store.get( 334 | { subject: dataFactory.namedNode('ex://s') }, 335 | { order: ['object'], reverse: true }, 336 | ); 337 | toBeFalse(idxResults.resorted); 338 | arrayToStartWith(idxResults.order, ['object']); 339 | arrayToStartWith(memResults.order, ['object']); 340 | arrayToHaveLength(idxResults.items, 90); 341 | equalsQuadArray(idxResults.items, memResults.items); 342 | toStrictlyEqual(idxResults.items[0].object.value, '99'); 343 | toStrictlyEqual(idxResults.items[89].object.value, '10'); 344 | }); 345 | 346 | it('should order by predicate while querying for a range of object literals, sorting in memory', async function () { 347 | const { dataFactory, store } = this; 348 | const memResults = await store.get( 349 | { 350 | object: { 351 | termType: 'Range', 352 | lt: dataFactory.literal('20', dataFactory.namedNode(xsd.decimal)), 353 | }, 354 | }, 355 | { order: ['predicate'] }, 356 | ); 357 | toBeTrue(memResults.resorted); 358 | arrayToHaveLength(memResults.items, 10); 359 | toStrictlyEqual(memResults.items[0].predicate.value, `ex://p80`); 360 | toStrictlyEqual(memResults.items[9].predicate.value, `ex://p89`); 361 | }); 362 | 363 | it('should order by predicate while querying for a range of object literals, sorting in memory, limiting', async function () { 364 | const { dataFactory, store } = this; 365 | const memResults = await store.get( 366 | { 367 | object: { 368 | termType: 'Range', 369 | lt: dataFactory.literal('20', dataFactory.namedNode(xsd.decimal)), 370 | }, 371 | }, 372 | { order: ['predicate'], limit: 2 }, 373 | ); 374 | toBeTrue(memResults.resorted); 375 | arrayToHaveLength(memResults.items, 2); 376 | toStrictlyEqual(memResults.items[0].predicate.value, `ex://p80`); 377 | toStrictlyEqual(memResults.items[1].predicate.value, `ex://p81`); 378 | }); 379 | 380 | it('should order by predicate while querying for a range of object literals, sorting in memory, in reverse', async function () { 381 | const { dataFactory, store } = this; 382 | const memResults = await store.get( 383 | { 384 | object: { 385 | termType: 'Range', 386 | lt: dataFactory.literal('20', dataFactory.namedNode(xsd.decimal)), 387 | }, 388 | }, 389 | { order: ['predicate'], reverse: true }, 390 | ); 391 | toBeTrue(memResults.resorted); 392 | arrayToHaveLength(memResults.items, 10); 393 | toStrictlyEqual(memResults.items[0].predicate.value, `ex://p89`); 394 | toStrictlyEqual(memResults.items[9].predicate.value, `ex://p80`); 395 | }); 396 | 397 | it('should order by predicate while querying for a range of object literals, sorting in memory, in reverse, limiting', async function () { 398 | const { dataFactory, store } = this; 399 | const memResults = await store.get( 400 | { 401 | object: { 402 | termType: 'Range', 403 | lt: dataFactory.literal('20', dataFactory.namedNode(xsd.decimal)), 404 | }, 405 | }, 406 | { order: ['predicate'], reverse: true, limit: 2 }, 407 | ); 408 | toBeTrue(memResults.resorted); 409 | arrayToHaveLength(memResults.items, 2); 410 | toStrictlyEqual(memResults.items[0].predicate.value, `ex://p89`); 411 | toStrictlyEqual(memResults.items[1].predicate.value, `ex://p88`); 412 | }); 413 | 414 | it('should order by object while querying for a range of object literals without sorting in memory', async function () { 415 | const { dataFactory, store } = this; 416 | const memResults = await store.get( 417 | { 418 | object: { 419 | termType: 'Range', 420 | lt: dataFactory.literal('20', dataFactory.namedNode(xsd.decimal)), 421 | }, 422 | }, 423 | { order: ['object'] }, 424 | ); 425 | toBeFalse(memResults.resorted); 426 | arrayToHaveLength(memResults.items, 10); 427 | toStrictlyEqual(memResults.items[0].object.value, `10`); 428 | toStrictlyEqual(memResults.items[9].object.value, `19`); 429 | }); 430 | 431 | it('should order by object while querying for a range of object literals without sorting in memory, in reverse', async function () { 432 | const { dataFactory, store } = this; 433 | const memResults = await store.get( 434 | { 435 | object: { 436 | termType: 'Range', 437 | lt: dataFactory.literal('20', dataFactory.namedNode(xsd.decimal)), 438 | }, 439 | }, 440 | { order: ['object'], reverse: true }, 441 | ); 442 | toBeFalse(memResults.resorted); 443 | arrayToHaveLength(memResults.items, 10); 444 | toStrictlyEqual(memResults.items[0].object.value, `19`); 445 | toStrictlyEqual(memResults.items[9].object.value, `10`); 446 | }); 447 | 448 | }); 449 | 450 | }; 451 | -------------------------------------------------------------------------------- /test/quadstore/import.ts: -------------------------------------------------------------------------------- 1 | 2 | import { ArrayIterator } from 'asynciterator'; 3 | import { waitForEvent, streamToArray } from '../../dist/utils/stuff.js'; 4 | import { arrayToHaveLength } from '../utils/expect.js'; 5 | 6 | export const runImportTests = () => { 7 | 8 | describe('Quadstore.prototype.import()', () => { 9 | 10 | it('should import a single quad correctly', async function () { 11 | const { dataFactory, store } = this; 12 | const quads = [ 13 | dataFactory.quad( 14 | dataFactory.namedNode('http://ex.com/s'), 15 | dataFactory.namedNode('http://ex.com/p'), 16 | dataFactory.literal('o', 'en-gb'), 17 | dataFactory.namedNode('http://ex.com/g') 18 | ) 19 | ]; 20 | const source = new ArrayIterator(quads); 21 | await waitForEvent(store.import(source), 'end', true); 22 | const matchedQuads = await streamToArray(store.match()); 23 | arrayToHaveLength(matchedQuads, 1); 24 | }); 25 | 26 | it('should import multiple quads correctly', async function () { 27 | const { dataFactory, store } = this; 28 | const quads = [ 29 | dataFactory.quad( 30 | dataFactory.namedNode('http://ex.com/s0'), 31 | dataFactory.namedNode('http://ex.com/p0'), 32 | dataFactory.literal('o0', 'en-gb'), 33 | dataFactory.namedNode('http://ex.com/g0') 34 | ), 35 | dataFactory.quad( 36 | dataFactory.namedNode('http://ex.com/s1'), 37 | dataFactory.namedNode('http://ex.com/p1'), 38 | dataFactory.literal('o1', 'en-gb'), 39 | dataFactory.namedNode('http://ex.com/g1') 40 | ), 41 | dataFactory.quad( 42 | dataFactory.namedNode('http://ex.com/s2'), 43 | dataFactory.namedNode('http://ex.com/p2'), 44 | dataFactory.literal('o2', 'en-gb'), 45 | dataFactory.namedNode('http://ex.com/g3') 46 | ) 47 | ]; 48 | const source = new ArrayIterator(quads); 49 | await waitForEvent(store.import(source), 'end', true); 50 | const matchedQuads = await streamToArray(store.match()); 51 | arrayToHaveLength(matchedQuads, 3); 52 | }); 53 | 54 | }); 55 | }; 56 | -------------------------------------------------------------------------------- /test/quadstore/match.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | import { ArrayIterator } from 'asynciterator'; 4 | import { waitForEvent, streamToArray } from '../../dist/utils/stuff.js'; 5 | import { arrayToHaveLength, equalsQuadArray } from '../utils/expect.js'; 6 | 7 | export const runMatchTests = () => { 8 | 9 | describe('Quadstore.prototype.match()', () => { 10 | 11 | describe('Match by value', () => { 12 | 13 | it('should match quads by subject', async function () { 14 | const { dataFactory, store } = this; 15 | const quads = [ 16 | dataFactory.quad( 17 | dataFactory.namedNode('http://ex.com/s'), 18 | dataFactory.namedNode('http://ex.com/p'), 19 | dataFactory.literal('o', 'en-gb'), 20 | dataFactory.namedNode('http://ex.com/g') 21 | ), 22 | dataFactory.quad( 23 | dataFactory.namedNode('http://ex.com/s2'), 24 | dataFactory.namedNode('http://ex.com/p'), 25 | dataFactory.literal('o', 'en-gb'), 26 | dataFactory.namedNode('http://ex.com/g') 27 | ) 28 | ]; 29 | const source = new ArrayIterator(quads); 30 | await waitForEvent(store.import(source), 'end', true); 31 | const subject = dataFactory.namedNode('http://ex.com/s2'); 32 | const matchedQuads = await streamToArray(store.match(subject)); 33 | 34 | equalsQuadArray(matchedQuads, [quads[1]]); 35 | }); 36 | 37 | it('should match quads by predicate', async function () { 38 | const { dataFactory, store } = this; 39 | const quads = [ 40 | dataFactory.quad( 41 | dataFactory.namedNode('http://ex.com/s'), 42 | dataFactory.namedNode('http://ex.com/p'), 43 | dataFactory.literal('o', 'en-gb'), 44 | dataFactory.namedNode('http://ex.com/g') 45 | ), 46 | dataFactory.quad( 47 | dataFactory.namedNode('http://ex.com/s'), 48 | dataFactory.namedNode('http://ex.com/p2'), 49 | dataFactory.literal('o', 'en-gb'), 50 | dataFactory.namedNode('http://ex.com/g') 51 | ) 52 | ]; 53 | const source = new ArrayIterator(quads); 54 | await waitForEvent(store.import(source), 'end', true); 55 | const predicate = dataFactory.namedNode('http://ex.com/p2'); 56 | const matchedQuads = await streamToArray(store.match(null, predicate)); 57 | 58 | equalsQuadArray(matchedQuads, [quads[1]]); 59 | }); 60 | 61 | it('should match quads by object', async function () { 62 | const { dataFactory, store } = this; 63 | const quads = [ 64 | dataFactory.quad( 65 | dataFactory.namedNode('http://ex.com/s'), 66 | dataFactory.namedNode('http://ex.com/p'), 67 | dataFactory.literal('o', 'en-gb'), 68 | dataFactory.namedNode('http://ex.com/g2') 69 | ), 70 | dataFactory.quad( 71 | dataFactory.namedNode('http://ex.com/s'), 72 | dataFactory.namedNode('http://ex.com/p'), 73 | dataFactory.literal('o2', 'en-gb'), 74 | dataFactory.namedNode('http://ex.com/g2') 75 | ) 76 | ]; 77 | const source = new ArrayIterator(quads); 78 | await waitForEvent(store.import(source), 'end', true); 79 | const object = dataFactory.literal('o', 'en-gb'); 80 | const matchedQuads = await streamToArray(store.match(null, null, object)); 81 | 82 | equalsQuadArray(matchedQuads, [quads[0]]); 83 | }); 84 | 85 | it('should match quads by graph', async function () { 86 | const { dataFactory, store } = this; 87 | const quads = [ 88 | dataFactory.quad( 89 | dataFactory.namedNode('http://ex.com/s'), 90 | dataFactory.namedNode('http://ex.com/p'), 91 | dataFactory.literal('o', 'en-gb'), 92 | dataFactory.namedNode('http://ex.com/g') 93 | ), 94 | dataFactory.quad( 95 | dataFactory.namedNode('http://ex.com/s'), 96 | dataFactory.namedNode('http://ex.com/p'), 97 | dataFactory.literal('o', 'en-gb'), 98 | dataFactory.namedNode('http://ex.com/g2') 99 | ) 100 | ]; 101 | const source = new ArrayIterator(quads); 102 | await waitForEvent(store.import(source), 'end', true); 103 | const graph = dataFactory.namedNode('http://ex.com/g2'); 104 | const matchedQuads = await streamToArray(store.match(null, null, null, graph)); 105 | 106 | equalsQuadArray(matchedQuads, [quads[1]]); 107 | }); 108 | 109 | it('should match the default graph when explicitly passed', async function () { 110 | const { dataFactory, store } = this; 111 | const quads = [ 112 | dataFactory.quad( 113 | dataFactory.namedNode('http://ex.com/s0'), 114 | dataFactory.namedNode('http://ex.com/p0'), 115 | dataFactory.literal('o0', 'en-gb'), 116 | dataFactory.defaultGraph() 117 | ), 118 | dataFactory.quad( 119 | dataFactory.namedNode('http://ex.com/s1'), 120 | dataFactory.namedNode('http://ex.com/p1'), 121 | dataFactory.literal('o1', 'en-gb'), 122 | dataFactory.namedNode('http://ex.com/g1') 123 | ) 124 | ]; 125 | const source = new ArrayIterator(quads); 126 | await waitForEvent(store.import(source), 'end', true); 127 | const matchedQuads = await streamToArray(store.match(null, null, null, dataFactory.defaultGraph())); 128 | 129 | equalsQuadArray(matchedQuads, [quads[0]]); 130 | }); 131 | 132 | }); 133 | 134 | describe('Match by range', () => { 135 | 136 | it('should match quads by object (literal) [GT]', async function () { 137 | const { dataFactory, store } = this; 138 | const quads = [ 139 | dataFactory.quad( 140 | dataFactory.namedNode('http://ex.com/s'), 141 | dataFactory.namedNode('http://ex.com/p'), 142 | dataFactory.literal('5', dataFactory.namedNode('http://www.w3.org/2001/XMLSchema#integer')), 143 | dataFactory.namedNode('http://ex.com/g') 144 | ), 145 | dataFactory.quad( 146 | dataFactory.namedNode('http://ex.com/s2'), 147 | dataFactory.namedNode('http://ex.com/p'), 148 | dataFactory.literal('7', dataFactory.namedNode('http://www.w3.org/2001/XMLSchema#integer')), 149 | dataFactory.namedNode('http://ex.com/g') 150 | ) 151 | ]; 152 | const source = new ArrayIterator(quads); 153 | await waitForEvent(store.import(source), 'end', true); 154 | const match = { termType: 'Range', 155 | gt: dataFactory.literal('6', dataFactory.namedNode('http://www.w3.org/2001/XMLSchema#integer')) }; 156 | const matchedQuads = await streamToArray(store.match(null, null, match, null)); 157 | 158 | equalsQuadArray(matchedQuads, [quads[1]]); 159 | }); 160 | 161 | it('should match quads by object (literal) [GTE]', async function () { 162 | const { dataFactory, store } = this; 163 | const quads = [ 164 | dataFactory.quad( 165 | dataFactory.namedNode('http://ex.com/s'), 166 | dataFactory.namedNode('http://ex.com/p'), 167 | dataFactory.literal('5', dataFactory.namedNode('http://www.w3.org/2001/XMLSchema#integer')), 168 | dataFactory.namedNode('http://ex.com/g') 169 | ), 170 | dataFactory.quad( 171 | dataFactory.namedNode('http://ex.com/s2'), 172 | dataFactory.namedNode('http://ex.com/p'), 173 | dataFactory.literal('7', dataFactory.namedNode('http://www.w3.org/2001/XMLSchema#integer')), 174 | dataFactory.namedNode('http://ex.com/g') 175 | ) 176 | ]; 177 | const source = new ArrayIterator(quads); 178 | await waitForEvent(store.import(source), 'end', true); 179 | const match = { termType: 'Range', 180 | gte: dataFactory.literal('7.0', dataFactory.namedNode('http://www.w3.org/2001/XMLSchema#double')) }; 181 | const matchedQuads = await streamToArray(store.match(null, null, match, null)); 182 | 183 | equalsQuadArray(matchedQuads, [quads[1]]); 184 | }); 185 | 186 | it('should not match quads by object (literal) if out of range [GT]', async function () { 187 | const { dataFactory, store } = this; 188 | const quads = [ 189 | dataFactory.quad( 190 | dataFactory.namedNode('http://ex.com/s'), 191 | dataFactory.namedNode('http://ex.com/p'), 192 | dataFactory.literal('5', dataFactory.namedNode('http://www.w3.org/2001/XMLSchema#integer')), 193 | dataFactory.namedNode('http://ex.com/g') 194 | ), 195 | dataFactory.quad( 196 | dataFactory.namedNode('http://ex.com/s2'), 197 | dataFactory.namedNode('http://ex.com/p'), 198 | dataFactory.literal('7', dataFactory.namedNode('http://www.w3.org/2001/XMLSchema#integer')), 199 | dataFactory.namedNode('http://ex.com/g') 200 | ) 201 | ]; 202 | const source = new ArrayIterator(quads); 203 | await waitForEvent(store.import(source), 'end', true); 204 | const match = { 205 | termType: 'Range', 206 | gt: dataFactory.literal('7.0', dataFactory.namedNode('http://www.w3.org/2001/XMLSchema#double')), 207 | }; 208 | const matchedQuads = await streamToArray(store.match(null, null, match, null)); 209 | arrayToHaveLength(matchedQuads, 0); 210 | }); 211 | }); 212 | 213 | }); 214 | 215 | }; 216 | -------------------------------------------------------------------------------- /test/quadstore/patch.ts: -------------------------------------------------------------------------------- 1 | 2 | import { equalsQuadArray } from '../utils/expect.js'; 3 | 4 | export const runPatchTests = () => { 5 | 6 | describe('Quadstore.prototype.patch()', async function () { 7 | 8 | it('should delete old quads and add new ones', async function () { 9 | const { dataFactory, store } = this; 10 | const quadsArray = [ 11 | dataFactory.quad( 12 | dataFactory.namedNode('ex://s'), 13 | dataFactory.namedNode('ex://p'), 14 | dataFactory.namedNode('ex://o'), 15 | dataFactory.namedNode('ex://g'), 16 | ), 17 | dataFactory.quad( 18 | dataFactory.namedNode('ex://s'), 19 | dataFactory.namedNode('ex://p2'), 20 | dataFactory.namedNode('ex://o2'), 21 | dataFactory.namedNode('ex://g2'), 22 | ), 23 | dataFactory.quad( 24 | dataFactory.namedNode('ex://s2'), 25 | dataFactory.namedNode('ex://p'), 26 | dataFactory.namedNode('ex://o'), 27 | dataFactory.namedNode('ex://g'), 28 | ), 29 | dataFactory.quad( 30 | dataFactory.namedNode('ex://s2'), 31 | dataFactory.namedNode('ex://p'), 32 | dataFactory.namedNode('ex://o2'), 33 | dataFactory.namedNode('ex://g'), 34 | ), 35 | dataFactory.quad( 36 | dataFactory.namedNode('ex://s2'), 37 | dataFactory.namedNode('ex://p2'), 38 | dataFactory.namedNode('ex://o2'), 39 | dataFactory.namedNode('ex://g2'), 40 | ), 41 | ]; 42 | const oldQuads = [ 43 | dataFactory.quad( 44 | dataFactory.namedNode('ex://s'), 45 | dataFactory.namedNode('ex://p'), 46 | dataFactory.namedNode('ex://o'), 47 | dataFactory.namedNode('ex://g'), 48 | ), 49 | dataFactory.quad( 50 | dataFactory.namedNode('ex://s'), 51 | dataFactory.namedNode('ex://p2'), 52 | dataFactory.namedNode('ex://o2'), 53 | dataFactory.namedNode('ex://g2'), 54 | ), 55 | ]; 56 | const newQuads = [ 57 | dataFactory.quad( 58 | dataFactory.namedNode('ex://s3'), 59 | dataFactory.namedNode('ex://p3'), 60 | dataFactory.namedNode('ex://o2'), 61 | dataFactory.namedNode('ex://g'), 62 | ), 63 | dataFactory.quad( 64 | dataFactory.namedNode('ex://s4'), 65 | dataFactory.namedNode('ex://p3'), 66 | dataFactory.namedNode('ex://o2'), 67 | dataFactory.namedNode('ex://g'), 68 | ), 69 | ]; 70 | const expected = quadsArray.slice(2).concat(newQuads); 71 | await store.multiPut(quadsArray); 72 | await store.multiPatch(oldQuads, newQuads); 73 | const { items: quads } = await store.get({}); 74 | equalsQuadArray(quads, expected); 75 | }); 76 | 77 | }); 78 | 79 | }; 80 | -------------------------------------------------------------------------------- /test/quadstore/prewrite.ts: -------------------------------------------------------------------------------- 1 | 2 | import type { Quad } from '@rdfjs/types'; 3 | import type { AbstractChainedBatch } from 'abstract-level'; 4 | 5 | import { toEqualUint8Array } from '../utils/expect.js'; 6 | 7 | const encoder = new TextEncoder(); 8 | 9 | export const runPrewriteTests = () => { 10 | 11 | describe('Quadstore preWrite option', () => { 12 | 13 | let quads: Quad[]; 14 | const prewriteValue = encoder.encode('value1'); 15 | 16 | beforeEach(function () { 17 | const { dataFactory } = this; 18 | quads = [ 19 | dataFactory.quad( 20 | dataFactory.namedNode('ex://s'), 21 | dataFactory.namedNode('ex://p'), 22 | dataFactory.namedNode('ex://o'), 23 | dataFactory.namedNode('ex://g'), 24 | ), 25 | dataFactory.quad( 26 | dataFactory.namedNode('ex://s2'), 27 | dataFactory.namedNode('ex://p2'), 28 | dataFactory.namedNode('ex://o2'), 29 | dataFactory.namedNode('ex://g2'), 30 | ), 31 | ]; 32 | }); 33 | 34 | it('should pre-write kvps when putting a quad', async function () { 35 | const { store } = this; 36 | await store.put(quads[0], { 37 | preWrite: (batch: AbstractChainedBatch) => batch.put('key1', prewriteValue) 38 | }); 39 | const value = await store.db.get('key1', { valueEncoding: 'view' }); 40 | toEqualUint8Array(prewriteValue, value); 41 | }); 42 | 43 | it('should pre-write kvps when putting quads', async function () { 44 | const { store } = this; 45 | await store.multiPut(quads, { 46 | preWrite: (batch: AbstractChainedBatch) => batch.put('key1', prewriteValue) 47 | }); 48 | const value = await store.db.get('key1', { valueEncoding: 'view' }); 49 | toEqualUint8Array(prewriteValue, value); 50 | }); 51 | 52 | it('should pre-write kvps when deleting a quad', async function () { 53 | const { store } = this; 54 | await store.put(quads[0]); 55 | await store.del(quads[0], { 56 | preWrite: (batch: AbstractChainedBatch) => batch.put('key1', prewriteValue) 57 | }); 58 | const value = await store.db.get('key1', { valueEncoding: 'view' }); 59 | toEqualUint8Array(prewriteValue, value); 60 | }); 61 | 62 | it('should pre-write kvps when deleting quads', async function () { 63 | const { store } = this; 64 | await store.multiPut(quads); 65 | await store.multiDel(quads, { 66 | preWrite: (batch: AbstractChainedBatch) => batch.put('key1', prewriteValue) 67 | }); 68 | const value = await store.db.get('key1', { valueEncoding: 'view' }); 69 | toEqualUint8Array(prewriteValue, value); 70 | }); 71 | 72 | it('should pre-write kvps when patching a quad', async function () { 73 | const { store } = this; 74 | await store.put(quads[0]); 75 | await store.patch(quads[0], quads[1], { 76 | preWrite: (batch: AbstractChainedBatch) => batch.put('key1', prewriteValue) 77 | }); 78 | const value = await store.db.get('key1', { valueEncoding: 'view' }); 79 | toEqualUint8Array(prewriteValue, value); 80 | }); 81 | 82 | it('should pre-write kvps when patching quads', async function () { 83 | const { store } = this; 84 | await store.multiPut([quads[0]]); 85 | await store.multiPatch([quads[0]], [quads[1]], { 86 | preWrite: (batch: AbstractChainedBatch) => batch.put('key1', prewriteValue) 87 | }); 88 | const value = await store.db.get('key1', { valueEncoding: 'view' }); 89 | toEqualUint8Array(prewriteValue, value); 90 | }); 91 | 92 | }); 93 | 94 | }; 95 | -------------------------------------------------------------------------------- /test/quadstore/put.ts: -------------------------------------------------------------------------------- 1 | 2 | import type {Quad, Term} from '@rdfjs/types'; 3 | import { ArrayIterator } from 'asynciterator'; 4 | import { streamToArray } from '../../dist/utils/stuff.js'; 5 | import { Scope, ScopeLabelMapping } from '../../dist/scope/index.js'; 6 | import { arrayToHaveLength, equalsQuadArray, toBeAnArray, toBeFalse, toStrictlyEqual, toBeTrue } from '../utils/expect.js'; 7 | import { LevelIterator } from '../../dist/get/leveliterator.js'; 8 | 9 | export const runPutTests = () => { 10 | 11 | describe('Quadstore.prototype.put()', () => { 12 | 13 | it('should store a single quad', async function () { 14 | const { dataFactory, store } = this; 15 | const newQuad = dataFactory.quad( 16 | dataFactory.namedNode('ex://s'), 17 | dataFactory.namedNode('ex://p'), 18 | dataFactory.namedNode('ex://o'), 19 | dataFactory.namedNode('ex://g'), 20 | ); 21 | await store.put(newQuad); 22 | const {items: foundQuads} = await store.get({}); 23 | equalsQuadArray(foundQuads, [newQuad]); 24 | }); 25 | 26 | it('should store a single quad with a term that serializes to a string longer than 127 chars', async function () { 27 | const { dataFactory, store } = this; 28 | const newQuad = dataFactory.quad( 29 | dataFactory.namedNode('ex://s'), 30 | dataFactory.namedNode('ex://p'), 31 | dataFactory.literal(''.padStart(129, 'aaabbb')), 32 | dataFactory.namedNode('ex://g'), 33 | ); 34 | await store.put(newQuad); 35 | const {items: foundQuads} = await store.get({}); 36 | equalsQuadArray(foundQuads, [newQuad]); 37 | }); 38 | 39 | it('should store multiple quads', async function () { 40 | const { dataFactory, store } = this; 41 | const newQuads = [ 42 | dataFactory.quad( 43 | dataFactory.namedNode('ex://s'), 44 | dataFactory.namedNode('ex://p'), 45 | dataFactory.namedNode('ex://o'), 46 | dataFactory.namedNode('ex://g'), 47 | ), 48 | dataFactory.quad( 49 | dataFactory.namedNode('ex://s2'), 50 | dataFactory.namedNode('ex://p2'), 51 | dataFactory.namedNode('ex://o2'), 52 | dataFactory.namedNode('ex://g2'), 53 | ), 54 | ]; 55 | await store.put(newQuads[0]); 56 | await store.put(newQuads[1]); 57 | const {items: foundQuads} = await store.get({}); 58 | equalsQuadArray(foundQuads, newQuads); 59 | }); 60 | 61 | it('should not duplicate quads', async function () { 62 | const { dataFactory, store } = this; 63 | const newQuads = [ 64 | dataFactory.quad( 65 | dataFactory.namedNode('ex://s'), 66 | dataFactory.namedNode('ex://p'), 67 | dataFactory.namedNode('ex://o'), 68 | dataFactory.namedNode('ex://g'), 69 | ), 70 | dataFactory.quad( 71 | dataFactory.namedNode('ex://s2'), 72 | dataFactory.namedNode('ex://p2'), 73 | dataFactory.namedNode('ex://o2'), 74 | dataFactory.namedNode('ex://g2'), 75 | ), 76 | ]; 77 | await store.put(newQuads[0]); 78 | await store.put(newQuads[1]); 79 | await store.put(newQuads[1]); 80 | const {items: foundQuads} = await store.get({}); 81 | equalsQuadArray(foundQuads, newQuads); 82 | }); 83 | 84 | }); 85 | 86 | describe('Quadstore.prototype.put() with scope', () => { 87 | 88 | it('Should transform blank node labels', async function () { 89 | const {dataFactory, store} = this; 90 | const scope = await store.initScope(); 91 | const quadA = dataFactory.quad( 92 | dataFactory.namedNode('ex://s'), 93 | dataFactory.namedNode('ex://p'), 94 | dataFactory.blankNode('bo'), 95 | dataFactory.namedNode('ex://g'), 96 | ); 97 | await store.put(quadA, { scope }); 98 | const { items: [quadB] } = await store.get({}); 99 | toBeTrue(quadB.subject.equals(quadA.subject)); 100 | toBeTrue(quadB.predicate.equals(quadA.predicate)); 101 | toBeFalse(quadB.object.equals(quadA.object)); 102 | toBeTrue(quadB.graph.equals(quadA.graph)); 103 | }); 104 | 105 | it('Should maintain mappings across different invocations', async function () { 106 | const {dataFactory, store} = this; 107 | const scope = await store.initScope(); 108 | const quadA = dataFactory.quad( 109 | dataFactory.namedNode('ex://s1'), 110 | dataFactory.namedNode('ex://p1'), 111 | dataFactory.blankNode('bo'), 112 | dataFactory.namedNode('ex://g1'), 113 | ); 114 | const quadB = dataFactory.quad( 115 | dataFactory.namedNode('ex://s2'), 116 | dataFactory.namedNode('ex://p2'), 117 | dataFactory.blankNode('bo'), 118 | dataFactory.namedNode('ex://g2'), 119 | ); 120 | await store.put(quadA, { scope }); 121 | await store.put(quadB, { scope }); 122 | const { items } = await store.get({}); 123 | arrayToHaveLength(items, 2); 124 | toBeTrue(items[1].object.equals(items[0].object)); 125 | toBeFalse(items[1].object.equals(quadA.object)); 126 | }); 127 | 128 | it('Should persist scope mappings', async function () { 129 | const {dataFactory, store} = this; 130 | const scope = await store.initScope(); 131 | const quad = dataFactory.quad( 132 | dataFactory.namedNode('ex://s'), 133 | dataFactory.namedNode('ex://p'), 134 | dataFactory.blankNode('bo'), 135 | dataFactory.namedNode('ex://g'), 136 | ); 137 | await store.put(quad, { scope }); 138 | const levelOpts = Scope.getLevelIteratorOpts(true, true, scope.id); 139 | const entries = await streamToArray(new LevelIterator( 140 | store.db.iterator(levelOpts), 141 | ([key, value]) => JSON.parse(value as string) as ScopeLabelMapping, 142 | )); 143 | toBeAnArray(entries); 144 | arrayToHaveLength(entries, 1); 145 | const [originalLabel, randomLabel] = entries[0]; 146 | toStrictlyEqual(originalLabel, 'bo'); 147 | }); 148 | 149 | }); 150 | 151 | describe('Quadstore.prototype.multiPut() with scope', () => { 152 | it('Should transform blank node labels', async function () { 153 | const {dataFactory, store} = this; 154 | const scope = await store.initScope(); 155 | const quadsA = [ 156 | dataFactory.quad( 157 | dataFactory.namedNode('ex://s'), 158 | dataFactory.namedNode('ex://p'), 159 | dataFactory.blankNode('bo'), 160 | dataFactory.namedNode('ex://g'), 161 | ), 162 | ]; 163 | await store.multiPut(quadsA, { scope }); 164 | const { items: quadsB } = await store.get({}); 165 | arrayToHaveLength(quadsB, 1); 166 | toBeTrue(quadsB[0].subject.equals(quadsA[0].subject)); 167 | toBeTrue(quadsB[0].predicate.equals(quadsA[0].predicate)); 168 | toBeFalse(quadsB[0].object.equals(quadsA[0].object)); 169 | toBeTrue(quadsB[0].graph.equals(quadsA[0].graph)); 170 | }); 171 | }); 172 | 173 | describe('Quadstore.prototype.putStream() with scope', () => { 174 | it('Should transform blank node labels', async function () { 175 | const {dataFactory, store} = this; 176 | const scope = await store.initScope(); 177 | const quadsA = [ 178 | dataFactory.quad( 179 | dataFactory.namedNode('ex://s'), 180 | dataFactory.namedNode('ex://p'), 181 | dataFactory.blankNode('bo'), 182 | dataFactory.namedNode('ex://g'), 183 | ), 184 | ]; 185 | await store.putStream(new ArrayIterator(quadsA), { scope }); 186 | const { items: quadsB } = await store.get({}); 187 | arrayToHaveLength(quadsB, 1); 188 | toBeTrue(quadsB[0].subject.equals(quadsA[0].subject)); 189 | toBeTrue(quadsB[0].predicate.equals(quadsA[0].predicate)); 190 | toBeFalse(quadsB[0].object.equals(quadsA[0].object)); 191 | toBeTrue(quadsB[0].graph.equals(quadsA[0].graph)); 192 | }); 193 | }); 194 | 195 | describe('Quadstore.prototype.putStream() with batchSize', () => { 196 | 197 | beforeEach(async function () { 198 | const { dataFactory } = this; 199 | const quads = []; 200 | for (let i = 0; i < 10; i += 1) { 201 | quads.push(dataFactory.quad( 202 | dataFactory.namedNode('ex://s'), 203 | dataFactory.namedNode('ex://p'), 204 | dataFactory.namedNode(`ex://o${i}`), 205 | dataFactory.namedNode('ex://g'), 206 | )); 207 | } 208 | this.quads = quads; 209 | }); 210 | 211 | afterEach(async function () { 212 | const { store, quads } = this; 213 | const { items } = await store.get({}); 214 | items.sort((a: Quad, b: Quad) => a.object.value < b.object.value ? -1 : 1); 215 | equalsQuadArray(items, quads); 216 | }); 217 | 218 | it('batchSize set to 1', async function () { 219 | const { store, quads } = this; 220 | await store.putStream(new ArrayIterator(quads), { batchSize: 1 }); 221 | }); 222 | 223 | it('batchSize set to the number of quads', async function () { 224 | const { store, quads } = this; 225 | await store.putStream(new ArrayIterator(quads), { batchSize: 10 }); 226 | }); 227 | 228 | it('batchSize set to a perfect divisor of the number of quads', async function () { 229 | const { store, quads } = this; 230 | await store.putStream(new ArrayIterator(quads), { batchSize: 2 }); 231 | }); 232 | 233 | it('batchSize set to an imperfect divisor of the number of quads', async function () { 234 | const { store, quads } = this; 235 | await store.putStream(new ArrayIterator(quads), { batchSize: 3 }); 236 | }); 237 | 238 | }); 239 | 240 | 241 | }; 242 | -------------------------------------------------------------------------------- /test/quadstore/quadstore.ts: -------------------------------------------------------------------------------- 1 | 2 | import type { Prefixes } from '../../dist/types/index'; 3 | import { Quadstore } from '../../dist/quadstore.js'; 4 | import { runSerializationTests } from './serialization.js'; 5 | import { runPrewriteTests } from './prewrite.js'; 6 | import { runGetTests } from './get.js'; 7 | import { runDelTests } from './del.js'; 8 | import { runRemoveTests } from './remove.js'; 9 | import { runImportTests } from './import.js'; 10 | import { runRemoveMatchesTests } from './removematches.js'; 11 | import { runPatchTests } from './patch.js'; 12 | import { runMatchTests } from './match.js'; 13 | import { runScopeTests } from './scope.js'; 14 | import { runPutTests } from './put.js'; 15 | import { runRangeTests } from './ranges.js'; 16 | 17 | const runTests = () => { 18 | runGetTests(); 19 | runDelTests(); 20 | runPutTests(); 21 | runScopeTests(); 22 | runPatchTests(); 23 | runMatchTests(); 24 | runRangeTests(); 25 | runImportTests(); 26 | runRemoveTests(); 27 | runRemoveMatchesTests(); 28 | runPrewriteTests(); 29 | runSerializationTests(); 30 | }; 31 | 32 | export const runQuadstoreTests = () => { 33 | 34 | describe('Constructor', () => { 35 | it('should throw if backend is not an instance of AbstractLevel', function (done) { 36 | try { 37 | new Quadstore({ 38 | dataFactory: (this.dataFactory as any), 39 | backend: (5 as any), 40 | }); 41 | } catch (err) { 42 | done(); 43 | } 44 | }); 45 | }); 46 | 47 | describe('Quadstore', () => { 48 | 49 | beforeEach(async function () { 50 | this.store = new Quadstore({ 51 | dataFactory: this.dataFactory, 52 | backend: this.db, 53 | indexes: this.indexes, 54 | prefixes: this.prefixes, 55 | }); 56 | await this.store.open(); 57 | }); 58 | 59 | afterEach(async function () { 60 | await this.store.close(); 61 | }); 62 | 63 | runTests(); 64 | 65 | }); 66 | 67 | describe('Quadstore, with prefixes', () => { 68 | 69 | const prefixes: Prefixes = { 70 | expandTerm: (term) => { 71 | if (term.startsWith('xsd:')) { 72 | return `http://www.w3.org/2001/XMLSchema#${term.slice(4)}`; 73 | } 74 | if (term.startsWith('rdf:')) { 75 | return `http://www.w3.org/1999/02/22-rdf-syntax-ns#${term.slice(4)}`; 76 | } 77 | if (term.startsWith('e:')) { 78 | return `ex://${term.slice(2)}`; 79 | } 80 | return term; 81 | }, 82 | compactIri: (iri) => { 83 | if (iri.startsWith('http://www.w3.org/2001/XMLSchema#')) { 84 | return `xsd:${iri.slice(33)}`; 85 | } 86 | if (iri.startsWith('http://www.w3.org/1999/02/22-rdf-syntax-ns#')) { 87 | return `rdf:${iri.slice(43)}`; 88 | } 89 | if (iri.startsWith('ex://')) { 90 | return `e:${iri.slice(5)}`; 91 | } 92 | return iri; 93 | }, 94 | }; 95 | 96 | beforeEach(async function () { 97 | this.store = new Quadstore({ 98 | dataFactory: this.dataFactory, 99 | backend: this.db, 100 | indexes: this.indexes, 101 | prefixes: this.prefixes, 102 | }); 103 | await this.store.open(); 104 | }); 105 | 106 | afterEach(async function () { 107 | await this.store.close(); 108 | }); 109 | 110 | runTests(); 111 | 112 | }); 113 | }; 114 | -------------------------------------------------------------------------------- /test/quadstore/ranges.ts: -------------------------------------------------------------------------------- 1 | 2 | import { equalsQuadArray } from '../utils/expect.js'; 3 | 4 | export const runRangeTests = () => { 5 | 6 | describe('Operations with ranges', () => { 7 | 8 | describe('String literals', () => { 9 | 10 | beforeEach(async function () { 11 | const { dataFactory } = this; 12 | this.quads = [ 13 | dataFactory.quad( 14 | dataFactory.namedNode('ex://s0'), 15 | dataFactory.namedNode('ex://p0'), 16 | dataFactory.literal('o0'), 17 | dataFactory.namedNode('ex://c0'), 18 | ), 19 | dataFactory.quad( 20 | dataFactory.namedNode('ex://s1'), 21 | dataFactory.namedNode('ex://p1'), 22 | dataFactory.literal('o1'), 23 | dataFactory.namedNode('ex://c1'), 24 | ), 25 | dataFactory.quad( 26 | dataFactory.namedNode('ex://s2'), 27 | dataFactory.namedNode('ex://p2'), 28 | dataFactory.literal('o2'), 29 | dataFactory.namedNode('ex://c2'), 30 | ), 31 | dataFactory.quad( 32 | dataFactory.namedNode('ex://s3'), 33 | dataFactory.namedNode('ex://p3'), 34 | dataFactory.literal('o3'), 35 | dataFactory.namedNode('ex://c3'), 36 | ), 37 | ]; 38 | await this.store.multiPut(this.quads); 39 | }); 40 | 41 | it('should match quads by a specific string literal', async function () { 42 | const { dataFactory, store } = this; 43 | const { items: quads } = await store.get({ 44 | object: dataFactory.literal('o2'), 45 | }); 46 | equalsQuadArray(quads, this.quads.slice(2, 3)); 47 | }); 48 | 49 | it('should match quads by a range of string literals (gte)', async function () { 50 | const { dataFactory, store } = this; 51 | const { items: quads } = await store.get({ 52 | object: { termType: 'Range', gte: dataFactory.literal('o1') }, 53 | }); 54 | equalsQuadArray(quads, this.quads.slice(1)); 55 | }); 56 | 57 | it('should match quads by a range of string literals (gt)', async function () { 58 | const { dataFactory, store } = this; 59 | const { items: quads } = await store.get({ 60 | object: { termType: 'Range', gt: dataFactory.literal('o1') }, 61 | }); 62 | equalsQuadArray(quads, this.quads.slice(2)); 63 | }); 64 | 65 | it('should match quads by a range of string literals (lte)', async function () { 66 | const { dataFactory, store } = this; 67 | const { items: quads } = await store.get({ 68 | object: { termType: 'Range', lte: dataFactory.literal('o2') }, 69 | }); 70 | equalsQuadArray(quads, this.quads.slice(0, 3)); 71 | }); 72 | 73 | it('should match quads by a range of string literals (lt)', async function () { 74 | const { dataFactory, store } = this; 75 | const { items: quads } = await store.get({ 76 | object: { termType: 'Range', lt: dataFactory.literal('o2') }, 77 | }); 78 | equalsQuadArray(quads, this.quads.slice(0, 2)); 79 | }); 80 | 81 | it('should match quads by a range of string literals (gt,lt)', async function () { 82 | const { dataFactory, store } = this; 83 | const { items: quads } = await store.get({ 84 | object: { 85 | termType: 'Range', 86 | gt: dataFactory.literal('o0'), 87 | lt: dataFactory.literal('o3'), 88 | }, 89 | }); 90 | equalsQuadArray(quads, this.quads.slice(1, 3)); 91 | }); 92 | 93 | it('should match quads by a range of string literals (gte,lt)', async function () { 94 | const { dataFactory, store } = this; 95 | const { items: quads } = await store.get({ 96 | object: { 97 | termType: 'Range', 98 | gte: dataFactory.literal('o0'), 99 | lt: dataFactory.literal('o3'), 100 | }, 101 | }); 102 | equalsQuadArray(quads, this.quads.slice(0, 3)); 103 | }); 104 | 105 | it('should match quads by a range of string literals (gt,lte)', async function () { 106 | const { dataFactory, store } = this; 107 | const { items: quads } = await store.get({ 108 | object: { 109 | termType: 'Range', 110 | gt: dataFactory.literal('o0'), 111 | lte: dataFactory.literal('o3'), 112 | }, 113 | }); 114 | equalsQuadArray(quads, this.quads.slice(1, 4)); 115 | }); 116 | 117 | it('should match quads by a range of string literals (gte,lte)', async function () { 118 | const { dataFactory, store } = this; 119 | const { items: quads } = await store.get({ 120 | object: { 121 | termType: 'Range', 122 | gte: dataFactory.literal('o1'), 123 | lte: dataFactory.literal('o2'), 124 | }, 125 | }); 126 | equalsQuadArray(quads, this.quads.slice(1, 3)); 127 | }); 128 | 129 | }); 130 | 131 | }); 132 | 133 | }; -------------------------------------------------------------------------------- /test/quadstore/remove.ts: -------------------------------------------------------------------------------- 1 | 2 | import { ArrayIterator } from 'asynciterator'; 3 | import { waitForEvent, streamToArray } from '../../dist/utils/stuff.js'; 4 | import { arrayToHaveLength } from '../utils/expect.js'; 5 | 6 | export const runRemoveTests = () => { 7 | 8 | describe('Quadstore.prototype.remove()', () => { 9 | 10 | it('should remove streamed quads correctly', async function () { 11 | const { dataFactory, store } = this; 12 | const importQuads = [ 13 | dataFactory.quad( 14 | dataFactory.namedNode('http://ex.com/s0'), 15 | dataFactory.namedNode('http://ex.com/p0'), 16 | dataFactory.literal('o0', 'en-gb'), 17 | dataFactory.namedNode('http://ex.com/g0') 18 | ), 19 | dataFactory.quad( 20 | dataFactory.namedNode('http://ex.com/s1'), 21 | dataFactory.namedNode('http://ex.com/p1'), 22 | dataFactory.literal('o1', 'en-gb'), 23 | dataFactory.namedNode('http://ex.com/g1') 24 | ), 25 | dataFactory.quad( 26 | dataFactory.namedNode('http://ex.com/s2'), 27 | dataFactory.namedNode('http://ex.com/p2'), 28 | dataFactory.literal('o2', 'en-gb'), 29 | dataFactory.namedNode('http://ex.com/g3') 30 | ) 31 | ]; 32 | const removeQuads = [ 33 | dataFactory.quad( 34 | dataFactory.namedNode('http://ex.com/s1'), 35 | dataFactory.namedNode('http://ex.com/p1'), 36 | dataFactory.literal('o1', 'en-gb'), 37 | dataFactory.namedNode('http://ex.com/g1') 38 | ), 39 | dataFactory.quad( 40 | dataFactory.namedNode('http://ex.com/s2'), 41 | dataFactory.namedNode('http://ex.com/p2'), 42 | dataFactory.literal('o2', 'en-gb'), 43 | dataFactory.namedNode('http://ex.com/g3') 44 | ) 45 | ]; 46 | const importStream = new ArrayIterator(importQuads); 47 | const removeStream = new ArrayIterator(removeQuads); 48 | await waitForEvent(store.import(importStream), 'end', true); 49 | await waitForEvent(store.remove(removeStream), 'end', true); 50 | const matchedQuads = await streamToArray(store.match()); 51 | arrayToHaveLength(matchedQuads, 1); 52 | }); 53 | 54 | }); 55 | 56 | }; 57 | -------------------------------------------------------------------------------- /test/quadstore/removematches.ts: -------------------------------------------------------------------------------- 1 | 2 | import { ArrayIterator } from 'asynciterator'; 3 | import { streamToArray, waitForEvent } from '../../dist/utils/stuff.js'; 4 | import { arrayToHaveLength } from '../utils/expect.js'; 5 | 6 | export const runRemoveMatchesTests = () => { 7 | describe('Quadstore.prototype.removeMatches()', () => { 8 | it('should remove matching quads correctly', async function () { 9 | const { dataFactory, store } = this; 10 | const importQuads = [ 11 | dataFactory.quad( 12 | dataFactory.namedNode('http://ex.com/s0'), 13 | dataFactory.namedNode('http://ex.com/p0'), 14 | dataFactory.literal('o0', 'en-gb'), 15 | dataFactory.namedNode('http://ex.com/g0') 16 | ), 17 | dataFactory.quad( 18 | dataFactory.namedNode('http://ex.com/s1'), 19 | dataFactory.namedNode('http://ex.com/p1'), 20 | dataFactory.literal('o1', 'en-gb'), 21 | dataFactory.namedNode('http://ex.com/g1') 22 | ), 23 | dataFactory.quad( 24 | dataFactory.namedNode('http://ex.com/s2'), 25 | dataFactory.namedNode('http://ex.com/p2'), 26 | dataFactory.literal('o2', 'en-gb'), 27 | dataFactory.namedNode('http://ex.com/g1') 28 | ) 29 | ]; 30 | const importStream = new ArrayIterator(importQuads); 31 | await waitForEvent(store.import(importStream), 'end', true); 32 | await waitForEvent(store.removeMatches(null, null, null, dataFactory.namedNode('http://ex.com/g1')), 'end', true); 33 | const matchedQuads = await streamToArray(store.match()); 34 | arrayToHaveLength(matchedQuads, 1); 35 | }); 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /test/quadstore/scope.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | import { streamToArray } from '../../dist/utils/stuff.js'; 4 | import { Scope } from '../../dist/scope/index.js'; 5 | import { LevelIterator } from '../../dist/get/leveliterator.js'; 6 | import { arrayToHaveLength, toNotEqualTerm, toBeAnArray, toBeInstanceOf } from '../utils/expect.js'; 7 | 8 | export const runScopeTests = () => { 9 | 10 | describe('Quadstore.prototype.initScope()', () => { 11 | 12 | it('Should return a newly-instantiated scope', async function () { 13 | const { store } = this; 14 | const scope = await store.initScope(); 15 | toBeInstanceOf(scope, Scope); 16 | }); 17 | 18 | }); 19 | 20 | describe('Quadstore.prototype.loadScope()', () => { 21 | 22 | it('Should return a newly-instantiated scope', async function () { 23 | const { store } = this; 24 | const scope = await store.loadScope('random-id'); 25 | toBeInstanceOf(scope, Scope); 26 | }); 27 | 28 | it('Should not leak mappings between different scopes', async function () { 29 | const {dataFactory, store} = this; 30 | const scopeA = await store.initScope(); 31 | const scopeB = await store.initScope(); 32 | const quad = dataFactory.quad( 33 | dataFactory.namedNode('ex://s'), 34 | dataFactory.namedNode('ex://p'), 35 | dataFactory.blankNode('bo'), 36 | dataFactory.namedNode('ex://g'), 37 | ); 38 | await store.put(quad, { scope: scopeA }); 39 | await store.put(quad, { scope: scopeB }); 40 | const { items } = await store.get({}); 41 | arrayToHaveLength(items, 2); 42 | const reloadedScopeA = await store.loadScope(scopeA.id); 43 | const reloadedScopeB = await store.loadScope(scopeB.id); 44 | toNotEqualTerm(reloadedScopeA.blankNodes.get('bo'), reloadedScopeB.blankNodes.get('bo')); 45 | }); 46 | 47 | }); 48 | 49 | describe('Quadstore.prototype.deleteScope()', () => { 50 | 51 | it('Should delete the mappings of a given scope', async function () { 52 | const {dataFactory, store} = this; 53 | const scopeA = await store.initScope(); 54 | const scopeB = await store.initScope(); 55 | const quad = dataFactory.quad( 56 | dataFactory.namedNode('ex://s'), 57 | dataFactory.namedNode('ex://p'), 58 | dataFactory.blankNode('bo'), 59 | dataFactory.namedNode('ex://g'), 60 | ); 61 | await store.put(quad, { scope: scopeA }); 62 | await store.put(quad, { scope: scopeB }); 63 | await store.deleteScope(scopeA.id); 64 | const entriesA = await streamToArray(new LevelIterator( 65 | store.db.iterator(Scope.getLevelIteratorOpts(true, true, scopeA.id)), 66 | ([key, value]) => value, 67 | )); 68 | const entriesB = await streamToArray(new LevelIterator( 69 | store.db.iterator(Scope.getLevelIteratorOpts(true, true, scopeB.id)), 70 | ([key, value]) => value, 71 | )); 72 | toBeAnArray(entriesA); 73 | toBeAnArray(entriesB); 74 | arrayToHaveLength(entriesA, 0); 75 | arrayToHaveLength(entriesB, 1); 76 | }); 77 | 78 | }); 79 | 80 | describe('Quadstore.prototype.deleteAllScopes()', () => { 81 | 82 | it('Should delete all scope mappings', async function () { 83 | const {dataFactory, store} = this; 84 | const scopeA = await store.initScope(); 85 | const scopeB = await store.initScope(); 86 | const quad = dataFactory.quad( 87 | dataFactory.namedNode('ex://s'), 88 | dataFactory.namedNode('ex://p'), 89 | dataFactory.blankNode('bo'), 90 | dataFactory.namedNode('ex://g'), 91 | ); 92 | await store.put(quad, { scope: scopeA }); 93 | await store.put(quad, { scope: scopeB }); 94 | await store.deleteAllScopes(); 95 | const entries = await streamToArray(new LevelIterator( 96 | store.db.iterator(Scope.getLevelIteratorOpts(true, true)), 97 | ([key, value]) => value, 98 | )); 99 | toBeAnArray(entries); 100 | arrayToHaveLength(entries, 0); 101 | }); 102 | 103 | }); 104 | 105 | 106 | 107 | }; 108 | -------------------------------------------------------------------------------- /test/quadstore/serialization.ts: -------------------------------------------------------------------------------- 1 | 2 | import type {InternalIndex} from '../../dist/types/index.js'; 3 | 4 | import * as xsd from '../../dist/serialization/xsd.js'; 5 | import {quadReader, twoStepsQuadWriter} from '../../dist/serialization/index.js'; 6 | import { toEqualQuad } from '../utils/expect.js'; 7 | 8 | export const runSerializationTests = () => { 9 | 10 | describe('Quadstore serialization', function () { 11 | 12 | it('Should serialize and deserialize quads with named nodes', function () { 13 | const { store } = this; 14 | const { indexes, prefixes, dataFactory: factory } = store; 15 | const quad = factory.quad( 16 | factory.namedNode('http://ex.com/s'), 17 | factory.namedNode('http://ex.com/p'), 18 | factory.namedNode('http://ex.com/o'), 19 | factory.namedNode('http://ex.com/g'), 20 | ); 21 | indexes.forEach((index: InternalIndex) => { 22 | const key = twoStepsQuadWriter.ingest(quad, prefixes).write(index.prefix, index.terms); 23 | const read = quadReader.read(key, index.prefix.length, index.terms, factory, prefixes); 24 | toEqualQuad(read, quad); 25 | }); 26 | }); 27 | 28 | it('Should serialize and deserialize quads in the default graph', function () { 29 | const { store } = this; 30 | const { indexes, prefixes, dataFactory: factory } = store; 31 | const quad = factory.quad( 32 | factory.namedNode('http://ex.com/s'), 33 | factory.namedNode('http://ex.com/p'), 34 | factory.namedNode('http://ex.com/o'), 35 | factory.defaultGraph(), 36 | ); 37 | indexes.forEach((index: InternalIndex) => { 38 | const key = twoStepsQuadWriter.ingest(quad, prefixes).write(index.prefix, index.terms); 39 | const read = quadReader.read(key, index.prefix.length, index.terms, factory, prefixes); 40 | toEqualQuad(read, quad); 41 | }); 42 | }); 43 | 44 | it('Should serialize and deserialize quads with generic literals', function () { 45 | const { store } = this; 46 | const { indexes, prefixes, dataFactory: factory } = store; 47 | const quad = factory.quad( 48 | factory.namedNode('http://ex.com/s'), 49 | factory.namedNode('http://ex.com/p'), 50 | factory.literal('someValue', factory.namedNode('http://ex.com/someDatatype')), 51 | factory.namedNode('http://ex.com/g'), 52 | ); 53 | indexes.forEach((index: InternalIndex) => { 54 | const key = twoStepsQuadWriter.ingest(quad, prefixes).write(index.prefix, index.terms); 55 | const read = quadReader.read(key, index.prefix.length, index.terms, factory, prefixes); 56 | toEqualQuad(read, quad); 57 | }); 58 | }); 59 | 60 | it('Should serialize and deserialize quads with named nodes and language-tagged literals', function () { 61 | const { store } = this; 62 | const { indexes, prefixes, dataFactory: factory } = store; 63 | const quad = factory.quad( 64 | factory.namedNode('http://ex.com/s'), 65 | factory.namedNode('http://ex.com/p'), 66 | factory.literal('Hello, world!', 'en'), 67 | factory.namedNode('http://ex.com/g'), 68 | ); 69 | indexes.forEach((index: InternalIndex) => { 70 | const key = twoStepsQuadWriter.ingest(quad, prefixes).write(index.prefix, index.terms); 71 | const read = quadReader.read(key, index.prefix.length, index.terms, factory, prefixes); 72 | toEqualQuad(read, quad); 73 | }); 74 | }); 75 | 76 | it('Should serialize and deserialize quads with named nodes and numeric literals', function () { 77 | const { store } = this; 78 | const { indexes, prefixes, dataFactory: factory } = store; 79 | const quad = factory.quad( 80 | factory.namedNode('http://ex.com/s'), 81 | factory.namedNode('http://ex.com/p'), 82 | factory.literal('44', factory.namedNode(xsd.decimal)), 83 | factory.namedNode('http://ex.com/g'), 84 | ); 85 | indexes.forEach((index: InternalIndex) => { 86 | const key = twoStepsQuadWriter.ingest(quad, prefixes).write(index.prefix, index.terms); 87 | const read = quadReader.read(key, index.prefix.length, index.terms, factory, prefixes); 88 | toEqualQuad(read, quad); 89 | }); 90 | }); 91 | 92 | it('Should serialize and deserialize a quad having a literal term that serializes to a string longer than 127 chars', async function () { 93 | const { store: { dataFactory: factory, indexes }, prefixes } = this; 94 | const quad = factory.quad( 95 | factory.namedNode('http://ex.com/s'), 96 | factory.namedNode('http://ex.com/p'), 97 | factory.literal(''.padStart(129, 'abab')), 98 | factory.namedNode('http://ex.com/g'), 99 | ); 100 | indexes.forEach((index: InternalIndex) => { 101 | const key = twoStepsQuadWriter.ingest(quad, prefixes).write(index.prefix, index.terms); 102 | const read = quadReader.read(key, index.prefix.length, index.terms, factory, prefixes); 103 | toEqualQuad(read, quad); 104 | }); 105 | }); 106 | 107 | it('Should serialize and deserialize a quad having a NaN literal term', async function () { 108 | const { store: { dataFactory: factory, indexes }, prefixes } = this; 109 | const quad = factory.quad( 110 | factory.namedNode('http://ex.com/s'), 111 | factory.namedNode('http://ex.com/p'), 112 | factory.literal('NaN', factory.namedNode(xsd.double)), 113 | factory.namedNode('http://ex.com/g'), 114 | ); 115 | indexes.forEach((index: InternalIndex) => { 116 | const key = twoStepsQuadWriter.ingest(quad, prefixes).write(index.prefix, index.terms); 117 | const read = quadReader.read(key, index.prefix.length, index.terms, factory, prefixes); 118 | toEqualQuad(read, quad); 119 | }); 120 | }); 121 | 122 | }); 123 | 124 | }; 125 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "esnext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | "lib": ["dom"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "esnext", /* Specify what module code is generated. */ 29 | "rootDir": "./", /* Specify the root folder within your source files. */ 30 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "resolveJsonModule": true, /* Enable importing .json files. */ 39 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 40 | 41 | /* JavaScript Support */ 42 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 43 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 44 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 45 | 46 | /* Emit */ 47 | "declaration": false, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 49 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 50 | "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 51 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 52 | "outDir": "./", /* Specify an output folder for all emitted files. */ 53 | // "removeComments": true, /* Disable emitting comments. */ 54 | // "noEmit": true, /* Disable emitting files from a compilation. */ 55 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 56 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 57 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 58 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 63 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 64 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 67 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 70 | 71 | /* Interop Constraints */ 72 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 73 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 74 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 76 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 77 | 78 | /* Type Checking */ 79 | "strict": true, /* Enable all strict type-checking options. */ 80 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 81 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 82 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 83 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 84 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 85 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 86 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 87 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 88 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 89 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 90 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 91 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 92 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 93 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 94 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 95 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 96 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 97 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 98 | 99 | /* Completeness */ 100 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 101 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 102 | }, 103 | "include": ["./**/*.ts"], 104 | "exclude": [], 105 | } 106 | -------------------------------------------------------------------------------- /test/utils/expect.ts: -------------------------------------------------------------------------------- 1 | 2 | import type { Quad, Term } from '@rdfjs/types'; 3 | 4 | import { expect } from 'chai'; 5 | import { termNames } from '../../dist/utils/constants.js'; 6 | 7 | export const toBeTrue = (value: any) => { 8 | expect(value).to.be.true; 9 | }; 10 | 11 | export const toBeFalse = (value: any) => { 12 | expect(value).to.be.false; 13 | }; 14 | 15 | export const toBeAnArray = (value: any, length?: number) => { 16 | expect(value).to.be.an('array'); 17 | if (typeof length === 'number') { 18 | expect(value).to.have.length(length); 19 | } 20 | }; 21 | 22 | export const toBeAnObject = (value: any) => { 23 | expect(value).to.be.an('object'); 24 | }; 25 | 26 | export const toBeAString = (value: any) => { 27 | expect(value).to.be.a('string'); 28 | }; 29 | 30 | export const toBeATerm = (value: any) => { 31 | expect(value).to.be.an('object'); 32 | expect(value).to.have.property('termType'); 33 | expect(value.termType).to.be.a('string'); 34 | }; 35 | 36 | export const toBeAQuad = (value: any) => { 37 | expect(value).to.be.an('object'); 38 | termNames.forEach((termName) => { 39 | expect(value).to.have.property(termName); 40 | toBeATerm(value[termName]); 41 | }); 42 | }; 43 | 44 | export const toBeAFiniteNumber = (value: any) => { 45 | expect(value).to.be.a('number'); 46 | expect(value).not.to.be.NaN; 47 | expect(value).not.to.equal(Infinity); 48 | expect(value).not.to.equal(-Infinity); 49 | }; 50 | 51 | export const toBeLessThanOrEqualTo = (value: any, compare: number) => { 52 | expect(value).not.to.be.greaterThan(compare); 53 | }; 54 | 55 | export const toStrictlyEqual = (value: any, compare: any) => { 56 | expect(value).to.equal(compare); 57 | }; 58 | 59 | export const toBeAQuadArray = (value: any, length?: number) => { 60 | expect(value).to.be.an('array'); 61 | if (typeof length === 'number') { 62 | expect(value).to.have.length(length); 63 | } 64 | for (let i = 0, l = value.length; i < l; i += 1) { 65 | toBeAQuad(value[i]); 66 | } 67 | }; 68 | 69 | export const toEqualTerm = (value: any, expected: Term) => { 70 | toBeATerm(value); 71 | expect(value.termType).to.equal(expected.termType); 72 | expect(value.value).to.equal(expected.value); 73 | if (expected.termType === 'Literal') { 74 | expect(value.language).to.equal(expected.language); 75 | if (expected.datatype) { 76 | toEqualTerm(value.datatype, expected.datatype); 77 | } 78 | } 79 | }; 80 | 81 | export const toNotEqualTerm = (value: any, expected: Term): undefined => { 82 | toBeATerm(value); 83 | if (value.termType !== expected.termType) { 84 | return; 85 | } 86 | if (value.value !== expected.value) { 87 | return; 88 | } 89 | if (expected.termType === 'Literal') { 90 | if (expected.language && value.language !== expected.language) { 91 | return; 92 | } 93 | if (expected.datatype) { 94 | return toNotEqualTerm(value.datatype, expected.datatype); 95 | } 96 | } 97 | throw new Error(`expected ${value} not to be equal to ${expected}`); 98 | }; 99 | 100 | export const toEqualQuad = (value: any, expected: Quad) => { 101 | toBeAQuad(value); 102 | toEqualTerm(value.subject, expected.subject); 103 | toEqualTerm(value.predicate, expected.predicate); 104 | toEqualTerm(value.object, expected.object); 105 | toEqualTerm(value.graph, expected.graph); 106 | }; 107 | 108 | export const equalsQuadArray = (value: any, expected: Quad[]) => { 109 | toBeAnArray(value); 110 | toStrictlyEqual(value.length, expected.length); 111 | for (let i = 0, l = expected.length; i < l; i += 1) { 112 | toEqualQuad(value[i], expected[i]); 113 | } 114 | }; 115 | 116 | export const arrayToStartWith = (arr: any[], start: any[]) => { 117 | for (let i = 0, l = start.length; i < l; i += 1) { 118 | expect(arr[i]).to.equal(start[i]); 119 | } 120 | }; 121 | 122 | export const arrayToHaveLength = (arr: any[], length: number) => { 123 | expect(arr).to.have.length(length); 124 | }; 125 | 126 | export const toBeInstanceOf = (value: any, clss: new (...args : any[]) => T) => { 127 | expect(value).to.be.instanceOf(clss); 128 | }; 129 | 130 | export const toEqualUint8Array = (value: any, expected: Uint8Array) => { 131 | expect(value).to.be.an('Uint8Array'); 132 | expect(value).to.have.length(expected.length); 133 | for (let i = 0, l = expected.length; i < l; i += 1) { 134 | expect(value[i]).to.equal(expected[i]); 135 | } 136 | }; 137 | -------------------------------------------------------------------------------- /test/utils/stuff.js: -------------------------------------------------------------------------------- 1 | export const iteratorToArray = (iterator) => { 2 | return new Promise((resolve, reject) => { 3 | const arr = []; 4 | iterator.on('data', (item) => { 5 | arr.push(item); 6 | }); 7 | iterator.on('end', () => { 8 | resolve(arr); 9 | }); 10 | }); 11 | }; 12 | export const delayIterator = (iterator, maxDelay = 5) => { 13 | return iterator.transform({ transform: (item, done, push) => { 14 | setTimeout(() => { 15 | push(item); 16 | done(); 17 | }, Math.round(Math.random() * maxDelay)); 18 | } }); 19 | }; 20 | export const equalsUint8Array = (a, b) => { 21 | if (a.byteLength !== b.byteLength) { 22 | return false; 23 | } 24 | for (let i = 0; i < a.length; i += 1) { 25 | if (a[i] !== b[i]) { 26 | return false; 27 | } 28 | } 29 | return true; 30 | }; 31 | //# sourceMappingURL=stuff.js.map -------------------------------------------------------------------------------- /test/utils/stuff.ts: -------------------------------------------------------------------------------- 1 | 2 | import type { AsyncIterator } from 'asynciterator'; 3 | 4 | export const iteratorToArray = (iterator: AsyncIterator): Promise => { 5 | return new Promise((resolve, reject) => { 6 | const arr: T[] = []; 7 | iterator.on('data', (item: T) => { 8 | arr.push(item); 9 | }); 10 | iterator.on('end', () => { 11 | resolve(arr); 12 | }); 13 | }); 14 | }; 15 | 16 | export const delayIterator = (iterator: AsyncIterator, maxDelay = 5): AsyncIterator => { 17 | return iterator.transform({ transform: (item, done, push) => { 18 | setTimeout(() => { 19 | push(item); 20 | done(); 21 | }, Math.round(Math.random() * maxDelay)); 22 | }}); 23 | }; 24 | 25 | export const equalsUint8Array = (a: Uint8Array, b: Uint8Array) => { 26 | if (a.byteLength !== b.byteLength) { 27 | return false; 28 | } 29 | for (let i = 0; i < a.length; i += 1) { 30 | if (a[i] !== b[i]) { 31 | return false; 32 | } 33 | } 34 | return true; 35 | }; 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*"], 3 | "exclude": ["dist", "test", "node_modules"], 4 | "compilerOptions": { 5 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 6 | 7 | /* Basic Options */ 8 | // "incremental": true, /* Enable incremental compilation */ 9 | "target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 10 | "module": "es2020", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 11 | // "lib": [], /* Specify library files to be included in the compilation. */ 12 | "allowJs": true, /* Allow javascript files to be compiled. */ 13 | "checkJs": false, /* Report errors in .js files. */ 14 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 15 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 16 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 17 | "sourceMap": true, /* Generates corresponding '.map' file. */ 18 | // "outFile": "./", /* Concatenate and emit output to single file. */ 19 | "outDir": "./dist", /* Redirect output structure to the directory. */ 20 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 21 | // "composite": true, /* Enable project compilation */ 22 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 23 | "removeComments": true, /* Do not emit comments to output. */ 24 | // "noEmit": true, /* Do not emit outputs. */ 25 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 26 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 27 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 28 | 29 | /* Strict Type-Checking Options */ 30 | "strict": true, /* Enable all strict type-checking options. */ 31 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 32 | // "strictNullChecks": true, /* Enable strict null checks. */ 33 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 34 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 35 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 36 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 37 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 38 | 39 | /* Additional Checks */ 40 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 41 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 42 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 43 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 44 | 45 | /* Module Resolution Options */ 46 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 47 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 48 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 49 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 50 | // "typeRoots": [], /* List of folders to include type definitions from. */ 51 | // "types": [], /* Type declaration files to be included in compilation. */ 52 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 53 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 54 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 55 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 56 | 57 | /* Source Map Options */ 58 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 61 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 62 | 63 | /* Experimental Options */ 64 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 65 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 66 | 67 | /* Advanced Options */ 68 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 69 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 70 | } 71 | } 72 | --------------------------------------------------------------------------------