├── .babelrc ├── .changeset ├── README.md └── config.json ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ ├── docs.yml │ ├── npmpublish.yml │ └── test.yml ├── .gitignore ├── .husky └── pre-commit ├── CHANGELOG.md ├── README.md ├── docs ├── .nojekyll ├── ASK.md ├── CONSTRUCT.md ├── DELETE-DATA.md ├── DELETE.md ├── DESCRIBE.md ├── INSERT-DATA.md ├── INSERT.md ├── README.md ├── SELECT.md ├── _sidebar.md ├── examples │ ├── execute-construct.js │ └── execute-select.js ├── execute.md ├── expressions.md ├── index.html ├── modifiers.md ├── overview.md └── prologue.md ├── package-lock.json ├── package.json ├── src ├── expressions.ts ├── index.ts └── lib │ ├── AskBuilder.ts │ ├── ConstructBuilder.ts │ ├── DeleteBuilder.ts │ ├── DescribeBuilder.ts │ ├── InsertBuilder.ts │ ├── QueryError.ts │ ├── SelectBuilder.ts │ ├── TemplateResult.ts │ ├── WithBuilder.ts │ ├── execute.ts │ ├── expressions │ ├── in.ts │ ├── union.ts │ └── values.ts │ ├── index.ts │ └── partials │ ├── DATA.ts │ ├── FROM.ts │ ├── GROUP.ts │ ├── HAVING.ts │ ├── INSERT.ts │ ├── LIMIT.ts │ ├── ORDER.ts │ ├── WHERE.ts │ └── prologue.ts ├── test ├── ASK.test.ts ├── CONSTRUCT.test.ts ├── DELETE.test.ts ├── DELETE_DATA.test.ts ├── DESCRIBE.test.ts ├── INSERT.test.ts ├── INSERT_DATA.test.ts ├── SELECT.test.ts ├── WITH.test.ts ├── __snapshots__ │ ├── ASK.test.ts.snap │ ├── CONSTRUCT.test.ts.snap │ ├── DELETE.test.ts.snap │ ├── DELETE_DATA.test.ts.snap │ ├── DESCRIBE.test.ts.snap │ ├── INSERT.test.ts.snap │ ├── INSERT_DATA.test.ts.snap │ └── SELECT.test.ts.snap ├── _mocks.ts ├── e2e.test.ts ├── execute.test.ts ├── expressions │ ├── IN.test.ts │ ├── UNION.test.ts │ └── VALUES.test.ts ├── mochaSetup.js └── sparql.ts ├── tsconfig.eslint.json └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "presets": [ 5 | [ 6 | "@babel/preset-env", 7 | { 8 | "targets": { 9 | "node": "current" 10 | } 11 | } 12 | ], 13 | "@babel/preset-typescript" 14 | ] 15 | }, 16 | "modules": { 17 | "presets": [ 18 | "@babel/preset-typescript" 19 | ], 20 | "plugins": [ 21 | ["babel-plugin-add-import-extension", { "extension": "mjs" }] 22 | ] 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/master/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@1.5.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "linked": [], 6 | "access": "restricted", 7 | "baseBranch": "master", 8 | "updateInternalDependencies": "patch", 9 | "ignore": [] 10 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_size = 2 3 | indent_style = space 4 | insert_final_newline = true 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | main.js 2 | docs/examples/ 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ "@tpluscode" ], 3 | "env": { 4 | "mocha": true, 5 | "browser": true 6 | }, 7 | "parserOptions": { 8 | "project": "./tsconfig.eslint.json" 9 | }, 10 | "overrides": [ 11 | { 12 | "files": ["docs/examples/**"], 13 | "rules": { 14 | "@typescript-eslint/no-var-requires": "off" 15 | } 16 | }, 17 | { 18 | "files": ["test/**"], 19 | "rules": { 20 | "@typescript-eslint/no-explicit-any": "off" 21 | } 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - run: npm ci 16 | - run: npx typedoc 17 | - name: GH Pages deploy 18 | uses: Cecilapp/GitHub-Pages-deploy@2.0.0 19 | env: 20 | EMAIL: tomasz@t-code.pl 21 | GH_TOKEN: ${{ secrets.ACCESS_TOKEN }} 22 | BUILD_DIR: docs/ 23 | -------------------------------------------------------------------------------- /.github/workflows/npmpublish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout Repo 14 | uses: actions/checkout@v4 15 | with: 16 | # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits 17 | fetch-depth: 0 18 | 19 | - name: Setup Node.js 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: 18 23 | 24 | - name: Install Dependencies 25 | run: npm ci 26 | 27 | - name: Create Release Pull Request or Publish to npm 28 | id: changesets 29 | uses: changesets/action@v1 30 | with: 31 | # This expects you to have a script called release which does a build for your packages and calls changeset publish 32 | publish: yarn release 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 36 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node: [ '20', '18' ] 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-node@v4 15 | with: 16 | node-version: ${{ matrix.node }} 17 | - run: npm ci 18 | - run: yarn build 19 | - run: yarn test 20 | - run: npx typedoc 21 | - name: Codecov 22 | uses: codecov/codecov-action@v3 23 | with: 24 | token: ${{ secrets.CODECOV_TOKEN }} 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.d.ts 3 | *.d.ts.map 4 | coverage/ 5 | *.js 6 | !main.js 7 | docs/api/ 8 | *.mjs 9 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 3.0.1 4 | 5 | ### Patch Changes 6 | 7 | - a4736b6: `UNION` expression typing did not accept a subquery 8 | 9 | ## 3.0.0 10 | 11 | ### Major Changes 12 | 13 | - ae69cd7: Removed built-in logging. Use an optional parameter to `execute`. Exceptions will be reported as a custom `Error` subclass 14 | 15 | ## 2.0.4 16 | 17 | ### Patch Changes 18 | 19 | - 463ea01: Update anylogger to 1.1-beta 20 | 21 | ## 2.0.3 22 | 23 | ### Patch Changes 24 | 25 | - 43e8ce9: Export `WithQuery` to avoid error like "The inferred type of 'X' cannot be named without a reference to '/node_modules/@tpluscode/sparql-builder/lib/WithBuilder.js'. This is likely not portable. A type annotation is necessary." 26 | 27 | ## 2.0.2 28 | 29 | ### Patch Changes 30 | 31 | - c48bd28: Export the `*Executable` types 32 | 33 | ## 2.0.1 34 | 35 | ### Patch Changes 36 | 37 | - 5e451cd: Correct imports to work with `moduleResolution = NodeNext` 38 | 39 | ## 2.0.0 40 | 41 | ### Major Changes 42 | 43 | - b34931c: Updated `sparql-http-client` to v3 44 | 45 | The `.execute` method now takes an instance of client and not the `query` object. Also, since the new client does not return a promise from methods which return streams, `await` is no longer necessary 46 | 47 | ## 1.1.0 48 | 49 | ### Minor Changes 50 | 51 | - 127f72f: No longer depends on `debug`. Use `anylogger-*` adapters to receive logs 52 | 53 | ### Patch Changes 54 | 55 | - 5bdc6b0: Relax dependency on `@tpluscode/rdf-ns-builders` 56 | 57 | ## 1.0.1 58 | 59 | ### Patch Changes 60 | 61 | - 1ad0905: Update `@tpluscode/rdf-string` - improves inference of return types 62 | 63 | ## 1.0.0 64 | 65 | ### Major Changes 66 | 67 | - 5b1a043: Package is now ESM-only 68 | 69 | ## 0.4.1 70 | 71 | ### Patch Changes 72 | 73 | - 9812437: ESM exported types which failed in runtime 74 | 75 | ## 0.4.0 76 | 77 | ### Minor Changes 78 | 79 | - beaf9e1: When interpolating `SELECT` inside another template, it will automatically be wrapped in a graph pattern 80 | 81 | ## 0.3.31 82 | 83 | ### Patch Changes 84 | 85 | - c7cea63: Main module incorrectly exported types as JS 86 | 87 | ## 0.3.30 88 | 89 | ### Patch Changes 90 | 91 | - d00f737: Export full builder types of query forms 92 | - 0f5f895: Added `DISTINCT` and `AND` which allow modifying the query after it has been initialised 93 | 94 | ## 0.3.29 95 | 96 | ### Patch Changes 97 | 98 | - 66cb644: Allow `string` as parameter of `UNION` 99 | 100 | ## 0.3.28 101 | 102 | ### Patch Changes 103 | 104 | - e04f163: SPARQL `UNION` expression builder 105 | 106 | ## 0.3.27 107 | 108 | ### Patch Changes 109 | 110 | - ec05a22: Added direct dependency on `@types/sparql-http-client` 111 | 112 | ## 0.3.26 113 | 114 | ### Patch Changes 115 | 116 | - 92e4fcf: Add `prefixes` parameter to `build` so ad-hoc prefixes can be applied to a query 117 | Re-export `prefixes` where it's possible to [add prefixes globally](https://github.com/zazuko/rdf-vocabularies#project-specific-prefixes) 118 | (re #81) 119 | 120 | ## 0.3.25 121 | 122 | ### Patch Changes 123 | 124 | - 9f681a2: Type declaration prevented `DELETE.DATA` where the interpolated value was quad array 125 | 126 | ## 0.3.24 127 | 128 | ### Patch Changes 129 | 130 | - 3f1495e: Adding prologue before the prefixes (closes #75) 131 | 132 | ## 0.3.23 133 | 134 | ### Patch Changes 135 | 136 | - 329c89e: Shorthand `CONSTRUCT` syntax would interpolate templates incorrectly, producing invalid SPARQL 137 | 138 | ## 0.3.22 139 | 140 | ### Patch Changes 141 | 142 | - 551f6a1: Updated `@tpluscode/rdf-ns-builders` to v2 143 | 144 | ## 0.3.21 145 | 146 | ### Patch Changes 147 | 148 | - 3231508: Correct syntax for `FROM` in shorthand `CONSTRUCT` 149 | 150 | ## 0.3.20 151 | 152 | ### Patch Changes 153 | 154 | - 6200b56: Add shorthand form `CONSTRUCT WHERE` 155 | 156 | ## 0.3.19 157 | 158 | ### Patch Changes 159 | 160 | - a4dea43: `DESCRIBE` query function should support `ORDER BY` clause 161 | 162 | ## 0.3.18 163 | 164 | ### Patch Changes 165 | 166 | - 0e045af: Wrong URLs in package meta 167 | 168 | ## 0.3.17 169 | 170 | ### Patch Changes 171 | 172 | - ede2fb5: Export types representing queries which can be executed and built 173 | - 837447c: Support for `GROUP BY` and `HAVING` clauses in `SELECT` (closes #57) 174 | 175 | ## 0.3.16 176 | 177 | ### Patch Changes 178 | 179 | - 3e72cd3: Export a function to create `IN` function 180 | - 4afbddb: Builder function for `VALUES` (closes #4) 181 | 182 | ## 0.3.15 183 | 184 | ### Patch Changes 185 | 186 | - 795ff8a: Export SparqlTemplateResult as type 187 | 188 | ## 0.3.14 189 | 190 | ### Patch Changes 191 | 192 | - d9a173b: Re-export SPARQL template from `@tpluscode/rdf-string` 193 | - 51c985a: Update @tpluscode/rdf-ns-builders and rdf-js types 194 | 195 | ## 0.3.13 196 | 197 | ### Patch Changes 198 | 199 | - fbf2e33: Re-export `sparql` string template tag function from [@tpluscode/rdf-string](https://npm.im/@tpluscode/rdf-string) 200 | 201 | ## 0.3.12 202 | 203 | ### Patch Changes 204 | 205 | - 928b9d9: Update @tpluscode/rdf-ns-builders 206 | 207 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 208 | 209 | ### [0.3.11](https://github.com/tpluscode/ts-template/compare/v0.3.10...v0.3.11) (2021-02-18) 210 | 211 | ### Features 212 | 213 | - added WITH clause ([b36a0b2](https://github.com/tpluscode/ts-template/commit/b36a0b22e2de2c59a5fbe4f92a226464118a226d)) 214 | 215 | ### [0.3.10](https://github.com/tpluscode/ts-template/compare/v0.3.9...v0.3.10) (2020-11-10) 216 | 217 | ### Features 218 | 219 | - add FROM to ASK and DESCRIBE builders ([63ccaea](https://github.com/tpluscode/ts-template/commit/63ccaea068243e14b9a2fff9e4ebc8b6eabd07f6)) 220 | 221 | ### [0.3.9](https://github.com/tpluscode/ts-template/compare/v0.3.8...v0.3.9) (2020-08-08) 222 | 223 | ### Features 224 | 225 | - multiple FROM statements ([5fa4f39](https://github.com/tpluscode/ts-template/commit/5fa4f39c1e2e8ff3f986087853b2fa5b73b2f4e0)) 226 | 227 | ### [0.3.8](https://github.com/tpluscode/ts-template/compare/v0.3.7...v0.3.8) (2020-08-08) 228 | 229 | ### Features 230 | 231 | - add FROM (NAMED) to Construct queries ([4a3d662](https://github.com/tpluscode/ts-template/commit/4a3d662974deb3b331160afb013351ddf605a189)) 232 | - add FROM NAMED clause builder ([2451561](https://github.com/tpluscode/ts-template/commit/2451561fbb02557dff6219078dd001518abd8186)) 233 | 234 | ### [0.3.7](https://github.com/tpluscode/ts-template/compare/v0.3.6...v0.3.7) (2020-08-02) 235 | 236 | ### Features 237 | 238 | - **select:** added SELECT \* shorthand ([ae9fdf4](https://github.com/tpluscode/ts-template/commit/ae9fdf418e204c9bea5849bf5083710bdc8f2df8)) 239 | 240 | ### Bug Fixes 241 | 242 | - **select:** added more intuitive API for multiple ORDER BY ([02a31d1](https://github.com/tpluscode/ts-template/commit/02a31d1ebcf6afd0a930749e3e0231aaa5203032)) 243 | - **select:** ordering descending was not implemented ([4537ca8](https://github.com/tpluscode/ts-template/commit/4537ca8242cf9c726f3edd0f6934c62db9b72864)) 244 | 245 | ### [0.3.6](https://github.com/tpluscode/ts-template/compare/v0.3.5...v0.3.6) (2020-07-01) 246 | 247 | ### [0.3.5](https://github.com/tpluscode/ts-template/compare/v0.3.4...v0.3.5) (2020-06-08) 248 | 249 | ### [0.3.4](https://github.com/tpluscode/ts-template/compare/v0.3.3...v0.3.4) (2020-06-07) 250 | 251 | ### [0.3.3](https://github.com/tpluscode/ts-template/compare/v0.3.2...v0.3.3) (2020-05-21) 252 | 253 | ### [0.3.2](https://github.com/tpluscode/ts-template/compare/v0.3.1...v0.3.2) (2020-04-08) 254 | 255 | ### Bug Fixes 256 | 257 | - skip INSERT in DELETE when empty ([5f0a734](https://github.com/tpluscode/ts-template/commit/5f0a734337480bb618c52b269bca62d64c7c8237)) 258 | 259 | ### [0.3.1](https://github.com/tpluscode/ts-template/compare/v0.3.0...v0.3.1) (2020-04-01) 260 | 261 | ### Bug Fixes 262 | 263 | - parameters of execute were not correctly inferred ([87ed666](https://github.com/tpluscode/ts-template/commit/87ed666a853a46adc08eba33ee8eaf9dddc38080)) 264 | 265 | ## [0.3.0](https://github.com/tpluscode/ts-template/compare/v0.2.2...v0.3.0) (2020-04-01) 266 | 267 | ### ⚠ BREAKING CHANGES 268 | 269 | - changes the signature of execute method 270 | 271 | ### Features 272 | 273 | - update to sparql-http-client-2 ([9953317](https://github.com/tpluscode/ts-template/commit/99533173972d87a1ca4b38d98cdf9f6fa9d30dfc)) 274 | 275 | ### [0.2.2](https://github.com/tpluscode/ts-template/compare/v0.2.1...v0.2.2) (2020-03-02) 276 | 277 | ### Bug Fixes 278 | 279 | - **execute:** incomplete export prevented the BASE from being applied ([7f301a5](https://github.com/tpluscode/ts-template/commit/7f301a51fc23999701abd4fa6e136b473f82911c)) 280 | - **execute:** the build was not actually called when executing ([437a749](https://github.com/tpluscode/ts-template/commit/437a749580f4154c37768dd203402d9ceadb223b)) 281 | 282 | ### [0.2.1](https://github.com/tpluscode/ts-template/compare/v0.2.0...v0.2.1) (2020-03-02) 283 | 284 | ### Bug Fixes 285 | 286 | - it's impossible to use BASE with execute ([2c43d2b](https://github.com/tpluscode/ts-template/commit/2c43d2bb110fb5c171c2497af11ba1108453559b)) 287 | 288 | ## [0.2.0](https://github.com/tpluscode/ts-template/compare/v0.1.2...v0.2.0) (2020-03-02) 289 | 290 | ### ⚠ BREAKING CHANGES 291 | 292 | - will potentially break if `SparqlQuery<>` was imported and not only inferred 293 | 294 | ### Bug Fixes 295 | 296 | - refactor execute to return the correct Response type ([928cdbe](https://github.com/tpluscode/ts-template/commit/928cdbe9c3e841b5546e128706df5f708a3d16e6)) 297 | 298 | ### [0.1.2](https://github.com/tpluscode/ts-template/compare/v0.1.1...v0.1.2) (2020-02-28) 299 | 300 | ### Bug Fixes 301 | 302 | - it was impossible to interpolate another (sub-)query ([a060113](https://github.com/tpluscode/ts-template/commit/a060113c5969c2b63aed716dd4ac0eff4b61af4a)) 303 | 304 | ### [0.1.1](https://github.com/tpluscode/ts-template/compare/v0.1.0...v0.1.1) (2020-02-25) 305 | 306 | ### Bug Fixes 307 | 308 | - the ORDER BY clause must come before LIMIT/OFFSET ([80f77c6](https://github.com/tpluscode/ts-template/commit/80f77c648821876281665cd59aa8fcb18471f3d5)) 309 | 310 | ## [0.1.0](https://github.com/tpluscode/ts-template/compare/v0.0.10...v0.1.0) (2020-02-24) 311 | 312 | ### ⚠ BREAKING CHANGES 313 | 314 | - CONSTRUCT/DESCRIBE now return Response and not a Stream 315 | 316 | ### Features 317 | 318 | - **select:** support for ORDER BY ([6841709](https://github.com/tpluscode/ts-template/commit/68417094b7f9d4b9686efb553d6738958bb491cb)) 319 | 320 | * change the graph query API ([ccfe4d6](https://github.com/tpluscode/ts-template/commit/ccfe4d62e71b2141a4f7432c1e5ebd18fec06bde)) 321 | 322 | ### [0.0.10](https://github.com/tpluscode/ts-template/compare/v0.0.9...v0.0.10) (2020-02-24) 323 | 324 | ### Bug Fixes 325 | 326 | - base was not applied to nested templates ([9ca4bea](https://github.com/tpluscode/ts-template/commit/9ca4bea68b3d93741a3911fa8257497a1925df27)) 327 | 328 | ### [0.0.9](https://github.com/tpluscode/ts-template/compare/v0.0.8...v0.0.9) (2020-02-24) 329 | 330 | ### [0.0.8](https://github.com/tpluscode/ts-template/compare/v0.0.7...v0.0.8) (2020-02-24) 331 | 332 | ### Features 333 | 334 | - support LIMIT/OFFSET in SPARQL Queries ([a52cc02](https://github.com/tpluscode/ts-template/commit/a52cc0275d8b9ac175e13216926f1a563e69a2fc)) 335 | - **select:** support REDUCED and DISTINCT ([e3987e8](https://github.com/tpluscode/ts-template/commit/e3987e82d5e633db919e842149f06e690ebb67bd)) 336 | 337 | ### [0.0.7](https://github.com/tpluscode/ts-template/compare/v0.0.6...v0.0.7) (2020-02-23) 338 | 339 | ### Bug Fixes 340 | 341 | - missing question marks when interpolating variables ([94e590d](https://github.com/tpluscode/ts-template/commit/94e590daf205af3c01aa7a8e8c9ca430758181e2)) 342 | 343 | ### [0.0.6](https://github.com/tpluscode/ts-template/compare/v0.0.5...v0.0.6) (2020-02-23) 344 | 345 | ### [0.0.5](https://github.com/tpluscode/ts-template/compare/v0.0.4...v0.0.5) (2020-02-23) 346 | 347 | ### [0.0.4](https://github.com/tpluscode/ts-template/compare/v0.0.3...v0.0.4) (2020-02-23) 348 | 349 | ### [0.0.3](https://github.com/tpluscode/ts-template/compare/v0.0.2...v0.0.3) (2020-02-23) 350 | 351 | ### [0.0.2](https://github.com/tpluscode/ts-template/compare/v0.0.1...v0.0.2) (2020-02-23) 352 | 353 | ### Features 354 | 355 | - **delete:** chain multiple INSERT and DELETE calls ([d5659dc](https://github.com/tpluscode/ts-template/commit/d5659dc26829eb03b60928c872ee12b54f593abb)) 356 | - **delete:** chaining multiple DATA calls ([c1d3c4c](https://github.com/tpluscode/ts-template/commit/c1d3c4cd9c507d56438f23175a05c2184140886d)) 357 | - **delete:** support DELETE..WHERE updates ([6d8dc1f](https://github.com/tpluscode/ts-template/commit/6d8dc1f17a04940ae9a7eca2b5c15c1c53e902d0)) 358 | - **insert:** chain multiple INSERT calls ([cc79fed](https://github.com/tpluscode/ts-template/commit/cc79fedf063615dbd0e1da1bd12e7186ac835913)) 359 | - **insert:** chaining multiple DATA calls ([60d2b50](https://github.com/tpluscode/ts-template/commit/60d2b50d612a5827bafb0e05bad5c4b3660adc47)) 360 | - **insert:** support INSERT..WHERE updates ([e8d254b](https://github.com/tpluscode/ts-template/commit/e8d254b17442aa689dbccd99e3c98adbb44f38de)) 361 | - **where:** chaining multiple WHERE calls ([ca88d77](https://github.com/tpluscode/ts-template/commit/ca88d77b2aefcd9fcdc7761a03f29290d19b5678)) 362 | 363 | ### 0.0.1 (2020-02-22) 364 | 365 | ### Features 366 | 367 | - moved all existing code from data-cube-curation ([71c1212](https://github.com/tpluscode/ts-template/commit/71c121246c1a61b7b23ad723da31c920fb5af778)) 368 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > # @tpluscode/sparql-builder ![Test](https://github.com/tpluscode/sparql-builder/workflows/Test/badge.svg) [![codecov](https://codecov.io/gh/tpluscode/sparql-builder/branch/master/graph/badge.svg)](https://codecov.io/gh/tpluscode/sparql-builder) [![npm version](https://badge.fury.io/js/%40tpluscode%2Fsparql-builder.svg)](https://badge.fury.io/js/%40tpluscode%2Fsparql-builder) 2 | 3 | Simple library to create SPARQL queries with tagged ES Template Strings 4 | 5 | ## How is it different from simply string concatenation/plain templates? 6 | 7 | 1. Focuses on graph patterns. No need to remember exact syntax 8 | 1. Still looks like SPARQL, having a familiar structure and little glue code 9 | 1. Has the IDE provide syntactic hints of valid SPARQL keywords 10 | 1. Ensures correct formatting of terms (URI nodes, literals variables) via [@tpluscode/rdf-string](https://github.com/tpluscode/rdf-string) 11 | 1. Automatically shortens URIs with [`@zazuko/prefixes`](http://npm.im/@zazuko/prefixes) 12 | 13 | ## Installation 14 | 15 | ``` 16 | npm i -S @tpluscode/sparql-builder 17 | ``` 18 | 19 | To be able to execute queries against a remote endpoint install a peer 20 | dependency: 21 | 22 | ``` 23 | npm i -S sparql-http-client 24 | ``` 25 | 26 | ## Usage 27 | 28 | ### Build a SPARQL query string 29 | 30 | ```js 31 | import rdf from '@zazuko/env' 32 | import { SELECT } from '@tpluscode/sparql-builder' 33 | 34 | const ex = rdf.namespace('http://example.com/') 35 | const { foaf } = rdf.ns 36 | 37 | /* 38 | PREFIX foaf: 39 | 40 | SELECT ?person ?name 41 | FROM 42 | WHERE 43 | { 44 | ?person a foaf:Person ; 45 | foaf:name ?name . 46 | } 47 | */ 48 | const person = rdf.variable('person') 49 | const query = 50 | SELECT`${person} ?name` 51 | .FROM(ex.People) 52 | .WHERE` 53 | ${person} a ${foaf.Person} ; 54 | ${foaf.name} ?name . 55 | `.build() 56 | ``` 57 | 58 | ### Executing a query 59 | 60 | Using [`sparql-http-client`](https://github.com/zazuko/sparql-http-client) 61 | 62 | ```js 63 | import rdf from '@zazuko/env' 64 | import SparqlHttp from 'sparql-http-client' 65 | import { ASK } from '@tpluscode/sparql-builder' 66 | 67 | const { dbo } = rdf.ns 68 | const dbr = rdf.namespace('http://dbpedia.org/resource/') 69 | 70 | const client = new SparqlHttp({ 71 | factory: rdf, 72 | endpointUrl: 'http://dbpedia.org/sparql', 73 | }) 74 | 75 | const scoobyDoo = dbr('Scooby-Doo') 76 | 77 | /* 78 | PREFIX dbo: 79 | 80 | ASK { 81 | a dbo:Person . 82 | } 83 | */ 84 | ASK`${scoobyDoo} a ${dbo.Person}` 85 | .execute(client) 86 | .then(isScoobyAPerson => { 87 | // Fun fact: DBpedia seems to claim that Scooby-Doo is indeed a Person... 88 | return isScoobyAPerson 89 | }) 90 | ``` 91 | 92 | ## Running examples 93 | 94 | The example in [`docs/examples`](docs/examples) can be executed locally. To do so, first replace the package import to point to the repository root. 95 | 96 | ```diff 97 | -const { /* */ } = require('@tpluscode/sparql-builder') 98 | +const { /* */ } = require('../..') 99 | ``` 100 | 101 | Then simply `npm run example`. 102 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpluscode/sparql-builder/4e9073e79b53c0786827109975657b0cb3f14b4e/docs/.nojekyll -------------------------------------------------------------------------------- /docs/ASK.md: -------------------------------------------------------------------------------- 1 | # ASK 2 | 3 | Reference: https://www.w3.org/TR/sparql11-query/#ask 4 | 5 | ## Simple query with a `WHERE` clause 6 | 7 | 8 | 9 | ```js 10 | const { ASK } = require('@tpluscode/sparql-builder') 11 | const { variable } = require('@rdfjs/data-model') 12 | const { schema } = require('@tpluscode/rdf-ns-builders') 13 | 14 | const person = variable('person') 15 | 16 | ASK`${person} a ${schema.Person}`.build() 17 | ``` 18 | 19 | 20 | -------------------------------------------------------------------------------- /docs/CONSTRUCT.md: -------------------------------------------------------------------------------- 1 | # CONSTRUCT 2 | 3 | Construct builder is similar to [`SELECT`](./SELECT.md) but the main difference is that instead of variables valid triple patterns must return from the string template. 4 | 5 | 6 | 7 | ```js 8 | const { CONSTRUCT } = require('@tpluscode/sparql-builder') 9 | const { schema } = require('@tpluscode/rdf-ns-builders') 10 | const { namedNode } = require('@rdfjs/data-model') 11 | 12 | CONSTRUCT`?person ?p ?o` 13 | .FROM(namedNode('urn:graph:default')) 14 | .FROM().NAMED(namedNode('urn:graph:john')) 15 | .FROM().NAMED(namedNode('urn:graph:jane')) 16 | .WHERE`GRAPH ?g { 17 | ?person a ${schema.Person} . 18 | ?person ?p ?o . 19 | }`.build() 20 | ``` 21 | 22 | 23 | -------------------------------------------------------------------------------- /docs/DELETE-DATA.md: -------------------------------------------------------------------------------- 1 | # DELETE DATA 2 | 3 | TBD 4 | -------------------------------------------------------------------------------- /docs/DELETE.md: -------------------------------------------------------------------------------- 1 | # DELETE 2 | 3 | TBD 4 | -------------------------------------------------------------------------------- /docs/DESCRIBE.md: -------------------------------------------------------------------------------- 1 | # DESCRIBE 2 | 3 | Reference: https://www.w3.org/TR/sparql11-query/#describe 4 | 5 | ## Typical usage 6 | 7 | 8 | 9 | ```js 10 | const { DESCRIBE } = require('@tpluscode/sparql-builder') 11 | const { variable } = require('@rdf-esm/data-model') 12 | const { schema } = require('@tpluscode/rdf-ns-builders') 13 | 14 | const person = variable('person') 15 | 16 | DESCRIBE`${person}` 17 | .WHERE` 18 | ?person a ${schema.Person} 19 | ` 20 | .build() 21 | ``` 22 | 23 | 24 | 25 | ## URIs, Without `WHERE' 26 | 27 | 28 | 29 | ```js 30 | const { DESCRIBE } = require('@tpluscode/sparql-builder') 31 | const { namedNode } = require('@rdf-esm/data-model') 32 | 33 | const person = namedNode('http://example.com/person') 34 | 35 | DESCRIBE`${person}`.build() 36 | ``` 37 | 38 | 39 | -------------------------------------------------------------------------------- /docs/INSERT-DATA.md: -------------------------------------------------------------------------------- 1 | # INSERT DATA 2 | 3 | TBD 4 | -------------------------------------------------------------------------------- /docs/INSERT.md: -------------------------------------------------------------------------------- 1 | # INSERT 2 | 3 | TBD 4 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | This package helps create SPARQL Queries/Updates in JavaScript by doing a 4 | little magic with ES Strings Templates. 5 | 6 | ## Installation 7 | 8 | ``` 9 | npm i -S @tpluscode/sparql-builder 10 | ``` 11 | 12 | ## What it can do? 13 | 14 | * Provides an API which looks like SPARQL 15 | * Uses [@tpluscode/rdf-string](https://github.com/tpluscode/rdf-string) to correctly format interpolated RDF/JS terms 16 | * Shortens URIs from [common vocabularies](https://github.com/zazuko/rdf-vocabularies/tree/master/ontologies) and introduces `PREFIX` statements 17 | * Execute queries against remote SPARQL endpoint 18 | 19 | ## What it does not do? 20 | 21 | * Provide API for keywords other than top-level (such as `OPTIONAL` or `GRAPH`) 22 | * Verify syntax 23 | * Process the constant parts (ie. not the static parts of string templates) 24 | 25 | ## Compatibility 26 | 27 | Package as both commonjs and ES modules, the package can be used in node as 28 | well as browsers. 29 | -------------------------------------------------------------------------------- /docs/SELECT.md: -------------------------------------------------------------------------------- 1 | # SELECT 2 | 3 | Reference: https://www.w3.org/TR/sparql11-query/#select 4 | 5 | ## Simple SELECT * query 6 | 7 | 8 | 9 | ```js 10 | const { SELECT } = require('@tpluscode/sparql-builder') 11 | const { variable } = require('@rdfjs/data-model') 12 | const { schema } = require('@tpluscode/rdf-ns-builders') 13 | 14 | const person = variable('person') 15 | 16 | SELECT.ALL.WHERE`${person} a ${schema.Person}`.build() 17 | ``` 18 | 19 | 20 | 21 | ## `SELECT DISTINCT` 22 | 23 | 24 | 25 | ```js 26 | const { SELECT } = require('@tpluscode/sparql-builder') 27 | const { schema } = require('@tpluscode/rdf-ns-builders') 28 | 29 | SELECT.DISTINCT`?person`.WHERE`?person a ${schema.Person}`.build() 30 | ``` 31 | 32 | 33 | 34 | ## `SELECT REDUCED` 35 | 36 | 37 | 38 | ```js 39 | const { SELECT } = require('@tpluscode/sparql-builder') 40 | const { schema } = require('@tpluscode/rdf-ns-builders') 41 | 42 | SELECT.REDUCED`?person`.WHERE`?person a ${schema.Person}`.build() 43 | ``` 44 | 45 | 46 | 47 | ## `FROM (NAMED)` 48 | 49 | 50 | 51 | ```js 52 | const { SELECT } = require('@tpluscode/sparql-builder') 53 | const { schema } = require('@tpluscode/rdf-ns-builders') 54 | const { namedNode } = require('@rdfjs/data-model') 55 | 56 | SELECT`?person` 57 | .FROM(namedNode('urn:graph:default')) 58 | .FROM().NAMED(namedNode('urn:graph:john')) 59 | .FROM().NAMED(namedNode('urn:graph:jane')) 60 | .WHERE`GRAPH ?g { 61 | ?person a ${schema.Person} 62 | }`.build() 63 | ``` 64 | 65 | 66 | 67 | ## `GROUP BY/HAVING` 68 | 69 | Simple grouping. Argument to `BY` can be a RDF/JS variable or string 70 | 71 | 72 | 73 | ```js 74 | const { SELECT } = require('@tpluscode/sparql-builder') 75 | const { schema } = require('@tpluscode/rdf-ns-builders') 76 | 77 | SELECT`?person (AVG(?age) as ?avg)` 78 | .WHERE`GRAPH ?g { 79 | ?person a ${schema.Person} ; 80 | ${schema.parent} ?parent . 81 | ?parent ${schema.age} ?age 82 | }` 83 | .GROUP().BY('person') 84 | .HAVING`AVG(?age) < 20` 85 | .build() 86 | ``` 87 | 88 | 89 | 90 | Grouping can be done by expression and using binding keyword. 91 | Similarly, argument to `AS` can be a RDF/JS variable or string 92 | 93 | 94 | 95 | ```js 96 | const { SELECT } = require('@tpluscode/sparql-builder') 97 | const { schema } = require('@tpluscode/rdf-ns-builders') 98 | 99 | SELECT`?name (AVG(?age) as ?avg)` 100 | .WHERE`GRAPH ?g { 101 | ?person a ${schema.Person} ; 102 | ${schema.parent} ?parent . 103 | ?parent ${schema.age} ?age 104 | }` 105 | .GROUP() 106 | .BY`REPLACE(STR(?person), "^(.+)/[^/]$", "")` 107 | .AS('name') 108 | .HAVING`AVG(?age) < 20` 109 | .build() 110 | ``` 111 | 112 | 113 | -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | * Getting started 2 | * [About](/) 3 | * [How it works](overview.md) 4 | * Examples 5 | * SPARQL Query 6 | * [SELECT](SELECT.md) 7 | * [ASK](ASK.md) 8 | * [DESCRIBE](DESCRIBE.md) 9 | * [CONSTRUCT](CONSTRUCT.md) 10 | * [Solution Modifiers](modifiers.md) 11 | * SPARQL Update 12 | * [INSERT](INSERT.md) 13 | * [DELETE](DELETE.md) 14 | * [INSERT DATA](INSERT-DATA.md) 15 | * [DELETE DATA](DELETE-DATA.md) 16 | * [Query prologue](prologue.md) 17 | * [Executing a query](execute.md) 18 | * [Expressions and operators](expressions.md) 19 | * References 20 | * [API](/api) 21 | -------------------------------------------------------------------------------- /docs/examples/execute-construct.js: -------------------------------------------------------------------------------- 1 | const ParsingClient = require('sparql-http-client/ParsingClient') 2 | const { CONSTRUCT, SELECT } = require('@tpluscode/sparql-builder') 3 | const { variable } = require('@rdfjs/data-model') 4 | const { dataset } = require('@rdfjs/dataset') 5 | const { dbo, foaf } = require('@tpluscode/rdf-ns-builders') 6 | const { turtle } = require('@tpluscode/rdf-string') 7 | 8 | const client = new ParsingClient({ 9 | endpointUrl: 'http://dbpedia.org/sparql', 10 | }) 11 | 12 | const person = variable('person') 13 | const peopleBornInBerlin = SELECT`${person}` 14 | .WHERE` ${person} ${dbo.birthPlace} ` 15 | .LIMIT(100) 16 | 17 | const quads = await CONSTRUCT`${person} ?p ?o` 18 | .WHERE` 19 | VALUES ?p { ${dbo.birthDate} ${dbo.deathDate} ${foaf.name} } 20 | ${person} ?p ?o . 21 | 22 | { 23 | ${peopleBornInBerlin} 24 | } 25 | ` 26 | .execute(client.query) 27 | 28 | turtle`${dataset(quads)}`.toString() 29 | -------------------------------------------------------------------------------- /docs/examples/execute-select.js: -------------------------------------------------------------------------------- 1 | const ParsingClient = require('sparql-http-client/ParsingClient') 2 | const { SELECT } = require('@tpluscode/sparql-builder') 3 | const { variable } = require('@rdfjs/data-model') 4 | const { dbo, foaf } = require('@tpluscode/rdf-ns-builders') 5 | 6 | const client = new ParsingClient({ 7 | endpointUrl: 'http://dbpedia.org/sparql', 8 | }) 9 | 10 | const name = variable('name') 11 | const birth = variable('birth') 12 | const death = variable('death') 13 | const person = variable('person') 14 | const maxBirth = new Date(1900, 1, 1) 15 | 16 | const results = await SELECT`${name} ${birth} ${death} ${person}` 17 | .WHERE` 18 | ${person} ${dbo.birthPlace} . 19 | ${person} ${dbo.birthDate} ${birth} . 20 | ${person} ${foaf.name} ${name} . 21 | ${person} ${dbo.deathDate} ${death} . 22 | ` 23 | .LIMIT(20) 24 | // .FILTER`${birth} < ${maxBirth}` 25 | .ORDER().BY(name) 26 | .execute(client.query) 27 | 28 | results.map(r => ({ 29 | person: r.person.value, 30 | name: r.name.value, 31 | birth: r.birth.value, 32 | death: r.death.value, 33 | })) 34 | -------------------------------------------------------------------------------- /docs/execute.md: -------------------------------------------------------------------------------- 1 | # Execute a SPARQL Query 2 | 3 | It is possible to run queries with a simple [sparql-http-client][client] library. 4 | It is a peer dependency so it must be installed explicitly. 5 | 6 | ``` 7 | npm i -S sparql-http-client 8 | ``` 9 | 10 | [client]: https://npm.im/sparql-http-client 11 | 12 | ## API 13 | 14 | The query is executed by invoking an `execute` method of a query builder instance. It takes a `SparqlHttpClient` as first, required parameter and an optional second which is defined as: 15 | 16 | ```typescript 17 | import { QueryOptions } from 'sparql-http-client' 18 | 19 | type SparqlExecuteOptions = QueryOptions & { 20 | base?: string 21 | } 22 | ``` 23 | 24 | In other words, the underlying HTTP fetch request can be full controlled by the `RequestInit` part and the optional `base` property can be set to be used as a `BASE` directive in the resulting SPARQL. 25 | 26 | ## DBpedia examples 27 | 28 | The queries below are adapted from DBpedias [Online Access](https://wiki.dbpedia.org/OnlineAccess) examples. 29 | 30 | ### SELECT 31 | 32 | 33 | 34 | [select](examples/execute-select.js ':include') 35 | 36 | 37 | 38 | ### CONSTRUCT/DESCRIBE 39 | 40 | 41 | 42 | [select](examples/execute-construct.js ':include') 43 | 44 | 45 | -------------------------------------------------------------------------------- /docs/expressions.md: -------------------------------------------------------------------------------- 1 | # Expressions and operators 2 | 3 | `@tpluscode/sparql-builder/expressions` exports some SPARQL function and expressions so that they are more easily incorporated into the constructed query. 4 | 5 | ## `IN` 6 | 7 | Reference: https://www.w3.org/TR/sparql11-query/#func-in 8 | 9 | 10 | 11 | ```js 12 | const { SELECT } = require('@tpluscode/sparql-builder') 13 | const { IN } = require('@tpluscode/sparql-builder/expressions') 14 | const { schema } = require('@tpluscode/rdf-ns-builders') 15 | 16 | const names = ['John', 'Jane'] 17 | 18 | SELECT.ALL.WHERE` 19 | ?person a ${schema.Person} ; ?person ${schema.name} ?name . 20 | 21 | FILTER ( 22 | ?name ${IN(...names)} 23 | ) 24 | `.build() 25 | ``` 26 | 27 | 28 | 29 | ## `VALUES` 30 | 31 | Reference: https://www.w3.org/TR/sparql11-query/#inline-data 32 | 33 | 34 | 35 | ```js 36 | const { SELECT } = require('@tpluscode/sparql-builder') 37 | const { VALUES } = require('@tpluscode/sparql-builder/expressions') 38 | const { schema, dcterms } = require('@tpluscode/rdf-ns-builders') 39 | const { namedNode } = require('@rdf-esm/data-model') 40 | 41 | // null or undefined values 42 | // will serialize as UNDEF 43 | const values = [ 44 | { book: null, title: 'SPARQL Tutorial' }, 45 | { book: namedNode('http://example.org/book') } 46 | ] 47 | 48 | SELECT.ALL.WHERE` 49 | ${VALUES(...values)} 50 | 51 | ?book ${dcterms.title} ?title ; 52 | ${schema.price} ?price . 53 | `.build() 54 | ``` 55 | 56 | 57 | 58 | 59 | ## `UNION` 60 | 61 | 62 | 63 | ```js 64 | const { sparql, SELECT } = require('@tpluscode/sparql-builder') 65 | const { UNION } = require('@tpluscode/sparql-builder/expressions') 66 | const { schema, foaf } = require('@tpluscode/rdf-ns-builders') 67 | 68 | const schemaName = sparql`?resource ${schema.name} ?name` 69 | const foafName = sparql`?resource ${foaf.name} ?name` 70 | 71 | SELECT`?name`.WHERE` 72 | ${UNION(schemaName, foafName)} 73 | `.build() 74 | ``` 75 | 76 | 77 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | @tpluscode/sparql-builder - Simple JS library to build SPARQL queries 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /docs/modifiers.md: -------------------------------------------------------------------------------- 1 | # Modifiers 2 | 3 | Reference: https://www.w3.org/TR/sparql11-query/#solutionModifiers 4 | 5 | ## LIMIT/OFFSET 6 | 7 | https://www.w3.org/TR/sparql11-query/#modOffset 8 | 9 | 10 | 11 | ```js 12 | const { CONSTRUCT } = require('@tpluscode/sparql-builder') 13 | const { variable } = require('@rdfjs/data-model') 14 | const { schema } = require('@tpluscode/rdf-ns-builders') 15 | 16 | const person = variable('person') 17 | 18 | CONSTRUCT`${person} a ${schema.Person}`.LIMIT(30).build() 19 | ``` 20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/overview.md: -------------------------------------------------------------------------------- 1 | # How it works 2 | 3 | The API takes top-level keywords of SPARQL Query/Update and exports them 4 | as builder. The intention is to closely match the syntax of SPARQL within 5 | JavaScript source code. 6 | 7 | For example, here's a slightly more complex query which uses a number of 8 | SPARQL's constructs: 9 | 10 | ```sparql 11 | BASE 12 | PREFIX foaf: 13 | PREFIX schema: 14 | 15 | SELECT DISTINCT ?person ?name 16 | FROM 17 | WHERE { 18 | ?person a foaf:Person . 19 | ?person schema:name ?name . 20 | } 21 | LIMIT 10 OFFSET 130 22 | ``` 23 | 24 | This could be written in JS as follows. 25 | 26 | 27 | 28 | ```js 29 | const { SELECT } = require('@tpluscode/sparql-builder') 30 | const { foaf, schema } = require('@tpluscode/rdf-ns-builders') 31 | const RDF = require('@rdfjs/data-model') 32 | 33 | const people = RDF.namedNode('http://example.com/people') 34 | const person = RDF.variable('person') 35 | const name = RDF.variable('name') 36 | 37 | SELECT.DISTINCT`${person} ${name}` 38 | .FROM(people) 39 | .WHERE` 40 | ${person} a ${foaf.Person} . 41 | ${person} ${schema.name} ${name} . 42 | ` 43 | .LIMIT(10).OFFSET(130).build({ 44 | base: 'http://example.com/', 45 | }) 46 | ``` 47 | 48 | 49 | -------------------------------------------------------------------------------- /docs/prologue.md: -------------------------------------------------------------------------------- 1 | # Query prologue 2 | 3 | The builder automatically handles interpolated named nodes and extracts their namespace into 4 | the list of prefixes. However, it may be desired to insert additional contents at the beginning 5 | of a query, such a comment, base URL or additional prefixes, such as modifiers that AllegroGraph 6 | uses. 7 | 8 | ## Typical usage 9 | 10 | 11 | 12 | ```js 13 | const { DESCRIBE } = require('@tpluscode/sparql-builder') 14 | const { variable, namedNode } = require('@rdf-esm/data-model') 15 | const namespace = require('@rdf-esm/namespace@0.5.0') 16 | 17 | const ns = namespace('http://example.com/') 18 | const person = variable('person') 19 | 20 | DESCRIBE`${person}` 21 | .WHERE` 22 | ?person a 23 | ` 24 | .prologue`#pragma describe.strategy cbd` 25 | .prologue`BASE ${ns()}` 26 | .build() 27 | ``` 28 | 29 | 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tpluscode/sparql-builder", 3 | "version": "3.0.1", 4 | "description": "Simple JS library to build SPARQL queries", 5 | "main": "index.js", 6 | "module": "index.js", 7 | "type": "module", 8 | "types": "index.d.ts", 9 | "sideEffects": false, 10 | "files": [ 11 | "CHANGELOG.md", 12 | "lib", 13 | "*.js", 14 | "*.d.ts", 15 | "*.d.ts.map" 16 | ], 17 | "exports": { 18 | ".": "./index.js", 19 | "./expressions": "./expressions.js" 20 | }, 21 | "scripts": { 22 | "prepare": "husky", 23 | "lint": "eslint . --ext .ts --quiet --ignore-path .gitignore --ignore-path .eslintignore", 24 | "test": "c8 --reporter=lcov mocha test/**/*.test.ts test/*.test.ts", 25 | "prepack": "npm run build", 26 | "build": "tsc -p tsconfig.json", 27 | "docs": "docsify serve docs", 28 | "release": "changeset publish" 29 | }, 30 | "dependencies": { 31 | "@rdfjs/data-model": "^2", 32 | "@rdfjs/term-set": "^2", 33 | "@rdfjs/types": "*", 34 | "@tpluscode/rdf-string": "^1.3.0", 35 | "@types/sparql-http-client": "^3.0.0" 36 | }, 37 | "peerDependencies": { 38 | "sparql-http-client": "^3.0.0" 39 | }, 40 | "devDependencies": { 41 | "@changesets/cli": "^2.16.0", 42 | "@tpluscode/eslint-config": "^0.4.5", 43 | "@types/chai": "^4.3.4", 44 | "@types/chai-as-promised": "^7.1.5", 45 | "@types/chai-snapshot-matcher": "^1.0.1", 46 | "@types/mocha": "^10.0.1", 47 | "@types/node": "^18.15.3", 48 | "@types/sinon": "^10.0.13", 49 | "@types/sinon-chai": "^3.2.9", 50 | "@types/sparqljs": "^3.1.2", 51 | "@typescript-eslint/eslint-plugin": "^7.3.1", 52 | "@typescript-eslint/parser": "^7.3.1", 53 | "@zazuko/env": "^2.0.6", 54 | "c8": "^7.13.0", 55 | "chai": "^4.5.0", 56 | "chai-as-promised": "^7.1.1", 57 | "docsify-cli": "^4.4.3", 58 | "eslint-import-resolver-typescript": "^3.5.3", 59 | "husky": "^9.0.11", 60 | "get-stream": "^9.0.1", 61 | "isomorphic-fetch": "^3.0.0", 62 | "lint-staged": "^15.2.2", 63 | "mocha": "^10.7.3", 64 | "mocha-chai-jest-snapshot": "^1.1.4", 65 | "npm-run-all": "^4.1.5", 66 | "sinon": "^17.0.1", 67 | "sinon-chai": "^3.7.0", 68 | "sparql-http-client": "^3.0.0", 69 | "sparqljs": "^3.0.1", 70 | "ts-node": "^10.9.1", 71 | "typedoc": "^0.23.27", 72 | "typescript": "^5.0.2" 73 | }, 74 | "repository": { 75 | "type": "git", 76 | "url": "git+https://github.com/tpluscode/sparql-builder.git" 77 | }, 78 | "author": "Tomasz Pluskiewicz", 79 | "license": "MIT", 80 | "bugs": { 81 | "url": "https://github.com/tpluscode/sparql-builder/issues" 82 | }, 83 | "homepage": "https://github.com/tpluscode/sparql-builder#readme", 84 | "publishConfig": { 85 | "access": "public" 86 | }, 87 | "lint-staged": { 88 | "*.{js,ts}": [ 89 | "eslint --fix --quiet" 90 | ] 91 | }, 92 | "mocha": { 93 | "loader": "ts-node/esm/transpile-only", 94 | "require": [ 95 | "test/mochaSetup.js" 96 | ] 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/expressions.ts: -------------------------------------------------------------------------------- 1 | export { IN } from './lib/expressions/in.js' 2 | export { VALUES } from './lib/expressions/values.js' 3 | export { UNION } from './lib/expressions/union.js' 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { sparql } from '@tpluscode/rdf-string' 2 | export { DELETE } from './lib/DeleteBuilder.js' 3 | export type { DeleteInsertQuery as DeleteInsert, DeleteData } from './lib/DeleteBuilder.js' 4 | export { INSERT } from './lib/InsertBuilder.js' 5 | export type { InsertQuery as Insert, InsertData } from './lib/InsertBuilder.js' 6 | export { ASK } from './lib/AskBuilder.js' 7 | export type { AskQuery as Ask } from './lib/AskBuilder.js' 8 | export { DESCRIBE } from './lib/DescribeBuilder.js' 9 | export type { DescribeQuery as Describe } from './lib/DescribeBuilder.js' 10 | export { CONSTRUCT } from './lib/ConstructBuilder.js' 11 | export type { ConstructQuery as Construct } from './lib/ConstructBuilder.js' 12 | export { SELECT } from './lib/SelectBuilder.js' 13 | export type { SelectQuery as Select } from './lib/SelectBuilder.js' 14 | export type { WithQuery } from './lib/WithBuilder.js' 15 | export { WITH } from './lib/WithBuilder.js' 16 | export { prefixes } from '@tpluscode/rdf-string' 17 | export type { SparqlTemplateResult } from '@tpluscode/rdf-string' 18 | export type { 19 | SparqlAskExecutable, SparqlGraphQueryExecutable, SparqlQueryExecutable, SparqlUpdateExecutable, 20 | } from './lib/index.js' 21 | -------------------------------------------------------------------------------- /src/lib/AskBuilder.ts: -------------------------------------------------------------------------------- 1 | import { sparql, SparqlTemplateResult, SparqlValue } from '@tpluscode/rdf-string' 2 | import { ask } from './execute.js' 3 | import LIMIT, { LimitOffsetBuilder } from './partials/LIMIT.js' 4 | import FROM, { FromBuilder } from './partials/FROM.js' 5 | import Builder, { SparqlQuery, SparqlAskExecutable } from './index.js' 6 | 7 | export type AskQuery = SparqlQuery 8 | & SparqlAskExecutable 9 | & FromBuilder 10 | & LimitOffsetBuilder & { 11 | readonly patterns: SparqlTemplateResult 12 | } 13 | 14 | export const ASK = (strings: TemplateStringsArray, ...values: SparqlValue[]): AskQuery => ({ 15 | ...Builder('ASK'), 16 | ...ask, 17 | ...LIMIT(), 18 | ...FROM(), 19 | patterns: sparql(strings, ...values), 20 | _getTemplateResult(): SparqlTemplateResult { 21 | return sparql`ASK ${this.fromClause()} { ${this.patterns} } 22 | ${this.limitOffsetClause()}` 23 | }, 24 | }) 25 | -------------------------------------------------------------------------------- /src/lib/ConstructBuilder.ts: -------------------------------------------------------------------------------- 1 | import { sparql, SparqlTemplateResult, SparqlValue } from '@tpluscode/rdf-string' 2 | import { graph } from './execute.js' 3 | import WHERE, { WhereBuilder } from './partials/WHERE.js' 4 | import LIMIT, { LimitOffsetBuilder } from './partials/LIMIT.js' 5 | import FROM, { FromBuilder } from './partials/FROM.js' 6 | import Builder, { SparqlGraphQueryExecutable, SparqlQuery } from './index.js' 7 | 8 | export type ConstructQuery = SparqlQuery 9 | & SparqlGraphQueryExecutable 10 | & WhereBuilder 11 | & FromBuilder 12 | & LimitOffsetBuilder & { 13 | readonly constructTemplate: SparqlTemplateResult 14 | readonly shorthand: boolean 15 | } 16 | 17 | interface ConstructBuilder { 18 | (strings: TemplateStringsArray, ...values: SparqlValue[]): ConstructQuery 19 | WHERE(strings: TemplateStringsArray, ...values: SparqlValue[]): ConstructQuery 20 | } 21 | 22 | const builder = (strings: TemplateStringsArray, ...values: SparqlValue[]): ConstructQuery => ({ 23 | ...Builder('CONSTRUCT'), 24 | ...graph, 25 | ...WHERE({ 26 | required: true, 27 | }), 28 | ...LIMIT(), 29 | ...FROM(), 30 | shorthand: false, 31 | constructTemplate: sparql(strings, ...values), 32 | _getTemplateResult(): SparqlTemplateResult { 33 | if (this.shorthand) { 34 | return sparql`CONSTRUCT 35 | ${this.fromClause()} 36 | WHERE { ${this.constructTemplate} } 37 | ${this.limitOffsetClause()}` 38 | } 39 | 40 | return sparql`CONSTRUCT { ${this.constructTemplate} } 41 | ${this.fromClause()} 42 | ${this.whereClause()} 43 | ${this.limitOffsetClause()}` 44 | }, 45 | }) 46 | 47 | builder.WHERE = (strings: TemplateStringsArray, ...values: SparqlValue[]) => ({ 48 | ...builder(strings, ...values), 49 | shorthand: true, 50 | }) 51 | 52 | export const CONSTRUCT = builder as ConstructBuilder 53 | -------------------------------------------------------------------------------- /src/lib/DeleteBuilder.ts: -------------------------------------------------------------------------------- 1 | import { Literal, NamedNode } from '@rdfjs/types' 2 | import { sparql, SparqlValue, SparqlTemplateResult } from '@tpluscode/rdf-string' 3 | import { update } from './execute.js' 4 | import DATA, { QuadDataBuilder } from './partials/DATA.js' 5 | import WHERE, { WhereBuilder } from './partials/WHERE.js' 6 | import INSERT, { InsertBuilder } from './partials/INSERT.js' 7 | import { concat } from './TemplateResult.js' 8 | import Builder, { SparqlQuery, SparqlUpdateExecutable } from './index.js' 9 | 10 | export type DeleteInsertQuery = InsertBuilder 11 | & WhereBuilder 12 | & SparqlQuery 13 | & SparqlUpdateExecutable & { 14 | readonly deletePatterns: SparqlTemplateResult 15 | readonly with?: NamedNode 16 | readonly using?: NamedNode[] 17 | readonly usingNamed?: NamedNode[] 18 | DELETE(strings: TemplateStringsArray, ...values: SparqlValue[]): DeleteInsertQuery 19 | } 20 | 21 | export type DeleteData = SparqlQuery & SparqlUpdateExecutable & QuadDataBuilder 22 | 23 | export const DELETE = (strings: TemplateStringsArray, ...values: SparqlValue[]): DeleteInsertQuery => ({ 24 | ...Builder('UPDATE'), 25 | ...update, 26 | ...WHERE({ 27 | required: true, 28 | }), 29 | ...INSERT(), 30 | deletePatterns: sparql(strings, ...values), 31 | DELETE(strings: TemplateStringsArray, ...values: SparqlValue[]): DeleteInsertQuery { 32 | return { 33 | ...this, 34 | deletePatterns: concat(this.deletePatterns, strings, values), 35 | } 36 | }, 37 | _getTemplateResult() { 38 | if (!this.insertPatterns) { 39 | return sparql`DELETE { ${this.deletePatterns} } ${this.whereClause()}` 40 | } 41 | 42 | return sparql`DELETE { ${this.deletePatterns} } ${this.insertClause()} ${this.whereClause()}` 43 | }, 44 | }) 45 | 46 | DELETE.DATA = (strings: TemplateStringsArray, ...values: SparqlValue[]): DeleteData => ({ 47 | ...Builder('UPDATE'), 48 | ...update, 49 | ...DATA(strings, values), 50 | _getTemplateResult() { 51 | return sparql`DELETE DATA { 52 | ${this.quadData} 53 | }` 54 | }, 55 | }) 56 | -------------------------------------------------------------------------------- /src/lib/DescribeBuilder.ts: -------------------------------------------------------------------------------- 1 | import { NamedNode, Variable } from '@rdfjs/types' 2 | import { sparql, SparqlTemplateResult, SparqlValue } from '@tpluscode/rdf-string' 3 | import { graph } from './execute.js' 4 | import WHERE, { WhereBuilder } from './partials/WHERE.js' 5 | import LIMIT, { LimitOffsetBuilder } from './partials/LIMIT.js' 6 | import FROM, { FromBuilder } from './partials/FROM.js' 7 | import ORDER, { OrderBuilder } from './partials/ORDER.js' 8 | import Builder, { SparqlGraphQueryExecutable, SparqlQuery } from './index.js' 9 | 10 | export type DescribeQuery = SparqlQuery 11 | & SparqlGraphQueryExecutable 12 | & WhereBuilder 13 | & OrderBuilder 14 | & FromBuilder 15 | & LimitOffsetBuilder & { 16 | readonly described: SparqlTemplateResult 17 | } 18 | 19 | export const DESCRIBE = (strings: TemplateStringsArray, ...values: SparqlValue[]): DescribeQuery => ({ 20 | ...Builder('CONSTRUCT'), 21 | ...graph, 22 | ...WHERE({ 23 | required: false, 24 | }), 25 | ...LIMIT(), 26 | ...FROM(), 27 | ...ORDER(), 28 | described: sparql(strings, ...values), 29 | _getTemplateResult() { 30 | return sparql`DESCRIBE ${this.described} 31 | ${this.fromClause()} 32 | ${this.whereClause()} 33 | ${this.orderClause()} 34 | ${this.limitOffsetClause()}` 35 | }, 36 | }) 37 | -------------------------------------------------------------------------------- /src/lib/InsertBuilder.ts: -------------------------------------------------------------------------------- 1 | import { BlankNode, Literal, NamedNode } from '@rdfjs/types' 2 | import { sparql, SparqlValue } from '@tpluscode/rdf-string' 3 | import { update } from './execute.js' 4 | import DATA, { QuadDataBuilder } from './partials/DATA.js' 5 | import WHERE, { WhereBuilder } from './partials/WHERE.js' 6 | import InsertBuilderPartial, { InsertBuilder } from './partials/INSERT.js' 7 | import Builder, { SparqlQuery, SparqlUpdateExecutable } from './index.js' 8 | 9 | export type InsertQuery = SparqlQuery 10 | & SparqlUpdateExecutable 11 | & InsertBuilder 12 | & WhereBuilder & { 13 | readonly with?: NamedNode 14 | readonly using?: NamedNode[] 15 | readonly usingNamed?: NamedNode[] 16 | } 17 | 18 | export type InsertData = SparqlQuery & SparqlUpdateExecutable & QuadDataBuilder 19 | 20 | export const INSERT = (strings: TemplateStringsArray, ...values: SparqlValue[]): InsertQuery => ({ 21 | ...Builder('UPDATE'), 22 | ...update, 23 | ...WHERE({ 24 | required: true, 25 | }), 26 | ...InsertBuilderPartial(sparql(strings, ...values)), 27 | _getTemplateResult() { 28 | return sparql`${this.insertClause()} ${this.whereClause()}` 29 | }, 30 | }) 31 | 32 | INSERT.DATA = (strings: TemplateStringsArray, ...values: SparqlValue[]): InsertData => ({ 33 | ...Builder('UPDATE'), 34 | ...update, 35 | ...DATA(strings, values), 36 | _getTemplateResult() { 37 | return sparql`INSERT DATA { 38 | ${this.quadData} 39 | }` 40 | }, 41 | }) 42 | -------------------------------------------------------------------------------- /src/lib/QueryError.ts: -------------------------------------------------------------------------------- 1 | export class QueryError extends Error { 2 | constructor(public query: string, cause: unknown) { 3 | super(`Error executing query: ${(cause as Error)?.message}`) 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/lib/SelectBuilder.ts: -------------------------------------------------------------------------------- 1 | import { Variable } from '@rdfjs/types' 2 | import { sparql, SparqlTemplateResult, SparqlValue } from '@tpluscode/rdf-string' 3 | import { select } from './execute.js' 4 | import WHERE, { WhereBuilder } from './partials/WHERE.js' 5 | import LIMIT, { LimitOffsetBuilder } from './partials/LIMIT.js' 6 | import ORDER, { OrderBuilder } from './partials/ORDER.js' 7 | import FROM, { FromBuilder } from './partials/FROM.js' 8 | import GROUP, { GroupBuilder } from './partials/GROUP.js' 9 | import HAVING, { HavingBuilder } from './partials/HAVING.js' 10 | import Builder, { SparqlQuery, SparqlQueryExecutable } from './index.js' 11 | 12 | export type SelectQuery = SparqlQuery 13 | & SparqlQueryExecutable 14 | & WhereBuilder 15 | & LimitOffsetBuilder 16 | & OrderBuilder 17 | & GroupBuilder 18 | & HavingBuilder 19 | & FromBuilder 20 | & { 21 | DISTINCT(): SelectQuery 22 | AND(strings: TemplateStringsArray, ...values: SparqlValue[]): SelectQuery 23 | readonly distinct: boolean 24 | readonly reduced: boolean 25 | readonly variables: SparqlTemplateResult 26 | } 27 | 28 | interface Select { 29 | (strings: TemplateStringsArray, ...values: SparqlValue[]): SelectQuery 30 | DISTINCT: (strings: TemplateStringsArray, ...values: SparqlValue[]) => SelectQuery 31 | REDUCED: (strings: TemplateStringsArray, ...values: SparqlValue[]) => SelectQuery 32 | ALL: SelectQuery 33 | } 34 | 35 | const SelectBuilder = (strings: TemplateStringsArray, ...values: SparqlValue[]): SelectQuery => ({ 36 | ...Builder('SELECT'), 37 | ...select, 38 | ...WHERE({ 39 | required: true, 40 | }), 41 | ...LIMIT(), 42 | ...ORDER(), 43 | ...GROUP(), 44 | ...HAVING(), 45 | ...FROM(), 46 | DISTINCT() { 47 | return { 48 | ...this, 49 | distinct: true, 50 | } 51 | }, 52 | AND(strings: TemplateStringsArray, ...values: SparqlValue[]) { 53 | return { 54 | ...this, 55 | variables: sparql`${this.variables}${sparql(strings, ...values)}`, 56 | } 57 | }, 58 | distinct: false, 59 | reduced: false, 60 | variables: sparql(strings, ...values), 61 | _getTemplateResult() { 62 | const modifier = this.distinct ? 'DISTINCT ' : this.reduced ? 'REDUCED ' : '' 63 | 64 | return sparql`SELECT ${modifier}${this.variables} 65 | ${this.fromClause()} 66 | ${this.whereClause()} 67 | ${this.orderClause()} 68 | ${this.groupByClause()} 69 | ${this.havingClause()} 70 | ${this.limitOffsetClause()}` 71 | }, 72 | }) 73 | 74 | SelectBuilder.DISTINCT = (strings: TemplateStringsArray, ...values: SparqlValue[]): SelectQuery => ({ 75 | ...SelectBuilder(strings, ...values), 76 | distinct: true, 77 | }) 78 | 79 | SelectBuilder.REDUCED = (strings: TemplateStringsArray, ...values: SparqlValue[]): SelectQuery => ({ 80 | ...SelectBuilder(strings, ...values), 81 | reduced: true, 82 | }) 83 | 84 | Object.defineProperty(SelectBuilder, 'ALL', { 85 | get() { 86 | return SelectBuilder`*` 87 | }, 88 | }) 89 | 90 | export const SELECT = SelectBuilder as Select 91 | -------------------------------------------------------------------------------- /src/lib/TemplateResult.ts: -------------------------------------------------------------------------------- 1 | import { sparql, SparqlTemplateResult, SparqlValue } from '@tpluscode/rdf-string' 2 | 3 | interface Options { 4 | wrapTemplate?: boolean 5 | newLineSeparator?: boolean 6 | } 7 | 8 | export function concat( 9 | current: SparqlTemplateResult | null | undefined, 10 | strings: TemplateStringsArray, 11 | values: SparqlValue[], 12 | { wrapTemplate = false, newLineSeparator = true }: Options = {}): SparqlTemplateResult { 13 | let next = sparql(strings, ...values) 14 | if (wrapTemplate) { 15 | next = sparql`( ${next} )` 16 | } 17 | 18 | if (!current) { 19 | return next 20 | } 21 | 22 | return sparql`${current}${newLineSeparator ? '\n' : ''} 23 | ${next}` 24 | } 25 | -------------------------------------------------------------------------------- /src/lib/WithBuilder.ts: -------------------------------------------------------------------------------- 1 | import { NamedNode } from '@rdfjs/types' 2 | import { sparql } from '@tpluscode/rdf-string' 3 | import RDF from '@rdfjs/data-model' 4 | import { InsertQuery } from './InsertBuilder.js' 5 | import { DeleteInsertQuery } from './DeleteBuilder.js' 6 | import { update } from './execute.js' 7 | import Builder, { SparqlQuery, SparqlUpdateExecutable } from './index.js' 8 | 9 | export type WithQuery = SparqlUpdateExecutable & SparqlQuery 10 | 11 | export const WITH = (graph: NamedNode | string, query: DeleteInsertQuery | InsertQuery): WithQuery => ({ 12 | ...Builder('UPDATE'), 13 | ...update, 14 | _getTemplateResult() { 15 | const graphNode = typeof graph === 'string' ? RDF.namedNode(graph) : graph 16 | 17 | return sparql`WITH ${graphNode}\n${query._getTemplateResult()}` 18 | }, 19 | }) 20 | -------------------------------------------------------------------------------- /src/lib/execute.ts: -------------------------------------------------------------------------------- 1 | import type { Client } from 'sparql-http-client' 2 | import { QueryError } from './QueryError.js' 3 | import { 4 | SparqlAskExecutable, 5 | SparqlExecuteOptions, 6 | SparqlGraphQueryExecutable, 7 | SparqlQuery, 8 | SparqlQueryExecutable, 9 | SparqlUpdateExecutable, 10 | } from './index.js' 11 | 12 | interface QueryAction { 13 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 14 | (...args: unknown[]): any 15 | } 16 | 17 | function buildAndRun(builder: SparqlQuery, clientExecute: TAction, { logQuery, ...requestInit }: SparqlExecuteOptions = {}): ReturnType { 18 | const query = builder.build(requestInit) 19 | logQuery?.(query) 20 | 21 | try { 22 | return clientExecute(query, requestInit) 23 | } catch (e) { 24 | throw new QueryError(query, e) 25 | } 26 | } 27 | 28 | export const update: SparqlUpdateExecutable = { 29 | execute(this: SparqlQuery, client: TClient, requestInit: SparqlExecuteOptions): ReturnType { 30 | return buildAndRun(this, client.query.update.bind(client.query), requestInit) 31 | }, 32 | } 33 | 34 | export const ask: SparqlAskExecutable = { 35 | execute(this: SparqlQuery, client: TClient, requestInit: SparqlExecuteOptions): ReturnType { 36 | return buildAndRun(this, client.query.ask.bind(client.query), requestInit) 37 | }, 38 | } 39 | 40 | export const select: SparqlQueryExecutable = { 41 | execute(this: SparqlQuery, client: TClient, requestInit: SparqlExecuteOptions): ReturnType { 42 | return buildAndRun(this, client.query.select.bind(client.query), requestInit) 43 | }, 44 | } 45 | 46 | export const graph: SparqlGraphQueryExecutable = { 47 | execute(this: SparqlQuery, client: TClient, requestInit: SparqlExecuteOptions): ReturnType { 48 | return buildAndRun(this, client.query.construct.bind(client.query), requestInit) 49 | }, 50 | } 51 | -------------------------------------------------------------------------------- /src/lib/expressions/in.ts: -------------------------------------------------------------------------------- 1 | import { sparql, SparqlTemplateResult, SparqlValue } from '@tpluscode/rdf-string' 2 | 3 | export function IN(...items: Array): SparqlTemplateResult { 4 | const list = items.reduce((previous: SparqlTemplateResult | null, next) => { 5 | if (!previous) { 6 | return sparql`${next}` 7 | } 8 | 9 | return sparql`${previous}, ${next}` 10 | }, null) 11 | 12 | return sparql`IN ( ${list || ''} )` 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/expressions/union.ts: -------------------------------------------------------------------------------- 1 | import { SparqlTemplateResult, sparql, SparqlValue } from '@tpluscode/rdf-string' 2 | 3 | export function UNION(...[first, ...rest]: Array): SparqlTemplateResult { 4 | if (rest.length === 0) { 5 | return sparql`${first}` || '' 6 | } 7 | 8 | return rest.reduce((previousValue, currentValue) => { 9 | return sparql`${previousValue} UNION { 10 | ${currentValue} 11 | }` 12 | }, sparql`{ 13 | ${first} 14 | }`) 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/expressions/values.ts: -------------------------------------------------------------------------------- 1 | import { SparqlTemplateResult, SparqlValue, sparql } from '@tpluscode/rdf-string' 2 | import RDF from '@rdfjs/data-model' 3 | 4 | type ValueMap = Record 5 | 6 | function toSingleLine(line: SparqlTemplateResult | undefined, value: SparqlValue): SparqlTemplateResult { 7 | return sparql`${line} ${value}` 8 | } 9 | 10 | export function VALUES(...values: Partial>[]): SparqlTemplateResult | string { 11 | if (values.length === 0) { 12 | return '' 13 | } 14 | 15 | const variables = [...new Set(values.map(Object.keys).flat())].map(RDF.variable) 16 | 17 | const vectors = values.reduce((previous, current: Record) => { 18 | const vector = variables.map((variable) => { 19 | const value = current[variable.value] 20 | if (value === null || typeof value === 'undefined') { 21 | return 'UNDEF' 22 | } 23 | if (typeof value === 'string') { 24 | return RDF.literal(value) 25 | } 26 | return value 27 | }) 28 | 29 | return sparql`${previous}\n(${vector.reduce(toSingleLine, sparql``)} )` 30 | }, sparql``) 31 | 32 | return sparql`VALUES (${variables.reduce(toSingleLine, sparql``)} ) 33 | {${vectors} 34 | }` 35 | } 36 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | import { QueryOptions, Client } from 'sparql-http-client' 2 | import { sparql, SparqlTemplateResult } from '@tpluscode/rdf-string' 3 | import type { NamespaceBuilder } from '@rdfjs/namespace' 4 | import type { SparqlOptions } from '@tpluscode/rdf-string' 5 | import prologue, { PrologueBuilder } from './partials/prologue.js' 6 | 7 | interface SparqlBuildOptions { 8 | base?: string 9 | prefixes?: Record 10 | } 11 | 12 | export type SparqlExecuteOptions = QueryOptions & SparqlBuildOptions & { 13 | logQuery?: (query: string) => void 14 | } 15 | 16 | export interface SparqlQuery extends PrologueBuilder { 17 | type: 'SELECT' | 'CONSTRUCT' | 'ASK' | 'UPDATE' 18 | build(options?: SparqlBuildOptions): string 19 | _getTemplateResult(): SparqlTemplateResult 20 | } 21 | 22 | export interface SparqlQueryExecutable { 23 | execute(client: TClient, requestInit?: SparqlExecuteOptions): ReturnType 24 | } 25 | 26 | export interface SparqlGraphQueryExecutable { 27 | execute(client: TClient, requestInit?: SparqlExecuteOptions): ReturnType 28 | } 29 | 30 | export interface SparqlUpdateExecutable { 31 | execute(client: TClient, requestInit?: SparqlExecuteOptions): ReturnType 32 | } 33 | 34 | export interface SparqlAskExecutable { 35 | execute(client: TClient, requestInit?: SparqlExecuteOptions): ReturnType 36 | } 37 | 38 | type TBuilder = Pick & Pick 39 | 40 | // eslint-disable-next-line no-unused-vars 41 | export default function Builder(type: SparqlQuery['type']): TBuilder & T { 42 | return { 43 | type, 44 | ...prologue(), 45 | build(this: SparqlQuery, { base, prefixes }: SparqlBuildOptions = {}): string { 46 | const queryResult = this._getTemplateResult().toString({ 47 | base, 48 | prefixes, 49 | }) 50 | 51 | if (this.prologueResult) { 52 | return `${this.prologueResult}\n\n${queryResult}` 53 | } 54 | 55 | return queryResult 56 | }, 57 | _toPartialString(this: SparqlQuery, options: SparqlOptions) { 58 | let result = this._getTemplateResult() 59 | if (this.type === 'SELECT') { 60 | result = sparql`{ ${result} }` 61 | } 62 | 63 | return result._toPartialString(options) 64 | }, 65 | } as TBuilder & T 66 | } 67 | -------------------------------------------------------------------------------- /src/lib/partials/DATA.ts: -------------------------------------------------------------------------------- 1 | import { Term } from '@rdfjs/types' 2 | import { sparql, SparqlTemplateResult, SparqlValue } from '@tpluscode/rdf-string' 3 | import { SparqlQuery } from '../index.js' 4 | import { concat } from '../TemplateResult.js' 5 | 6 | export interface QuadDataBuilder { 7 | readonly quadData: SparqlTemplateResult 8 | DATA(strings: TemplateStringsArray, ...values: SparqlValue[]): T 9 | } 10 | 11 | // eslint-disable-next-line no-use-before-define 12 | export default , TTerm extends Term = Term>(strings: TemplateStringsArray, values: SparqlValue[]): QuadDataBuilder => ({ 13 | quadData: sparql(strings, ...values), 14 | DATA(strings: TemplateStringsArray, ...values): T { 15 | return { 16 | ...this, 17 | quadData: concat(this.quadData, strings, values), 18 | } as T 19 | }, 20 | }) 21 | -------------------------------------------------------------------------------- /src/lib/partials/FROM.ts: -------------------------------------------------------------------------------- 1 | import { DefaultGraph, NamedNode } from '@rdfjs/types' 2 | import TermSet from '@rdfjs/term-set' 3 | import { sparql, SparqlTemplateResult } from '@tpluscode/rdf-string' 4 | import { SparqlQuery } from '../index.js' 5 | 6 | interface FromNamed { 7 | NAMED(graph: NamedNode): T 8 | } 9 | 10 | export interface FromBuilder { 11 | readonly defaultGraph: Set 12 | readonly fromNamed: Set 13 | FROM(defaultGraph: NamedNode | DefaultGraph): T 14 | FROM(): FromNamed 15 | fromClause(): SparqlTemplateResult 16 | } 17 | 18 | export default function > (): FromBuilder { 19 | const builder = { 20 | defaultGraph: new TermSet(), 21 | fromNamed: new TermSet(), 22 | fromClause(): SparqlTemplateResult { 23 | const clause = [...this.defaultGraph.values()].reduce((current, graph) => sparql`${current}\nFROM ${graph}`, sparql``) 24 | 25 | return [...this.fromNamed.values()].reduce((current, graph) => sparql`${current}\nFROM NAMED ${graph}`, clause) 26 | }, 27 | } 28 | 29 | return { 30 | ...builder, 31 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 32 | FROM(defaultGraph?: NamedNode | DefaultGraph): any { 33 | if (!defaultGraph) { 34 | return { 35 | NAMED: (graph: NamedNode) => { 36 | this.fromNamed.add(graph) 37 | return this 38 | }, 39 | } 40 | } 41 | 42 | if (defaultGraph.termType === 'DefaultGraph') { 43 | this.defaultGraph.clear() 44 | } else { 45 | this.defaultGraph.add(defaultGraph) 46 | } 47 | return this 48 | }, 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/lib/partials/GROUP.ts: -------------------------------------------------------------------------------- 1 | import { Variable } from '@rdfjs/types' 2 | import { sparql, SparqlTemplateResult, SparqlValue } from '@tpluscode/rdf-string' 3 | import RDF from '@rdfjs/data-model' 4 | import { SparqlQuery } from '../index.js' 5 | 6 | interface Grouping { 7 | by: SparqlTemplateResult 8 | as?: Variable 9 | } 10 | 11 | interface ThenGroupByBuilder { 12 | // eslint-disable-next-line no-use-before-define 13 | THEN: GroupByBuilder 14 | } 15 | 16 | interface BoundGroupBuilder extends ThenGroupByBuilder { 17 | AS(variable: Variable | string): T & ThenGroupByBuilder 18 | } 19 | 20 | interface GroupByBuilder { 21 | BY(strings: TemplateStringsArray, ...values: SparqlValue[]): T & BoundGroupBuilder 22 | BY(variable: string | Variable, ...values: SparqlValue[]): T & BoundGroupBuilder 23 | } 24 | 25 | export interface GroupBuilder { 26 | groupings: Grouping[] 27 | groupByClause(): SparqlTemplateResult 28 | GROUP(): GroupByBuilder 29 | } 30 | 31 | function isTemplateArray(arg: TemplateStringsArray | unknown): arg is TemplateStringsArray { 32 | return Array.isArray(arg) 33 | } 34 | 35 | function addGrouping>(builder: GroupBuilder) { 36 | return (strings: TemplateStringsArray | string | Variable, ...values: SparqlValue[]): T & BoundGroupBuilder => { 37 | let by: SparqlTemplateResult 38 | if (isTemplateArray(strings)) { 39 | by = sparql(strings, ...values) 40 | } else if (typeof strings === 'string') { 41 | by = sparql`${RDF.variable(strings)}` 42 | } else { 43 | by = sparql`${strings}` 44 | } 45 | 46 | const grouping: Grouping = { by } 47 | const childBuilder = { 48 | ...builder, 49 | groupings: [...builder.groupings, grouping], 50 | } as T & GroupBuilder 51 | const thenBuilder: T & ThenGroupByBuilder = { 52 | ...childBuilder, 53 | THEN: { 54 | BY: addGrouping(childBuilder), 55 | }, 56 | } 57 | 58 | return { 59 | ...thenBuilder, 60 | AS(variable: string | Variable) { 61 | if (typeof variable === 'string') { 62 | grouping.as = RDF.variable(variable) 63 | } else { 64 | grouping.as = variable 65 | } 66 | 67 | return thenBuilder 68 | }, 69 | } as T & BoundGroupBuilder 70 | } 71 | } 72 | 73 | export default >(): GroupBuilder => ({ 74 | groupings: [], 75 | GROUP(): GroupByBuilder { 76 | return { 77 | BY: addGrouping(this), 78 | } 79 | }, 80 | groupByClause() { 81 | if (this.groupings.some(Boolean)) { 82 | return this.groupings.reduce((result, grouping) => { 83 | if (grouping.as) { 84 | return sparql`${result}\n (${grouping.by} as ${grouping.as})` 85 | } 86 | 87 | return sparql`${result}\n (${grouping.by})` 88 | }, 89 | sparql`GROUP BY`) 90 | } 91 | 92 | return sparql`` 93 | }, 94 | }) 95 | -------------------------------------------------------------------------------- /src/lib/partials/HAVING.ts: -------------------------------------------------------------------------------- 1 | import { sparql, SparqlTemplateResult, SparqlValue } from '@tpluscode/rdf-string' 2 | import { SparqlQuery } from '../index.js' 3 | import { concat } from '../TemplateResult.js' 4 | 5 | export interface HavingBuilder { 6 | readonly havings: SparqlTemplateResult | null 7 | havingClause(): SparqlTemplateResult 8 | HAVING(strings: TemplateStringsArray, ...values: SparqlValue[]): T 9 | } 10 | 11 | export default >(): HavingBuilder => ({ 12 | havings: null, 13 | havingClause() { 14 | if (this.havings) { 15 | return sparql`HAVING ${this.havings}` 16 | } 17 | 18 | return sparql`` 19 | }, 20 | HAVING(strings: TemplateStringsArray, ...values: SparqlValue[]): T { 21 | return { 22 | ...this, 23 | havings: concat(this.havings, strings, values, { wrapTemplate: true }), 24 | } as T 25 | }, 26 | }) 27 | -------------------------------------------------------------------------------- /src/lib/partials/INSERT.ts: -------------------------------------------------------------------------------- 1 | import { SparqlTemplateResult, sparql, SparqlValue } from '@tpluscode/rdf-string' 2 | import { SparqlQuery } from '../index.js' 3 | import { concat } from '../TemplateResult.js' 4 | 5 | export interface InsertBuilder { 6 | readonly insertPatterns: SparqlTemplateResult | null 7 | insertClause(): SparqlTemplateResult 8 | INSERT(strings: TemplateStringsArray, ...values: SparqlValue[]): T 9 | } 10 | 11 | export default >(insertPatterns: SparqlTemplateResult | null = null): InsertBuilder => ({ 12 | insertPatterns, 13 | insertClause(): SparqlTemplateResult { 14 | return sparql`INSERT{ 15 | ${this.insertPatterns} 16 | }` 17 | }, 18 | INSERT(strings: TemplateStringsArray, ...values: SparqlValue[]) { 19 | return { 20 | ...this, 21 | insertPatterns: concat(this.insertPatterns, strings, values), 22 | } as T 23 | }, 24 | }) 25 | -------------------------------------------------------------------------------- /src/lib/partials/LIMIT.ts: -------------------------------------------------------------------------------- 1 | import { SparqlQuery } from '../index.js' 2 | 3 | export interface LimitOffsetBuilder { 4 | offset: number | null 5 | limit: number | null 6 | LIMIT(limit: number): T 7 | OFFSET(offset: number): T 8 | limitOffsetClause(): string 9 | } 10 | 11 | export default >(): LimitOffsetBuilder => ({ 12 | limit: null, 13 | offset: null, 14 | limitOffsetClause() { 15 | let clause = '' 16 | if (this.limit != null) { 17 | clause += `LIMIT ${this.limit} ` 18 | } 19 | 20 | if (this.offset != null) { 21 | clause += `OFFSET ${this.offset}` 22 | } 23 | 24 | return clause 25 | }, 26 | LIMIT(limit: number): T { 27 | return { 28 | ...this, 29 | limit, 30 | } as T 31 | }, 32 | OFFSET(offset: number): T { 33 | return { 34 | ...this, 35 | offset, 36 | } as T 37 | }, 38 | }) 39 | -------------------------------------------------------------------------------- /src/lib/partials/ORDER.ts: -------------------------------------------------------------------------------- 1 | import { Variable } from '@rdfjs/types' 2 | import { sparql, SparqlTemplateResult } from '@tpluscode/rdf-string' 3 | import { SparqlQuery } from '../index.js' 4 | 5 | interface OrderCondition { 6 | variable: Variable 7 | desc: boolean 8 | } 9 | 10 | interface ThenOrderByBuilder { 11 | // eslint-disable-next-line no-use-before-define 12 | THEN: OrderByBuilder 13 | } 14 | 15 | interface OrderByBuilder { 16 | BY(variable: Variable, desc?: boolean): T & ThenOrderByBuilder 17 | } 18 | 19 | export interface OrderBuilder { 20 | orderConditions: OrderCondition[] 21 | orderClause(): SparqlTemplateResult 22 | ORDER(): OrderByBuilder 23 | } 24 | 25 | function addOrder>(builder: OrderBuilder) { 26 | return (variable: Variable, desc: boolean): T & ThenOrderByBuilder => { 27 | const thenBuilder = { 28 | ...builder, 29 | orderConditions: [ 30 | ...builder.orderConditions, { 31 | variable, 32 | desc, 33 | }], 34 | } 35 | 36 | return { 37 | ...thenBuilder, 38 | THEN: { 39 | BY: addOrder(thenBuilder), 40 | }, 41 | } as T & ThenOrderByBuilder 42 | } 43 | } 44 | 45 | export default >(): OrderBuilder => ({ 46 | orderConditions: [], 47 | ORDER(): OrderByBuilder { 48 | return { 49 | BY: addOrder(this), 50 | } 51 | }, 52 | orderClause() { 53 | if (this.orderConditions.some(Boolean)) { 54 | return this.orderConditions.reduce((result, condition) => { 55 | if (condition.desc) { 56 | return sparql`${result} desc(${condition.variable})` 57 | } 58 | 59 | return sparql`${result} ${condition.variable}` 60 | }, 61 | sparql`ORDER BY`) 62 | } 63 | 64 | return sparql`` 65 | }, 66 | }) 67 | -------------------------------------------------------------------------------- /src/lib/partials/WHERE.ts: -------------------------------------------------------------------------------- 1 | import { sparql, SparqlTemplateResult, SparqlValue } from '@tpluscode/rdf-string' 2 | import { SparqlQuery } from '../index.js' 3 | import { concat } from '../TemplateResult.js' 4 | 5 | export interface WhereBuilder { 6 | readonly patterns: SparqlTemplateResult | null 7 | whereClause(): SparqlTemplateResult 8 | WHERE(strings: TemplateStringsArray, ...values: SparqlValue[]): T 9 | } 10 | 11 | export default >({ required }: { required: boolean }): WhereBuilder => ({ 12 | patterns: null, 13 | whereClause() { 14 | if (this.patterns) { 15 | return sparql`WHERE { 16 | ${this.patterns} 17 | }` 18 | } 19 | 20 | if (required) { 21 | return sparql`WHERE {}` 22 | } 23 | 24 | return sparql`` 25 | }, 26 | WHERE(strings: TemplateStringsArray, ...values: SparqlValue[]): T { 27 | return { 28 | ...this, 29 | patterns: concat(this.patterns, strings, values), 30 | } as T 31 | }, 32 | }) 33 | -------------------------------------------------------------------------------- /src/lib/partials/prologue.ts: -------------------------------------------------------------------------------- 1 | import { SparqlTemplateResult, SparqlValue } from '@tpluscode/rdf-string' 2 | import { SparqlQuery } from '../index.js' 3 | import { concat } from '../TemplateResult.js' 4 | 5 | export interface PrologueBuilder { 6 | readonly prologueResult: SparqlTemplateResult | null 7 | prologue(strings: TemplateStringsArray, ...values: SparqlValue[]): this 8 | } 9 | 10 | export default (): PrologueBuilder => ({ 11 | prologueResult: null, 12 | prologue(strings: TemplateStringsArray, ...values): T { 13 | return { 14 | ...this, 15 | prologueResult: concat(this.prologueResult, strings, values, { newLineSeparator: false }), 16 | } as T 17 | }, 18 | }) 19 | -------------------------------------------------------------------------------- /test/ASK.test.ts: -------------------------------------------------------------------------------- 1 | import RDF from '@zazuko/env' 2 | import chai, { expect } from 'chai' 3 | import { jestSnapshotPlugin } from 'mocha-chai-jest-snapshot' 4 | import { ASK } from '../src/index.js' 5 | import { sparqlClient } from './_mocks.js' 6 | import './sparql.js' 7 | 8 | describe('ASK', () => { 9 | chai.use(jestSnapshotPlugin()) 10 | 11 | it('creates expected query', () => { 12 | // given 13 | const expected = `ASK { 14 | ?s ?p ?o . 15 | ?x ?y ?z . 16 | }` 17 | 18 | // when 19 | const query = ASK`?s ?p ?o . ?x ?y ?z .`.build() 20 | 21 | // then 22 | expect(query).to.be.query(expected) 23 | }) 24 | 25 | it('executes as ask', async () => { 26 | // given 27 | const client = sparqlClient() 28 | 29 | // when 30 | await ASK``.execute(client) 31 | 32 | // then 33 | expect(client.query.ask).to.have.been.called 34 | }) 35 | 36 | it('can have additional prologue', function () { 37 | // given 38 | const base = RDF.namedNode('http://foo.bar/baz') 39 | 40 | // when 41 | const query = ASK`?s ?p ?o .` 42 | .prologue`#pragma join.hash off` 43 | .prologue`BASE ${base}` 44 | .build() 45 | 46 | // then 47 | expect(query).toMatchSnapshot() 48 | }) 49 | 50 | it('supports LIMIT/OFFSET', () => { 51 | // given 52 | const expected = `ASK { 53 | ?s ?p ?o . 54 | } LIMIT 10 OFFSET 20` 55 | 56 | // when 57 | const query = ASK`?s ?p ?o .`.LIMIT(10).OFFSET(20).build() 58 | 59 | // then 60 | expect(query).to.be.query(expected) 61 | }) 62 | 63 | it('supports FROM (NAMED)', () => { 64 | // given 65 | const expected = `ASK 66 | FROM 67 | FROM NAMED 68 | { 69 | ?s ?p ?o . 70 | }` 71 | 72 | // when 73 | const query = ASK`?s ?p ?o .` 74 | .FROM(RDF.namedNode('http://example.com/foo')) 75 | .FROM().NAMED(RDF.namedNode('http://example.com/bar')) 76 | .build() 77 | 78 | // then 79 | expect(query).to.be.query(expected) 80 | }) 81 | 82 | it('can be constructed with a base', () => { 83 | // given 84 | const ns = RDF.namespace('http://example.com/') 85 | const expected = `BASE 86 | 87 | ASK { 88 | a 89 | }` 90 | 91 | // when 92 | const query = ASK`${ns.person} a ${ns.Person} .`.build({ 93 | base: 'http://example.com/', 94 | }) 95 | 96 | // then 97 | expect(query).to.be.query(expected) 98 | }) 99 | }) 100 | -------------------------------------------------------------------------------- /test/CONSTRUCT.test.ts: -------------------------------------------------------------------------------- 1 | import RDF from '@zazuko/env' 2 | import chai, { expect } from 'chai' 3 | import { jestSnapshotPlugin } from 'mocha-chai-jest-snapshot' 4 | import { CONSTRUCT, SELECT } from '../src/index.js' 5 | import { sparqlClient } from './_mocks.js' 6 | import './sparql.js' 7 | 8 | const { dbo, foaf, schema, skos } = RDF.ns 9 | 10 | describe('CONSTRUCT', () => { 11 | chai.use(jestSnapshotPlugin()) 12 | 13 | it('executes as construct', () => { 14 | // given 15 | const client = sparqlClient() 16 | 17 | // when 18 | CONSTRUCT``.execute(client) 19 | 20 | // then 21 | expect(client.query.construct).to.have.been.called 22 | }) 23 | 24 | it('can have additional prologue', function () { 25 | // given 26 | const base = RDF.namedNode('http://foo.bar/baz') 27 | 28 | // when 29 | const query = CONSTRUCT`?s ?p ?o .` 30 | .prologue`#pragma join.hash off` 31 | .prologue`BASE ${base}` 32 | .build() 33 | 34 | // then 35 | expect(query).toMatchSnapshot() 36 | }) 37 | 38 | it('generates empty WHERE clause by default', () => { 39 | // given 40 | const expected = `PREFIX schema: 41 | CONSTRUCT { a schema:Person } WHERE {}` 42 | 43 | // when 44 | const actual = CONSTRUCT` a ${schema.Person}`.build() 45 | 46 | // then 47 | expect(actual).to.be.query(expected) 48 | }) 49 | 50 | it('support shorthand syntax', () => { 51 | // given 52 | const expected = `PREFIX schema: 53 | CONSTRUCT WHERE { ?person a schema:Person }` 54 | 55 | // when 56 | const actual = CONSTRUCT.WHERE`?person a ${schema.Person}`.build() 57 | 58 | // then 59 | expect(actual).to.be.query(expected) 60 | }) 61 | 62 | it('correctly interpolate shorthand syntax', () => { 63 | // given 64 | const root = RDF.namedNode('https://example.com/') 65 | const expected = `PREFIX skos: <${skos().value}> 66 | CONSTRUCT WHERE { 67 | skos:narrower ?narrower . 68 | ?narrower skos:prefLabel ?label . 69 | }` 70 | 71 | // when 72 | const actual = CONSTRUCT.WHERE` 73 | ${root} ${skos.narrower} ?narrower . 74 | ?narrower ${skos.prefLabel} ?label . 75 | `.build() 76 | 77 | // then 78 | expect(actual).to.be.query(expected) 79 | }) 80 | 81 | it('support FROM in shorthand syntax', () => { 82 | // given 83 | const expected = `PREFIX schema: 84 | CONSTRUCT FROM WHERE { ?person a schema:Person }` 85 | 86 | // when 87 | const actual = CONSTRUCT.WHERE`?person a ${schema.Person}` 88 | .FROM(RDF.namedNode('http://example.com/graph')).build() 89 | 90 | // then 91 | expect(actual).to.be.query(expected) 92 | }) 93 | 94 | it('supports LIMIT/OFFSET', () => { 95 | // given 96 | const expected = `PREFIX schema: 97 | CONSTRUCT { a schema:Person } WHERE {} 98 | LIMIT 5 OFFSET 305` 99 | 100 | // when 101 | const actual = CONSTRUCT` a ${schema.Person}`.LIMIT(5).OFFSET(305).build() 102 | 103 | // then 104 | expect(actual).to.be.query(expected) 105 | }) 106 | 107 | it('can be constructed with a base', () => { 108 | // given 109 | const ns = RDF.namespace('http://example.com/') 110 | const expected = `BASE 111 | 112 | CONSTRUCT { 113 | a 114 | } WHERE {}` 115 | 116 | // when 117 | const actual = CONSTRUCT`${ns.person} a ${ns.Person}`.build({ 118 | base: 'http://example.com/', 119 | }) 120 | 121 | // then 122 | expect(actual).to.be.query(expected) 123 | }) 124 | 125 | it('can be combined with another query to create a subquery', () => { 126 | // given 127 | const person = RDF.variable('person') 128 | const selectPeopleBornInBerlin = SELECT`${person}` 129 | .WHERE`${person} ${dbo.birthPlace} ` 130 | .LIMIT(100) 131 | const expected = `PREFIX dbo: 132 | PREFIX foaf: 133 | 134 | CONSTRUCT { ?person ?p ?o } 135 | WHERE { 136 | 137 | VALUES ?p { dbo:birthDate dbo:deathDate foaf:name } 138 | ?person ?p ?o . 139 | 140 | { 141 | SELECT ?person 142 | 143 | WHERE { 144 | ?person dbo:birthPlace 145 | } 146 | LIMIT 100 147 | } 148 | }` 149 | 150 | // when 151 | const construct = CONSTRUCT`${person} ?p ?o` 152 | .WHERE` 153 | VALUES ?p { ${dbo.birthDate} ${dbo.deathDate} ${foaf.name} } 154 | ${person} ?p ?o . 155 | 156 | ${selectPeopleBornInBerlin} 157 | `.build() 158 | 159 | // then 160 | expect(construct).to.be.query(expected) 161 | }) 162 | 163 | it('adds FROM when default graph set', () => { 164 | // given 165 | const expected = 'CONSTRUCT { ?s ?p ?o } FROM WHERE { ?s ?p ?o }' 166 | 167 | // when 168 | const actual = CONSTRUCT`?s ?p ?o`.FROM(RDF.namedNode('urn:foo:bar')).WHERE`?s ?p ?o`.build() 169 | 170 | // then 171 | expect(actual).to.be.query(expected) 172 | }) 173 | 174 | it('supports multiple FROM NAMED', () => { 175 | // given 176 | const expected = `CONSTRUCT { ?s ?p ?o } 177 | FROM NAMED 178 | FROM NAMED 179 | WHERE { ?s ?p ?o }` 180 | 181 | // when 182 | const actual = CONSTRUCT`?s ?p ?o` 183 | .FROM().NAMED(RDF.namedNode('urn:foo:bar')) 184 | .FROM().NAMED(RDF.namedNode('urn:foo:baz')) 185 | .WHERE`?s ?p ?o`.build() 186 | 187 | // then 188 | expect(actual).to.be.query(expected) 189 | }) 190 | 191 | it('supports multiple FROM', () => { 192 | // given 193 | const expected = `CONSTRUCT { ?s ?p ?o } 194 | FROM 195 | FROM 196 | WHERE { ?s ?p ?o }` 197 | 198 | // when 199 | const actual = CONSTRUCT`?s ?p ?o` 200 | .FROM(RDF.namedNode('urn:foo:bar')) 201 | .FROM(RDF.namedNode('urn:foo:baz')) 202 | .WHERE`?s ?p ?o`.build() 203 | 204 | // then 205 | expect(actual).to.be.query(expected) 206 | }) 207 | 208 | it('allows mixing FROM and FROM NAMED', () => { 209 | // given 210 | const expected = `CONSTRUCT { ?s ?p ?o } 211 | FROM 212 | FROM NAMED 213 | WHERE { ?s ?p ?o }` 214 | 215 | // when 216 | const actual = CONSTRUCT`?s ?p ?o` 217 | .FROM(RDF.namedNode('urn:foo:bar')) 218 | .FROM().NAMED(RDF.namedNode('urn:foo:baz')) 219 | .WHERE`?s ?p ?o`.build() 220 | 221 | // then 222 | expect(actual).to.be.query(expected) 223 | }) 224 | }) 225 | -------------------------------------------------------------------------------- /test/DELETE.test.ts: -------------------------------------------------------------------------------- 1 | import RDF from '@zazuko/env' 2 | import chai, { expect } from 'chai' 3 | import { jestSnapshotPlugin } from 'mocha-chai-jest-snapshot' 4 | import { DELETE } from '../src/index.js' 5 | import { sparqlClient } from './_mocks.js' 6 | import './sparql.js' 7 | 8 | const { foaf, owl, schema } = RDF.ns 9 | 10 | describe('DELETE', () => { 11 | chai.use(jestSnapshotPlugin()) 12 | 13 | it('adds an empty WHERE if no pattern provided', () => { 14 | // given 15 | const expected = `DELETE { 16 | ?s ?p ?o . 17 | } WHERE {}` 18 | 19 | // when 20 | const query = DELETE`?s ?p ?o .`.build() 21 | 22 | // then 23 | expect(query).to.be.query(expected) 24 | }) 25 | 26 | it('can have additional prologue', function () { 27 | // given 28 | const base = RDF.namedNode('http://foo.bar/baz') 29 | 30 | // when 31 | const query = DELETE` ${owl.sameAs} .` 32 | .prologue`#pragma join.hash off` 33 | .prologue`BASE ${base}` 34 | .build() 35 | 36 | // then 37 | expect(query).toMatchSnapshot() 38 | }) 39 | 40 | it('combines multiple DELETE calls', () => { 41 | // given 42 | const expected = `PREFIX owl: 43 | 44 | DELETE { 45 | owl:sameAs . 46 | owl:sameAs . 47 | } WHERE {}` 48 | 49 | // when 50 | const query = DELETE` ${owl.sameAs} .` 51 | .DELETE` ${owl.sameAs} .` 52 | .build() 53 | 54 | // then 55 | expect(query).to.be.query(expected) 56 | }) 57 | 58 | it('skips empty INSERT clause', () => { 59 | // when 60 | const query = DELETE` ${owl.sameAs} .` 61 | .build() 62 | 63 | // then 64 | expect(query).not.to.include('INSERT') 65 | }) 66 | 67 | it('has a WHERE method', () => { 68 | // given 69 | const expected = `PREFIX schema: 70 | 71 | DELETE { 72 | ?s ?p ?o . 73 | } WHERE { 74 | ?s a schema:Person ; ?p ?o 75 | }` 76 | 77 | // when 78 | const query = DELETE`?s ?p ?o .` 79 | .WHERE`?s a ${schema.Person} ; ?p ?o` 80 | .build() 81 | 82 | // then 83 | expect(query).to.be.query(expected) 84 | }) 85 | 86 | it('complete DELETE/INSERT/WHERE', () => { 87 | // given 88 | const expected = ` 89 | PREFIX foaf: 90 | PREFIX schema: 91 | 92 | DELETE { 93 | ?s a foaf:Person . 94 | } INSERT { 95 | ?s a schema:Person . 96 | } WHERE { 97 | ?s a foaf:Person . 98 | }` 99 | 100 | // when 101 | const query = DELETE`?s a ${foaf.Person}` 102 | .INSERT`?s a ${schema.Person}` 103 | .WHERE`?s a ${foaf.Person}` 104 | .build() 105 | 106 | // then 107 | expect(query).to.be.query(expected) 108 | }) 109 | 110 | it('executes as update', async () => { 111 | // given 112 | const client = sparqlClient() 113 | 114 | // when 115 | await DELETE``.execute(client) 116 | 117 | // then 118 | expect(client.query.update).to.have.been.called 119 | }) 120 | }) 121 | -------------------------------------------------------------------------------- /test/DELETE_DATA.test.ts: -------------------------------------------------------------------------------- 1 | import RDF from '@zazuko/env' 2 | import chai, { expect } from 'chai' 3 | import { jestSnapshotPlugin } from 'mocha-chai-jest-snapshot' 4 | import { DELETE } from '../src/index.js' 5 | import { sparqlClient } from './_mocks.js' 6 | import './sparql.js' 7 | 8 | describe('DELETE DATA', () => { 9 | chai.use(jestSnapshotPlugin()) 10 | 11 | it('builds correct query', () => { 12 | // given 13 | const expected = `PREFIX owl: 14 | DELETE DATA { owl:sameAs . }` 15 | 16 | // when 17 | const actual = DELETE.DATA` ${RDF.ns.owl.sameAs} `.build() 18 | 19 | // then 20 | expect(actual).to.be.query(expected) 21 | }) 22 | 23 | it('executes as update', async () => { 24 | // given 25 | const client = sparqlClient() 26 | 27 | // when 28 | await DELETE.DATA` owl:sameAs `.execute(client) 29 | 30 | // then 31 | expect(client.query.update).to.have.been.called 32 | }) 33 | 34 | it('can have additional prologue', function () { 35 | // given 36 | const base = RDF.namedNode('http://foo.bar/baz') 37 | 38 | // when 39 | const query = DELETE.DATA` ${RDF.ns.owl.sameAs} .` 40 | .prologue`#pragma join.hash off` 41 | .prologue`BASE ${base}` 42 | .build() 43 | 44 | // then 45 | expect(query).toMatchSnapshot() 46 | }) 47 | 48 | it('can delete triples', function () { 49 | // given 50 | const data = RDF.quad( 51 | RDF.namedNode('http://example.com/bar'), 52 | RDF.ns.owl.sameAs, 53 | RDF.namedNode('http://example.com/bar'), 54 | ) 55 | 56 | // when 57 | const query = DELETE.DATA`${data}`.build() 58 | 59 | // then 60 | expect(query).toMatchSnapshot() 61 | }) 62 | 63 | it('can delete quads', function () { 64 | // given 65 | const data = RDF.quad( 66 | RDF.namedNode('http://example.com/bar'), 67 | RDF.ns.owl.sameAs, 68 | RDF.namedNode('http://example.com/bar'), 69 | RDF.namedNode('http://example.com/G'), 70 | ) 71 | 72 | // when 73 | const query = DELETE.DATA`${data}`.build() 74 | 75 | // then 76 | expect(query).toMatchSnapshot() 77 | }) 78 | 79 | it('can delete dataset', function () { 80 | // given 81 | const data = RDF.dataset([RDF.quad( 82 | RDF.namedNode('http://example.com/bar'), 83 | RDF.ns.owl.sameAs, 84 | RDF.namedNode('http://example.com/bar'), 85 | )]) 86 | 87 | // when 88 | const query = DELETE.DATA`${data}`.build() 89 | 90 | // then 91 | expect(query).toMatchSnapshot() 92 | }) 93 | 94 | it('can chain multiple quad data calls', () => { 95 | // given 96 | const expected = `PREFIX owl: 97 | DELETE DATA { 98 | owl:sameAs . 99 | owl:sameAs . 100 | }` 101 | 102 | // when 103 | const actual = DELETE 104 | .DATA` ${RDF.ns.owl.sameAs} .` 105 | .DATA` ${RDF.ns.owl.sameAs} .` 106 | .build() 107 | 108 | // then 109 | expect(actual).to.be.query(expected) 110 | }) 111 | }) 112 | -------------------------------------------------------------------------------- /test/DESCRIBE.test.ts: -------------------------------------------------------------------------------- 1 | import RDF from '@zazuko/env' 2 | import chai, { expect } from 'chai' 3 | import { jestSnapshotPlugin } from 'mocha-chai-jest-snapshot' 4 | import { DESCRIBE } from '../src/index.js' 5 | import { sparqlClient } from './_mocks.js' 6 | import './sparql.js' 7 | 8 | describe('DESCRIBE', () => { 9 | chai.use(jestSnapshotPlugin()) 10 | 11 | it('executes a construct', async () => { 12 | // given 13 | const client = sparqlClient() 14 | 15 | // when 16 | await DESCRIBE``.execute(client) 17 | 18 | // then 19 | expect(client.query.construct).to.have.been.called 20 | }) 21 | 22 | it('builds a DESCRIBE without WHERE', () => { 23 | // given 24 | const expected = 'DESCRIBE ' 25 | 26 | // when 27 | const actual = DESCRIBE`${RDF.namedNode('urn:foo:bar')}`.build() 28 | 29 | // then 30 | expect(actual).to.be.query(expected) 31 | }) 32 | 33 | it('can have additional prologue', function () { 34 | // given 35 | const base = RDF.namedNode('http://foo.bar/baz') 36 | 37 | // when 38 | const query = DESCRIBE`?foo` 39 | .prologue`#pragma join.hash off` 40 | .prologue`BASE ${base}` 41 | .WHERE`?foo a ?bar` 42 | .build() 43 | 44 | // then 45 | expect(query).toMatchSnapshot() 46 | }) 47 | 48 | it('supports LIMIT/OFFSET', () => { 49 | // given 50 | const expected = 'DESCRIBE ?foo LIMIT 100 OFFSET 200' 51 | 52 | // when 53 | const actual = DESCRIBE`${RDF.variable('foo')}`.LIMIT(100).OFFSET(200).build() 54 | 55 | // then 56 | expect(actual).to.be.query(expected) 57 | }) 58 | 59 | it('supports ORDER BY', () => { 60 | // given 61 | const expected = 'DESCRIBE ?foo WHERE { ?foo a ?bar } ORDER BY ?bar LIMIT 100 OFFSET 200' 62 | const foo = RDF.variable('foo') 63 | const bar = RDF.variable('bar') 64 | 65 | // when 66 | const actual = DESCRIBE`${foo}` 67 | .WHERE`${foo} a ${bar}` 68 | .ORDER().BY(bar) 69 | .LIMIT(100) 70 | .OFFSET(200) 71 | .build() 72 | 73 | // then 74 | expect(actual).to.be.query(expected) 75 | }) 76 | 77 | it('supports FROM (NAMED)', () => { 78 | // given 79 | const expected = `DESCRIBE ?foo 80 | FROM 81 | FROM NAMED ` 82 | 83 | // when 84 | const actual = DESCRIBE`${RDF.variable('foo')}` 85 | .FROM(RDF.namedNode('http://example.com/foo')) 86 | .FROM().NAMED(RDF.namedNode('http://example.com/bar')).build() 87 | 88 | // then 89 | expect(actual).to.be.query(expected) 90 | }) 91 | 92 | it('can be constructed with a base', () => { 93 | // given 94 | const ns = RDF.namespace('http://example.com/') 95 | const expected = `BASE 96 | 97 | DESCRIBE ` 98 | 99 | // when 100 | const actual = DESCRIBE`${ns.person}`.build({ 101 | base: 'http://example.com/', 102 | }) 103 | 104 | // then 105 | expect(actual).to.be.query(expected) 106 | }) 107 | }) 108 | -------------------------------------------------------------------------------- /test/INSERT.test.ts: -------------------------------------------------------------------------------- 1 | import RDF from '@zazuko/env' 2 | import chai, { expect } from 'chai' 3 | import { jestSnapshotPlugin } from 'mocha-chai-jest-snapshot' 4 | import { INSERT } from '../src/index.js' 5 | import { sparqlClient } from './_mocks.js' 6 | import './sparql.js' 7 | 8 | const { owl, schema } = RDF.ns 9 | 10 | describe('INSERT', () => { 11 | chai.use(jestSnapshotPlugin()) 12 | 13 | it('adds an empty WHERE if no pattern provided', () => { 14 | // given 15 | const expected = `INSERT { 16 | ?s ?p ?o . 17 | } WHERE {}` 18 | 19 | // when 20 | const query = INSERT`?s ?p ?o .`.build() 21 | 22 | // then 23 | expect(query).to.be.query(expected) 24 | }) 25 | 26 | it('combines multiple INSERT calls', () => { 27 | // given 28 | const expected = `PREFIX owl: 29 | 30 | INSERT { 31 | owl:sameAs . 32 | owl:sameAs . 33 | } WHERE {}` 34 | 35 | // when 36 | const query = INSERT` ${owl.sameAs} .` 37 | .INSERT` ${owl.sameAs} .` 38 | .build() 39 | 40 | // then 41 | expect(query).to.be.query(expected) 42 | }) 43 | 44 | it('can have additional prologue', function () { 45 | // given 46 | const base = RDF.namedNode('http://foo.bar/baz') 47 | 48 | // when 49 | const query = INSERT` ${owl.sameAs} .` 50 | .prologue`#pragma join.hash off` 51 | .prologue`BASE ${base}` 52 | .build() 53 | 54 | // then 55 | expect(query).toMatchSnapshot() 56 | }) 57 | 58 | it('has a WHERE method', () => { 59 | // given 60 | const expected = `PREFIX schema: 61 | 62 | INSERT { 63 | ?s ?p ?o . 64 | } WHERE { 65 | ?s a schema:Person ; ?p ?o 66 | }` 67 | 68 | // when 69 | const query = INSERT`?s ?p ?o .` 70 | .WHERE`?s a ${schema.Person} ; ?p ?o` 71 | .build() 72 | 73 | // then 74 | expect(query).to.be.query(expected) 75 | }) 76 | 77 | it('executes as update', async () => { 78 | // given 79 | const client = sparqlClient() 80 | 81 | // when 82 | await INSERT``.execute(client) 83 | 84 | // then 85 | expect(client.query.update).to.have.been.called 86 | }) 87 | }) 88 | -------------------------------------------------------------------------------- /test/INSERT_DATA.test.ts: -------------------------------------------------------------------------------- 1 | import RDF from '@zazuko/env' 2 | import chai, { expect } from 'chai' 3 | import { jestSnapshotPlugin } from 'mocha-chai-jest-snapshot' 4 | import { INSERT } from '../src/index.js' 5 | import { sparqlClient } from './_mocks.js' 6 | import './sparql.js' 7 | 8 | describe('INSERT DATA', () => { 9 | chai.use(jestSnapshotPlugin()) 10 | 11 | it('builds correct query', () => { 12 | // given 13 | const expected = `PREFIX owl: 14 | INSERT DATA { owl:sameAs . }` 15 | 16 | // when 17 | const actual = INSERT.DATA` ${RDF.ns.owl.sameAs} `.build() 18 | 19 | // then 20 | expect(actual).to.be.query(expected) 21 | }) 22 | 23 | it('executes as update', async () => { 24 | // given 25 | const client = sparqlClient() 26 | 27 | // when 28 | await INSERT.DATA` owl:sameAs `.execute(client) 29 | 30 | // then 31 | expect(client.query.update).to.have.been.called 32 | }) 33 | 34 | it('can have additional prologue', function () { 35 | // given 36 | const base = RDF.namedNode('http://foo.bar/baz') 37 | 38 | // when 39 | const query = INSERT.DATA` ${RDF.ns.owl.sameAs} .` 40 | .prologue`#pragma join.hash off` 41 | .prologue`BASE ${base}` 42 | .build() 43 | 44 | // then 45 | expect(query).toMatchSnapshot() 46 | }) 47 | 48 | it('can chain multiple quad data calls', () => { 49 | // given 50 | const expected = `PREFIX owl: 51 | INSERT DATA { 52 | owl:sameAs . 53 | owl:sameAs . 54 | }` 55 | 56 | // when 57 | const actual = INSERT 58 | .DATA` ${RDF.ns.owl.sameAs} .` 59 | .DATA` ${RDF.ns.owl.sameAs} .` 60 | .build() 61 | 62 | // then 63 | expect(actual).to.be.query(expected) 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /test/SELECT.test.ts: -------------------------------------------------------------------------------- 1 | import RDF from '@zazuko/env' 2 | import chai, { expect } from 'chai' 3 | import { jestSnapshotPlugin } from 'mocha-chai-jest-snapshot' 4 | import { SELECT } from '../src/index.js' 5 | import { sparqlClient } from './_mocks.js' 6 | import './sparql.js' 7 | 8 | describe('SELECT', () => { 9 | chai.use(jestSnapshotPlugin()) 10 | 11 | it('executes as select', () => { 12 | // given 13 | const client = sparqlClient() 14 | 15 | // when 16 | SELECT``.execute(client) 17 | 18 | // then 19 | expect(client.query.select).to.have.been.called 20 | }) 21 | 22 | it('creates a simple select/where', () => { 23 | // given 24 | const expected = 'SELECT ?s ?p ?o WHERE { ?s ?p ?o }' 25 | 26 | // when 27 | const actual = SELECT`?s ?p ?o`.WHERE`?s ?p ?o`.build() 28 | 29 | // then 30 | expect(actual).to.be.query(expected) 31 | }) 32 | 33 | it('can have additional prologue', function () { 34 | // given 35 | const base = RDF.namedNode('http://foo.bar/baz') 36 | 37 | // when 38 | const actual = SELECT`?s ?p ?o` 39 | .WHERE`?s ?p ?o` 40 | .prologue`#pragma join.hash off` 41 | .prologue`BASE ${base}` 42 | .build() 43 | 44 | // then 45 | expect(actual).toMatchSnapshot() 46 | }) 47 | 48 | it('combines multiple WHERE clauses', () => { 49 | // given 50 | const expected = 'SELECT * WHERE { ?s ?p ?o. ?a ?b ?c }' 51 | 52 | // when 53 | const actual = SELECT`*` 54 | .WHERE`?s ?p ?o .` 55 | .WHERE`?a ?b ?c .` 56 | .build() 57 | 58 | // then 59 | expect(actual).to.be.query(expected) 60 | }) 61 | 62 | it('adds FROM when default graph set', () => { 63 | // given 64 | const expected = 'SELECT * FROM WHERE { ?s ?p ?o }' 65 | 66 | // when 67 | const actual = SELECT`*`.FROM(RDF.namedNode('urn:foo:bar')).WHERE`?s ?p ?o`.build() 68 | 69 | // then 70 | expect(actual).to.be.query(expected) 71 | }) 72 | 73 | it('supports multiple FROM', () => { 74 | // given 75 | const expected = `SELECT * 76 | FROM 77 | FROM 78 | WHERE { ?s ?p ?o }` 79 | 80 | // when 81 | const actual = SELECT`*` 82 | .FROM(RDF.namedNode('urn:foo:bar')) 83 | .FROM(RDF.namedNode('urn:foo:baz')) 84 | .WHERE`?s ?p ?o`.build() 85 | 86 | // then 87 | expect(actual).to.be.query(expected) 88 | }) 89 | 90 | it('allows mixing FROM and FROM NAMED', () => { 91 | // given 92 | const expected = `SELECT * 93 | FROM 94 | FROM NAMED 95 | WHERE { ?s ?p ?o }` 96 | 97 | // when 98 | const actual = SELECT`*` 99 | .FROM(RDF.namedNode('urn:foo:bar')) 100 | .FROM().NAMED(RDF.namedNode('urn:foo:baz')) 101 | .WHERE`?s ?p ?o`.build() 102 | 103 | // then 104 | expect(actual).to.be.query(expected) 105 | }) 106 | 107 | it('does not add FROM when graph is defaultGraph', () => { 108 | // given 109 | const expected = 'SELECT * WHERE { ?s ?p ?o }' 110 | 111 | // when 112 | const actual = SELECT`*`.FROM(RDF.defaultGraph()).WHERE`?s ?p ?o`.build() 113 | 114 | // then 115 | expect(actual).to.be.query(expected) 116 | }) 117 | 118 | it('resets default graph when FROM default is called', () => { 119 | // given 120 | const expected = 'SELECT * WHERE { ?s ?p ?o }' 121 | 122 | // when 123 | const actual = SELECT`*` 124 | .FROM(RDF.namedNode('urn:foo:bar')) 125 | .FROM(RDF.defaultGraph()) 126 | .WHERE`?s ?p ?o`.build() 127 | 128 | // then 129 | expect(actual).to.be.query(expected) 130 | }) 131 | 132 | it('supports LIMIT/OFFSET', () => { 133 | // given 134 | const expected = 'SELECT ?s ?p ?o WHERE { ?s ?p ?o } LIMIT 15 OFFSET 40' 135 | 136 | // when 137 | const actual = SELECT`?s ?p ?o`.WHERE`?s ?p ?o`.LIMIT(15).OFFSET(40).build() 138 | 139 | // then 140 | expect(actual).to.be.query(expected) 141 | }) 142 | 143 | it('can be constructed with a base', () => { 144 | // given 145 | const ns = RDF.namespace('http://example.com/') 146 | const expected = ` 147 | BASE 148 | 149 | SELECT * 150 | FROM 151 | WHERE { 152 | a 153 | }` 154 | 155 | // when 156 | const actual = SELECT`*` 157 | .FROM(ns.graph) 158 | .WHERE`${ns.person} a ${ns.Person}` 159 | .build({ 160 | base: 'http://example.com/', 161 | }) 162 | 163 | // then 164 | expect(actual).to.be.query(expected) 165 | }) 166 | 167 | it('can be ordered by variable', () => { 168 | // given 169 | const expected = 'SELECT ?s ?p ?o WHERE { ?s ?p ?o } ORDER BY ?s' 170 | 171 | // when 172 | const s = RDF.variable('s') 173 | const actual = SELECT`${s} ?p ?o` 174 | .WHERE`${s} ?p ?o` 175 | .ORDER().BY(s) 176 | .build() 177 | 178 | // then 179 | expect(actual).to.be.query(expected) 180 | }) 181 | 182 | it('can be ordered by desc(?variable)', () => { 183 | // given 184 | const expected = 'SELECT ?s ?p ?o WHERE { ?s ?p ?o } ORDER BY desc(?s)' 185 | 186 | // when 187 | const s = RDF.variable('s') 188 | const actual = SELECT`${s} ?p ?o` 189 | .WHERE`${s} ?p ?o` 190 | .ORDER().BY(s, true) 191 | .build() 192 | 193 | // then 194 | expect(actual).to.be.query(expected) 195 | }) 196 | 197 | it('can be ordered by multiple variables', () => { 198 | // given 199 | const expected = 'SELECT ?s ?p ?o WHERE { ?s ?p ?o } ORDER BY desc(?s) ?o' 200 | 201 | // when 202 | const s = RDF.variable('s') 203 | const o = RDF.variable('o') 204 | const actual = SELECT`${s} ?p ${o}` 205 | .WHERE`${s} ?p ${o}` 206 | .ORDER().BY(s, true).THEN.BY(o) 207 | .build() 208 | 209 | // then 210 | expect(actual).to.be.query(expected) 211 | }) 212 | 213 | it('can be ordered and limited, when calls are reversed', () => { 214 | // given 215 | const expected = 'SELECT ?s ?p ?o WHERE { ?s ?p ?o } ORDER BY ?s LIMIT 20' 216 | 217 | // when 218 | const s = RDF.variable('s') 219 | const actual = SELECT`${s} ?p ?o` 220 | .WHERE`${s} ?p ?o` 221 | .LIMIT(20) 222 | .ORDER().BY(s) 223 | .build() 224 | 225 | // then 226 | expect(actual).to.be.query(expected) 227 | }) 228 | 229 | it('can be made distinct at any time', () => { 230 | // given 231 | const expected = 'SELECT DISTINCT ?s WHERE { ?s ?p ?o }' 232 | 233 | // when 234 | const actual = SELECT`?s`.WHERE`?s ?p ?o`.DISTINCT().build() 235 | 236 | // then 237 | expect(actual).to.be.query(expected) 238 | }) 239 | 240 | it('can add more variables', () => { 241 | // given 242 | const expected = 'SELECT ?s ?p ?o WHERE { ?s ?p ?o }' 243 | 244 | // when 245 | const actual = SELECT`?s`.AND`?p`.AND`?o`.WHERE`?s ?p ?o`.build() 246 | 247 | // then 248 | expect(actual).to.be.query(expected) 249 | }) 250 | 251 | it('wraps when interpolated in a template', () => { 252 | // given 253 | const expected = 'SELECT ?s ?p ?o WHERE { { SELECT ?s ?p ?o WHERE { ?s ?p ?o } } }' 254 | 255 | // when 256 | const subquery = SELECT`?s ?p ?o`.WHERE`?s ?p ?o` 257 | const actual = SELECT`?s ?p ?o`.WHERE`${subquery}`.build() 258 | 259 | // then 260 | expect(actual).to.be.query(expected) 261 | }) 262 | 263 | describe('ALL', () => { 264 | it('is alias for SELECT *', () => { 265 | // given 266 | const expected = 'SELECT * WHERE { ?s ?p ?o } ' 267 | 268 | // when 269 | const actual = SELECT.ALL 270 | .WHERE`?s ?p ?o` 271 | .build() 272 | 273 | // then 274 | expect(actual).to.be.query(expected) 275 | }) 276 | }) 277 | 278 | describe('DISTINCT', () => { 279 | it('creates correct SPARQL', () => { 280 | // given 281 | const expected = 'SELECT DISTINCT * WHERE { ?s ?p ?o }' 282 | 283 | // when 284 | const actual = SELECT.DISTINCT`*`.WHERE`?s ?p ?o`.build() 285 | 286 | // then 287 | expect(actual).to.be.query(expected) 288 | }) 289 | }) 290 | 291 | describe('REDUCED', () => { 292 | it('creates correct SPARQL', () => { 293 | // given 294 | const expected = 'SELECT REDUCED * WHERE { ?s ?p ?o }' 295 | 296 | // when 297 | const actual = SELECT.REDUCED`*`.WHERE`?s ?p ?o`.build() 298 | 299 | // then 300 | expect(actual).to.be.query(expected) 301 | }) 302 | }) 303 | 304 | describe('GROUP BY', () => { 305 | it('can be grouped by variable', () => { 306 | // given 307 | const expected = 'SELECT (SUM(?s) as ?sum) WHERE { ?s ?p ?o } GROUP BY (?s)' 308 | 309 | // when 310 | const s = RDF.variable('s') 311 | const actual = SELECT`(SUM(${s}) as ?sum)` 312 | .WHERE`${s} ?p ?o` 313 | .GROUP().BY(s) 314 | .build() 315 | 316 | // then 317 | expect(actual).to.be.query(expected) 318 | }) 319 | 320 | it('can be grouped by variable name (string)', () => { 321 | // given 322 | const expected = 'SELECT (SUM(?s) as ?sum) WHERE { ?s ?p ?o } GROUP BY (?s)' 323 | 324 | // when 325 | const s = RDF.variable('s') 326 | const actual = SELECT`(SUM(${s}) as ?sum)` 327 | .WHERE`${s} ?p ?o` 328 | .GROUP().BY('s') 329 | .build() 330 | 331 | // then 332 | expect(actual).to.be.query(expected) 333 | }) 334 | 335 | it('can be grouped by variable with binding keyword as variable name', () => { 336 | // given 337 | const expected = 'SELECT (SUM(?x) as ?sum) WHERE { ?s ?p ?o } GROUP BY (?s as ?x)' 338 | 339 | // when 340 | const s = RDF.variable('s') 341 | const actual = SELECT`(SUM(?x) as ?sum)` 342 | .WHERE`${s} ?p ?o` 343 | .GROUP().BY(s).AS('x') 344 | .build() 345 | 346 | // then 347 | expect(actual).to.be.query(expected) 348 | }) 349 | 350 | it('can be grouped by expression with binding keyword as variable', () => { 351 | // given 352 | const expected = 'SELECT (SUM(?x) as ?sum) WHERE { ?s ?p ?o } GROUP BY (?s + ?p as ?x)' 353 | 354 | // when 355 | const s = RDF.variable('s') 356 | const x = RDF.variable('x') 357 | const actual = SELECT`(SUM(${x}) as ?sum)` 358 | .WHERE`${s} ?p ?o` 359 | .GROUP().BY`${s} + ?p`.AS(x) 360 | .build() 361 | 362 | // then 363 | expect(actual).to.be.query(expected) 364 | }) 365 | 366 | it('can be grouped multiple times', () => { 367 | // given 368 | const expected = 'SELECT (SUM(?s) as ?sum) WHERE { ?s ?p ?o } GROUP BY (?s) (?x as ?z)' 369 | 370 | // when 371 | const s = RDF.variable('s') 372 | const x = RDF.variable('x') 373 | const actual = SELECT`(SUM(${s}) as ?sum)` 374 | .WHERE`${s} ?p ?o` 375 | .GROUP().BY(s).THEN.BY(x).AS('z') 376 | .build() 377 | 378 | // then 379 | expect(actual).to.be.query(expected) 380 | }) 381 | }) 382 | 383 | describe('HAVING', () => { 384 | it('can build multiple HAVING clauses', () => { 385 | // given 386 | const expected = 'SELECT * WHERE { ?s ?p ?o } HAVING (AVG(?s) > 10) (SUM(?p) = 0)' 387 | 388 | // when 389 | const s = RDF.variable('s') 390 | const actual = SELECT.ALL 391 | .WHERE`?s ?p ?o` 392 | .HAVING`AVG(${s}) > 10` 393 | .HAVING`SUM(?p) = 0` 394 | .build() 395 | 396 | // then 397 | expect(actual).to.be.query(expected) 398 | }) 399 | }) 400 | }) 401 | -------------------------------------------------------------------------------- /test/WITH.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import RDF from '@zazuko/env' 3 | import { DELETE, INSERT, WITH } from '../src/index.js' 4 | import './sparql.js' 5 | 6 | describe('WITH', () => { 7 | it('prepends WITH clause given as string', () => { 8 | // given 9 | const expected = 'WITH DELETE { ?s ?p ?o } WHERE { ?s ?p ?o }' 10 | 11 | // when 12 | const actual = WITH('http://test.graph/', DELETE`?s ?p ?o`.WHERE`?s ?p ?o`).build() 13 | 14 | // then 15 | expect(actual).to.be.query(expected) 16 | }) 17 | 18 | it('prepends WITH clause given as named node', () => { 19 | // given 20 | const expected = 'WITH INSERT { ?s ?p ?o } WHERE { ?s ?p ?o }' 21 | 22 | // when 23 | const actual = WITH(RDF.namedNode('http://test.graph/'), INSERT`?s ?p ?o`.WHERE`?s ?p ?o`).build() 24 | 25 | // then 26 | expect(actual).to.be.query(expected) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /test/__snapshots__/ASK.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ASK can have additional prologue 1`] = ` 4 | "#pragma join.hash off 5 | BASE 6 | 7 | ASK { ?s ?p ?o . } 8 | " 9 | `; 10 | -------------------------------------------------------------------------------- /test/__snapshots__/CONSTRUCT.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`CONSTRUCT can have additional prologue 1`] = ` 4 | "#pragma join.hash off 5 | BASE 6 | 7 | CONSTRUCT { ?s ?p ?o . } 8 | 9 | WHERE {} 10 | " 11 | `; 12 | -------------------------------------------------------------------------------- /test/__snapshots__/DELETE.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`DELETE can have additional prologue 1`] = ` 4 | "#pragma join.hash off 5 | BASE 6 | 7 | PREFIX owl: 8 | 9 | DELETE { owl:sameAs . } WHERE {}" 10 | `; 11 | -------------------------------------------------------------------------------- /test/__snapshots__/DELETE_DATA.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`DELETE DATA can delete dataset 1`] = ` 4 | "PREFIX owl: 5 | 6 | DELETE DATA { 7 | 8 | 9 | owl:sameAs . 10 | }" 11 | `; 12 | 13 | exports[`DELETE DATA can delete quads 1`] = ` 14 | "PREFIX owl: 15 | 16 | DELETE DATA { 17 | GRAPH { owl:sameAs . } 18 | }" 19 | `; 20 | 21 | exports[`DELETE DATA can delete triples 1`] = ` 22 | "PREFIX owl: 23 | 24 | DELETE DATA { 25 | owl:sameAs . 26 | }" 27 | `; 28 | 29 | exports[`DELETE DATA can have additional prologue 1`] = ` 30 | "#pragma join.hash off 31 | BASE 32 | 33 | PREFIX owl: 34 | 35 | DELETE DATA { 36 | owl:sameAs . 37 | }" 38 | `; 39 | -------------------------------------------------------------------------------- /test/__snapshots__/DESCRIBE.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`DESCRIBE can have additional prologue 1`] = ` 4 | "#pragma join.hash off 5 | BASE 6 | 7 | DESCRIBE ?foo 8 | 9 | WHERE { 10 | ?foo a ?bar 11 | } 12 | 13 | " 14 | `; 15 | -------------------------------------------------------------------------------- /test/__snapshots__/INSERT.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`INSERT can have additional prologue 1`] = ` 4 | "#pragma join.hash off 5 | BASE 6 | 7 | PREFIX owl: 8 | 9 | INSERT{ 10 | owl:sameAs . 11 | } WHERE {}" 12 | `; 13 | -------------------------------------------------------------------------------- /test/__snapshots__/INSERT_DATA.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`INSERT DATA can have additional prologue 1`] = ` 4 | "#pragma join.hash off 5 | BASE 6 | 7 | PREFIX owl: 8 | 9 | INSERT DATA { 10 | owl:sameAs . 11 | }" 12 | `; 13 | -------------------------------------------------------------------------------- /test/__snapshots__/SELECT.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`SELECT can have additional prologue 1`] = ` 4 | "#pragma join.hash off 5 | BASE 6 | 7 | SELECT ?s ?p ?o 8 | 9 | WHERE { 10 | ?s ?p ?o 11 | } 12 | 13 | 14 | 15 | " 16 | `; 17 | -------------------------------------------------------------------------------- /test/_mocks.ts: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon' 2 | import type { Client, Query } from 'sparql-http-client' 3 | 4 | export function sparqlClient(): Client, never> { 5 | return { 6 | query: { 7 | ask: sinon.stub(), 8 | select: sinon.stub(), 9 | construct: sinon.stub(), 10 | update: sinon.stub(), 11 | }, 12 | store: {} as never, 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/e2e.test.ts: -------------------------------------------------------------------------------- 1 | import ParsingClient from 'sparql-http-client/ParsingClient.js' 2 | import StreamClient from 'sparql-http-client/StreamClient.js' 3 | import { expect } from 'chai' 4 | import $rdf from '@zazuko/env' 5 | import { getStreamAsArray } from 'get-stream' 6 | import { ASK, CONSTRUCT, SELECT } from '../src/index.js' 7 | import './sparql.js' 8 | 9 | describe('ParsingClient', function () { 10 | this.timeout(10000) 11 | 12 | describe('ASK', () => { 13 | it('returns result', async () => { 14 | // given 15 | const client = new ParsingClient({ 16 | factory: $rdf, 17 | endpointUrl: 'http://dbpedia.org/sparql', 18 | }) 19 | 20 | // when 21 | const result = await ASK`?s ?p ?o`.execute(client) 22 | 23 | // then 24 | expect(result).to.be.ok 25 | }) 26 | }) 27 | 28 | describe('CONSTRUCT', () => { 29 | it('returns result', async () => { 30 | // given 31 | const client = new ParsingClient({ 32 | factory: $rdf, 33 | endpointUrl: 'http://dbpedia.org/sparql', 34 | }) 35 | 36 | // when 37 | const result = await CONSTRUCT`?s ?p ?o` 38 | .WHERE`?s ?p ?o` 39 | .LIMIT(1).execute(client) 40 | 41 | // then 42 | expect(result.size).to.eq(1) 43 | }) 44 | }) 45 | 46 | describe('SELECT', () => { 47 | it('returns result', async () => { 48 | // given 49 | const client = new ParsingClient({ 50 | factory: $rdf, 51 | endpointUrl: 'http://dbpedia.org/sparql', 52 | }) 53 | 54 | // when 55 | const result = await SELECT`?s ?p ?o` 56 | .WHERE`?s ?p ?o` 57 | .LIMIT(1).execute(client) 58 | 59 | // then 60 | expect(result.length).to.eq(1) 61 | }) 62 | }) 63 | }) 64 | 65 | describe('StreamClient', function () { 66 | this.timeout(10000) 67 | 68 | describe('ASK', () => { 69 | it('returns result', async () => { 70 | // given 71 | const client = new StreamClient({ 72 | factory: $rdf, 73 | endpointUrl: 'http://dbpedia.org/sparql', 74 | }) 75 | 76 | // when 77 | const result = await ASK`?s ?p ?o`.execute(client) 78 | 79 | // then 80 | expect(result).to.be.ok 81 | }) 82 | }) 83 | 84 | describe('CONSTRUCT', () => { 85 | it('returns result', async () => { 86 | // given 87 | const client = new StreamClient({ 88 | factory: $rdf, 89 | endpointUrl: 'http://dbpedia.org/sparql', 90 | }) 91 | 92 | // when 93 | const stream = CONSTRUCT`?s ?p ?o` 94 | .WHERE`?s ?p ?o` 95 | .LIMIT(1).execute(client) 96 | const result = await $rdf.dataset().import(stream) 97 | 98 | // then 99 | expect(result.size).to.eq(1) 100 | }) 101 | }) 102 | 103 | describe('SELECT', () => { 104 | it('returns result', async () => { 105 | // given 106 | const client = new StreamClient({ 107 | factory: $rdf, 108 | endpointUrl: 'http://dbpedia.org/sparql', 109 | }) 110 | 111 | // when 112 | const stream = SELECT`?s ?p ?o` 113 | .WHERE`?s ?p ?o` 114 | .LIMIT(1).execute(client) 115 | const result = await getStreamAsArray(stream) 116 | 117 | // then 118 | expect(result.length).to.eq(1) 119 | }) 120 | }) 121 | }) 122 | -------------------------------------------------------------------------------- /test/execute.test.ts: -------------------------------------------------------------------------------- 1 | import { Term } from '@rdfjs/types' 2 | import type { Client } from 'sparql-http-client' 3 | import { expect } from 'chai' 4 | import { ask, graph, select, update } from '../src/lib/execute.js' 5 | import { SparqlQuery } from '../src/lib/index.js' 6 | import { sparqlClient } from './_mocks.js' 7 | import './sparql.js' 8 | 9 | const builder: Pick = { 10 | build(): string { 11 | return '' 12 | }, 13 | } 14 | 15 | describe('execute', () => { 16 | describe('select', () => { 17 | let execute: (client: Client, requestInit: RequestInit) => Promise[]> 18 | 19 | beforeEach(() => { 20 | execute = select.execute.bind(builder) 21 | }) 22 | 23 | it('passes request init to sparql client', () => { 24 | // given 25 | const client = sparqlClient() 26 | 27 | // when 28 | execute(client, { 29 | headers: { 30 | authentication: 'Bearer foobar', 31 | }, 32 | }) 33 | 34 | // then 35 | expect(client.query.select).to.have.been.calledWith('', { 36 | headers: { 37 | authentication: 'Bearer foobar', 38 | }, 39 | }) 40 | }) 41 | }) 42 | describe('graph', () => { 43 | let execute: (client: Client, requestInit: RequestInit) => Promise 44 | 45 | beforeEach(() => { 46 | execute = graph.execute.bind(builder) 47 | }) 48 | 49 | it('passes request init to sparql client', () => { 50 | // given 51 | const client = sparqlClient() 52 | 53 | // when 54 | execute(client, { 55 | headers: { 56 | authentication: 'Bearer foobar', 57 | }, 58 | }) 59 | 60 | // then 61 | expect(client.query.construct).to.have.been.calledWith('', { 62 | headers: { 63 | authentication: 'Bearer foobar', 64 | }, 65 | }) 66 | }) 67 | }) 68 | describe('update', () => { 69 | let execute: (client: Client, requestInit: RequestInit) => Promise 70 | 71 | beforeEach(() => { 72 | execute = update.execute.bind(builder) 73 | }) 74 | 75 | it('passes request init to sparql client', () => { 76 | // given 77 | const client = sparqlClient() 78 | 79 | // when 80 | execute(client, { 81 | headers: { 82 | authentication: 'Bearer foobar', 83 | }, 84 | }) 85 | 86 | // then 87 | expect(client.query.update).to.have.been.calledWith('', { 88 | headers: { 89 | authentication: 'Bearer foobar', 90 | }, 91 | }) 92 | }) 93 | }) 94 | 95 | describe('ask', () => { 96 | let execute: (client: Client, requestInit: RequestInit) => Promise 97 | 98 | beforeEach(() => { 99 | execute = ask.execute.bind(builder) 100 | }) 101 | 102 | it('passes request init to sparql client', () => { 103 | // given 104 | const client = sparqlClient() 105 | 106 | // when 107 | execute(client, { 108 | headers: { 109 | authentication: 'Bearer foobar', 110 | }, 111 | }) 112 | 113 | // then 114 | expect(client.query.ask).to.have.been.calledWith('', { 115 | headers: { 116 | authentication: 'Bearer foobar', 117 | }, 118 | }) 119 | }) 120 | }) 121 | }) 122 | -------------------------------------------------------------------------------- /test/expressions/IN.test.ts: -------------------------------------------------------------------------------- 1 | import RDF from '@zazuko/env' 2 | import { expect } from 'chai' 3 | import { IN } from '../../src/expressions.js' 4 | 5 | describe('IN', () => { 6 | it('returns empty IN clause for empty array', () => { 7 | // when 8 | const expr = IN().toString() 9 | 10 | // then 11 | expect(expr).to.eq('IN ( )') 12 | }) 13 | 14 | it('combines items in comma-separated list', () => { 15 | // given 16 | const items = [ 17 | RDF.literal('foo', RDF.ns.xsd.normalizedString), 18 | RDF.ns.xsd.anyType, 19 | RDF.variable('foo'), 20 | 10.6, 21 | ] 22 | 23 | // when 24 | const expr = IN(...items)._toPartialString({ prologue: false, env: RDF }).value 25 | 26 | // then 27 | expect(expr).to.eq('IN ( "foo"^^xsd:normalizedString, xsd:anyType, ?foo, 10.6 )') 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /test/expressions/UNION.test.ts: -------------------------------------------------------------------------------- 1 | import { sparql } from '@tpluscode/rdf-string' 2 | import { expect } from 'chai' 3 | import { UNION } from '../../src/expressions.js' 4 | import { Select, SELECT } from '../../src/index.js' 5 | import '../sparql.js' 6 | 7 | describe('UNION', () => { 8 | it('returns empty for empty arg list', () => { 9 | // when 10 | const union = UNION().toString() 11 | 12 | // then 13 | expect(union).to.eq('') 14 | }) 15 | 16 | it('returns a single argument without wrapping in BGP', () => { 17 | // given 18 | const patterns = sparql` .` 19 | 20 | // when 21 | const union = UNION(patterns).toString() 22 | 23 | // then 24 | expect(union).to.eq(' .') 25 | }) 26 | 27 | it('combines multiple args in UNION', () => { 28 | // given 29 | const patterns = [ 30 | sparql` .`, 31 | sparql` .`, 32 | sparql` .`, 33 | ] 34 | 35 | // when 36 | const union = UNION(...patterns).toString() 37 | 38 | // then 39 | expect(union).to.eq(`{ 40 | . 41 | } UNION { 42 | . 43 | } UNION { 44 | . 45 | }`) 46 | }) 47 | 48 | it('combines multiple strings in UNION', () => { 49 | // given 50 | const patterns = [ 51 | ' .', 52 | ' .', 53 | ' .', 54 | ] 55 | 56 | // when 57 | const union = UNION(...patterns).toString() 58 | 59 | // then 60 | expect(union).to.eq(`{ 61 | . 62 | } UNION { 63 | . 64 | } UNION { 65 | . 66 | }`) 67 | }) 68 | 69 | it('combines multiple subqueries in UNION', () => { 70 | // given 71 | const patterns: Select[] = [ 72 | SELECT`?s ?p ?o`.WHERE`?s ?p ?o`, 73 | SELECT`?a ?b ?c`.WHERE`?a ?b ?c`, 74 | ] 75 | 76 | // when 77 | const union = SELECT.ALL.WHERE`${UNION(...patterns)}`.build() 78 | 79 | // then 80 | expect(union).to.be.query(`SELECT * WHERE { { 81 | SELECT ?s ?p ?o WHERE { 82 | ?s ?p ?o 83 | } 84 | } UNION { 85 | SELECT ?a ?b ?c WHERE { 86 | ?a ?b ?c 87 | } 88 | } }`) 89 | }) 90 | }) 91 | -------------------------------------------------------------------------------- /test/expressions/VALUES.test.ts: -------------------------------------------------------------------------------- 1 | import RDF from '@rdfjs/data-model' 2 | import { expect } from 'chai' 3 | import { VALUES } from '../../src/expressions.js' 4 | 5 | describe('VALUES', () => { 6 | it('returns empty for empty array', () => { 7 | // when 8 | const expr = VALUES().toString() 9 | 10 | // then 11 | expect(expr).to.eq('') 12 | }) 13 | 14 | it('builds values clause', () => { 15 | // given 16 | const bar = { foo: 'bar' } 17 | const baz = { foo: 'baz' } 18 | 19 | // when 20 | const expr = VALUES(bar, baz).toString() 21 | 22 | // then 23 | expect(expr).to.eq(`VALUES ( ?foo ) 24 | { 25 | ( "bar" ) 26 | ( "baz" ) 27 | }`) 28 | }) 29 | 30 | it('uses UNDEF for undefined or null values', () => { 31 | // given 32 | const foo = { val: 'foo' } 33 | const bar = { val: null } 34 | const baz = { } 35 | 36 | // when 37 | const expr = VALUES(foo, bar, baz).toString() 38 | 39 | // then 40 | expect(expr).to.eq(`VALUES ( ?val ) 41 | { 42 | ( "foo" ) 43 | ( UNDEF ) 44 | ( UNDEF ) 45 | }`) 46 | }) 47 | 48 | it('handles various values', () => { 49 | // given 50 | const foo = { val: 11.6 } 51 | const bar = { val: true } 52 | const baz = { val: RDF.literal('baz', 'de') } 53 | 54 | // when 55 | const expr = VALUES(foo, bar, baz).toString() 56 | 57 | // then 58 | expect(expr).to.eq(`VALUES ( ?val ) 59 | { 60 | ( 11.6 ) 61 | ( true ) 62 | ( "baz"@de ) 63 | }`) 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /test/mochaSetup.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai' 2 | import sinonChai from 'sinon-chai' 3 | import chaiAsPromised from 'chai-as-promised' 4 | 5 | chai.use(chaiAsPromised) 6 | chai.use(sinonChai) 7 | -------------------------------------------------------------------------------- /test/sparql.ts: -------------------------------------------------------------------------------- 1 | import sparqljs from 'sparqljs' 2 | import { Assertion, AssertionError } from 'chai' 3 | 4 | const sparqlParser = new sparqljs.Parser() 5 | const generator = new sparqljs.Generator({ 6 | allPrefixes: false, 7 | }) 8 | 9 | declare global { 10 | // eslint-disable-next-line @typescript-eslint/no-namespace 11 | namespace Chai { 12 | interface TypeComparison { 13 | query(expected: string): void 14 | } 15 | } 16 | } 17 | 18 | Assertion.addMethod('query', function (this: Chai.AssertionStatic, expected: string) { 19 | const received = this._obj 20 | let expectedQuery: any 21 | try { 22 | expectedQuery = generator.stringify(sparqlParser.parse(expected)) 23 | } catch (e) { 24 | throw new AssertionError(`Failed to parse expected query:\n ${expected}`) 25 | } 26 | let actualQuery: any 27 | try { 28 | actualQuery = generator.stringify(sparqlParser.parse(received)) 29 | } catch (e: any) { 30 | throw new AssertionError(`Failed to parse actual query. 31 | ${e.message}. 32 | 33 | Query was: 34 | ${received.toString()} 35 | `) 36 | } 37 | 38 | new Assertion(actualQuery).to.equal(expectedQuery) 39 | }) 40 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["test/**/*.ts", "docs", "test/mochaSetup.js"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "NodeNext", 4 | "target": "ES2020", 5 | "outDir": ".", 6 | "lib": [ 7 | "ES2020", 8 | "DOM" 9 | ], 10 | "strictNullChecks": true, 11 | "strict": true, 12 | "noImplicitAny": true, 13 | "noImplicitReturns": true, 14 | "noImplicitThis": true, 15 | "moduleResolution": "NodeNext", 16 | "esModuleInterop": true, 17 | "declaration": true, 18 | "declarationMap": true 19 | }, 20 | "files": [ 21 | "src/index.ts", 22 | "src/expressions.ts" 23 | ], 24 | "typedocOptions": { 25 | "name": "@tpluscode/sparql-builder", 26 | "entryPoints": [ 27 | "src/index.ts", 28 | "src/expressions.ts" 29 | ], 30 | "out": "docs/api" 31 | } 32 | } 33 | --------------------------------------------------------------------------------