├── .circleci └── config.yml ├── .github └── workflows │ └── npm-publish.yml ├── .gitignore ├── .npmignore ├── .tool-versions ├── README.md ├── babel.config.js ├── jest-plugins.js ├── package.json ├── src ├── ComponentStore │ ├── ComponentCache.ts │ ├── ComponentStore.ts │ └── __tests__ │ │ └── ComponentStore.spec.ts ├── LinkedDataAPI.ts ├── LinkedRenderStore.ts ├── ProcessBroadcast.ts ├── RDFStore.ts ├── Schema.ts ├── TypedRecord.ts ├── __tests__ │ ├── LinkedRenderStore.spec.ts │ ├── LinkedRenderStore │ │ ├── components.spec.ts │ │ ├── data.spec.ts │ │ ├── delta.spec.ts │ │ ├── dig.spec.ts │ │ ├── findSubject.spec.ts │ │ ├── fixtures.ts │ │ ├── network.spec.ts │ │ ├── render.spec.ts │ │ └── subscriptions.spec.ts │ ├── ProcessBroadcast.spec.ts │ ├── RDFStore.spec.ts │ ├── Schema.spec.ts │ ├── TypedRecord.spec.ts │ ├── createStore.spec.ts │ ├── factoryHelpers.spec.ts │ ├── factoryHelpersHashFactory.spec.ts │ ├── linkMiddleware.spec.ts │ ├── useFactory.ts │ └── utilities.spec.ts ├── createStore.ts ├── datastrucures │ ├── DataSlice.ts │ ├── DataSliceDSL.ts │ ├── DeepSlice.ts │ ├── EmpSlice.ts │ ├── Fields.ts │ └── __tests__ │ │ └── DataSliceDSL.spec.ts ├── factoryHelpers.ts ├── index.ts ├── linkMiddleware.ts ├── messages │ ├── __tests__ │ │ └── messageProcessor.spec.ts │ ├── message.ts │ └── messageProcessor.ts ├── ontology │ ├── argu.ts │ ├── ex.ts │ ├── example.ts │ ├── http.ts │ ├── http07.ts │ ├── httph.ts │ ├── ld.ts │ ├── link.ts │ └── ll.ts ├── processor │ ├── DataProcessor.ts │ ├── DataToGraph.ts │ ├── ProcessorError.ts │ ├── RequestInitGenerator.ts │ └── __tests__ │ │ ├── DataProcessor.spec.ts │ │ ├── DataToGraph.spec.ts │ │ ├── ProcessorError.spec.ts │ │ └── RequestInitGenerator.spec.ts ├── rdf.ts ├── schema │ ├── __tests__ │ │ └── rdfs.spec.ts │ ├── owl.ts │ └── rdfs.ts ├── store │ ├── RDFAdapter.ts │ ├── RecordJournal.ts │ ├── RecordState.ts │ ├── RecordStatus.ts │ ├── StructuredStore.ts │ ├── StructuredStore │ │ └── references.ts │ ├── __tests__ │ │ ├── RDFAdapter.spec.ts │ │ ├── RDFIndex.spec.ts │ │ ├── RecordJournal.spec.ts │ │ ├── StructuredStore.spec.ts │ │ └── deltaProcessor.spec.ts │ └── deltaProcessor.ts ├── testUtilities.ts ├── transformers │ ├── hextuples.ts │ ├── index.ts │ ├── linked-delta.ts │ └── rdf-formats-common.ts ├── types.ts ├── utilities.ts ├── utilities │ ├── DisjointSet.ts │ ├── __tests__ │ │ ├── memoizedNamespace.spec.ts │ │ ├── responses.spec.ts │ │ └── slices.spec.ts │ ├── constants.ts │ ├── memoizedNamespace.ts │ ├── responses.ts │ └── slices.ts └── worker │ ├── DataWorkerLoader.ts │ └── messages.ts ├── tsconfig.json ├── tslint.json └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | defaults: &defaults 3 | docker: 4 | - image: circleci/node:13 5 | working_directory: ~/link-lib 6 | 7 | jobs: 8 | build: 9 | <<: *defaults 10 | steps: 11 | - run: 12 | name: Download cc-test-reporter 13 | command: | 14 | mkdir -p tmp/ 15 | curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./tmp/cc-test-reporter 16 | chmod +x ./tmp/cc-test-reporter 17 | - persist_to_workspace: 18 | root: tmp 19 | paths: 20 | - cc-test-reporter 21 | 22 | build-node-16: 23 | <<: *defaults 24 | docker: 25 | - image: circleci/node:16 26 | steps: 27 | - checkout 28 | - attach_workspace: 29 | at: ~/link-lib/tmp 30 | - restore_cache: 31 | keys: 32 | - v1-dependencies-16-{{ checksum "package.json" }} 33 | # fallback to using the latest cache if no exact match is found 34 | - v1-dependencies-16- 35 | 36 | - run: yarn install 37 | 38 | - save_cache: 39 | paths: 40 | - node_modules 41 | key: v1-dependencies-16-{{ checksum "package.json" }} 42 | - run: yarn lint 43 | - run: yarn test -w 1 44 | - run: yarn build 45 | 46 | build-node-14: 47 | <<: *defaults 48 | docker: 49 | - image: circleci/node:14 50 | steps: 51 | - checkout 52 | - attach_workspace: 53 | at: ~/link-lib/tmp 54 | - restore_cache: 55 | keys: 56 | - v1-dependencies-14-{{ checksum "package.json" }} 57 | # fallback to using the latest cache if no exact match is found 58 | - v1-dependencies-14- 59 | 60 | - run: yarn install 61 | 62 | - save_cache: 63 | paths: 64 | - node_modules 65 | key: v1-dependencies-14-{{ checksum "package.json" }} 66 | - run: yarn lint 67 | - run: yarn test -w 1 68 | - run: ./tmp/cc-test-reporter format-coverage -t lcov -o ~/link-lib/tmp/codeclimate.node-14.json coverage/lcov.info 69 | - run: yarn build 70 | - persist_to_workspace: 71 | root: tmp 72 | paths: 73 | - codeclimate.node-14.json 74 | 75 | build-node-12: 76 | <<: *defaults 77 | docker: 78 | - image: circleci/node:12 79 | steps: 80 | - checkout 81 | - restore_cache: 82 | keys: 83 | - v1-dependencies-12-{{ checksum "package.json" }} 84 | # fallback to using the latest cache if no exact match is found 85 | - v1-dependencies-12- 86 | 87 | - run: yarn install 88 | 89 | - save_cache: 90 | paths: 91 | - node_modules 92 | key: v1-dependencies-12-{{ checksum "package.json" }} 93 | - run: yarn lint 94 | - run: yarn test -w 1 95 | - run: yarn build 96 | 97 | upload-coverage: 98 | <<: *defaults 99 | environment: 100 | CC_TEST_REPORTER_ID: f49a72b364886e0b9aae7a678c2fc1235276270cce13dc92f0b856f3438df624 101 | steps: 102 | - attach_workspace: 103 | at: ~/link-lib/tmp 104 | - run: 105 | name: Upload coverage results to Code Climate 106 | command: | 107 | ./tmp/cc-test-reporter sum-coverage tmp/codeclimate.*.json -p 1 -o tmp/codeclimate.total.json 108 | ./tmp/cc-test-reporter upload-coverage -i tmp/codeclimate.total.json 109 | 110 | workflows: 111 | version: 2 112 | commit: 113 | jobs: 114 | - build 115 | - build-node-16 116 | - build-node-14: 117 | requires: 118 | - build 119 | - build-node-12 120 | - upload-coverage: 121 | requires: 122 | - build-node-14 123 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | push: 8 | branches: 9 | - "*" 10 | tags: 11 | - v* 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: actions/setup-node@v3 18 | with: 19 | node-version: 16 20 | cache: 'yarn' 21 | - run: yarn install --frozen-lockfile 22 | - run: yarn build 23 | - run: yarn test 24 | continue-on-error: true 25 | 26 | publish-npm: 27 | needs: build 28 | runs-on: ubuntu-latest 29 | if: contains(github.ref, 'refs/tags/v') 30 | steps: 31 | - uses: actions/checkout@v3 32 | - uses: actions/setup-node@v3 33 | with: 34 | node-version: 16 35 | registry-url: https://registry.npmjs.org/ 36 | - run: yarn install --frozen-lockfile 37 | - run: yarn build 38 | - run: npm version --no-git-tag-version prerelease --preid=dev.$GITHUB_SHA 39 | working-directory: ./pkg 40 | - run: npm publish 41 | working-directory: ./pkg 42 | env: 43 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 44 | 45 | pre-publish-npm: 46 | needs: build 47 | runs-on: ubuntu-latest 48 | steps: 49 | - uses: actions/checkout@v3 50 | - uses: actions/setup-node@v3 51 | with: 52 | node-version: 16 53 | registry-url: https://registry.npmjs.org/ 54 | - run: yarn install --frozen-lockfile 55 | - run: yarn build 56 | - run: npm version --no-git-tag-version prerelease --preid=dev.$GITHUB_SHA 57 | working-directory: ./pkg 58 | - run: npm publish --tag dev 59 | working-directory: ./pkg 60 | env: 61 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | .idea 3 | .rpt2* 4 | dist/docs/ 5 | coverage/** 6 | pkg/ 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | dist/docs/ 3 | coverage/ 4 | .rpt2_cache/ -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 16.15.0 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Link Library 2 | *A Link to the Web* 3 | 4 | ### [Reference](https://github.com/rescribet/link-redux/wiki): How to use Link with React 5 | 6 | [![CircleCI](https://img.shields.io/circleci/build/gh/rescribet/link-lib)](https://circleci.com/gh/rescribet/link-lib) 7 | ![Code Climate coverage](https://img.shields.io/codeclimate/coverage/rescribet/link-lib) 8 | 9 | This package aims to make building rich web applications quick and easy by providing all the tools 10 | needed to work with linked data, providing high-level API's for view rendering, data querying 11 | & manipulation, and API communication. See the [link-redux](https://github.com/rescribet/link-redux) package on how to 12 | use this in a React project. 13 | 14 | To transform your Rails application into a linked-data serving beast, see our 15 | [Active Model Serializers plugin](https://github.com/argu-co/rdf-serializers). 16 | 17 | This was built at [Argu](https://argu.co), if you like what we do, these technologies 18 | or open data, send us [a mail](mailto:info@argu.co). 19 | 20 | ## Example 21 | See the [TODO app](https://rescribet.github.io/link-redux-todo/#/) for a live example and 22 | [link-redux-todo](https://github.com/rescribet/link-redux-todo) for the implementation. Mind that it isn't connected to 23 | a back-end, so it's only a demo for the view rendering mechanism. 24 | 25 | ## Installation 26 | 27 | `yarn add link-lib` 28 | 29 | and some peer dependencies: 30 | 31 | `yarn add @ontologies/as @ontologies/core @ontologies/schema @ontologies/shacl @ontologies/xsd http-status-codes n-quads-parser` 32 | 33 | The package externalizes the Promise API, so make sure to include your own when targeting platforms without native 34 | support. 35 | 36 | # Usage 37 | 38 | See the [Hypermedia API page](https://github.com/rescribet/link-lib/wiki/Hypermedia-API) for documentation on how to 39 | execute actions against the service. 40 | 41 | See [Link Redux](https://github.com/rescribet/link-redux) for documentation on how to use Link in a React application. 42 | 43 | # Contributing 44 | 45 | The usual stuff. Open an issue to discuss a change, open PR's from topic-branches targeted to master for bugfixes and 46 | refactors. 47 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', {targets: {node: 'current'}}], 4 | '@babel/preset-typescript', 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /jest-plugins.js: -------------------------------------------------------------------------------- 1 | global.fetch = require('jest-fetch-mock'); 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "link-lib", 3 | "version": "4.0.0-0", 4 | "description": "The Link library for creating information based applications", 5 | "repository": "https://github.com/rescribet/link-lib.git", 6 | "scripts": { 7 | "build": "./node_modules/.bin/pika build", 8 | "lint": "tslint -c tslint.json 'src/**/*.{ts,tsx}'", 9 | "prepublish": "npm run build", 10 | "pretest": "npm run lint", 11 | "test": "jest --coverage", 12 | "version": "yarn run build" 13 | }, 14 | "author": "Thom van Kalkeren ", 15 | "license": "LGPL-3.0", 16 | "@pika/pack": { 17 | "pipeline": [ 18 | [ 19 | "@pika/plugin-ts-standard-pkg", 20 | { 21 | "exclude": [ 22 | "__tests__/**/*" 23 | ] 24 | } 25 | ], 26 | [ 27 | "@pika/plugin-build-node" 28 | ], 29 | [ 30 | "@pika/plugin-build-web", 31 | { 32 | "targets": { 33 | "firefox": "70" 34 | } 35 | } 36 | ] 37 | ] 38 | }, 39 | "peerDependencies": { 40 | "@ontologies/as": ">=1.0.1", 41 | "@ontologies/core": ">=2.0.0", 42 | "@ontologies/ld": ">=1.0.0", 43 | "@ontologies/schema": ">=1.0.0", 44 | "@ontologies/shacl": ">=1.0.0", 45 | "@ontologies/xsd": ">=1.0.0", 46 | "hextuples": ">=2.0.0", 47 | "http-status-codes": ">= 1.x", 48 | "n-quads-parser": "^2.1.1" 49 | }, 50 | "devDependencies": { 51 | "@ontola/memoized-hash-factory": "^2.1.0", 52 | "@ontologies/as": "^2.0.0", 53 | "@ontologies/core": "^2.0.2", 54 | "@ontologies/dcterms": "^2.0.0", 55 | "@ontologies/ld": "^2.0.0", 56 | "@ontologies/owl": "^2.0.0", 57 | "@ontologies/rdf": "^2.0.0", 58 | "@ontologies/rdfs": "^2.0.1", 59 | "@ontologies/schema": "^2.0.0", 60 | "@ontologies/shacl": "^2.0.0", 61 | "@ontologies/xsd": "^2.0.0", 62 | "@pika/pack": "^0.5.0", 63 | "@pika/plugin-build-node": "^0.9.2", 64 | "@pika/plugin-build-web": "^0.9.2", 65 | "@pika/plugin-bundle-web": "^0.9.2", 66 | "@pika/plugin-ts-standard-pkg": "^0.9.2", 67 | "@rdfdev/iri": "^1.0.0", 68 | "@types/jest": "^27.0.0", 69 | "@types/murmurhash-js": "^1.0.3", 70 | "@types/node": "^12.11.2", 71 | "core-js": "^3.16.1", 72 | "esdoc": "^1.1.0", 73 | "gh-pages": "^2.2.0", 74 | "hextuples": "^2.0.0", 75 | "http-status-codes": ">= 1.x", 76 | "jest": "^27.0.6", 77 | "jest-fetch-mock": "^3.0.3", 78 | "n-quads-parser": "^2.1.1", 79 | "ts-jest": "^27.0.4", 80 | "tslint": "^5.20.1", 81 | "typescript": "^4.6.3" 82 | }, 83 | "babel": { 84 | "presets": [ 85 | "@babel/preset-typescript" 86 | ] 87 | }, 88 | "jest": { 89 | "automock": false, 90 | "coveragePathIgnorePatterns": [ 91 | "/node_modules/", 92 | "src/utilities/DisjointSet.ts" 93 | ], 94 | "coverageThreshold": { 95 | "global": { 96 | "branches": 82, 97 | "functions": 88, 98 | "lines": 90, 99 | "statements": 90 100 | } 101 | }, 102 | "testEnvironment": "jsdom", 103 | "setupFiles": [ 104 | "core-js", 105 | "./jest-plugins" 106 | ], 107 | "testMatch": [ 108 | "**/*.spec.ts" 109 | ], 110 | "moduleFileExtensions": [ 111 | "js", 112 | "ts" 113 | ], 114 | "testURL": "http://example.org/resources/5" 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/ComponentStore/ComponentCache.ts: -------------------------------------------------------------------------------- 1 | export class ComponentCache { 2 | private lookupCache: { [s: string]: T | null } = {}; 3 | 4 | /** 5 | * Adds a renderer to {this.lookupCache} 6 | * @param component The render component. 7 | * @param key The memoization key. 8 | * @returns The renderer passed with {component} 9 | */ 10 | public add(component: T | null, key: string): T | null { 11 | this.lookupCache[key] = component; 12 | 13 | return this.lookupCache[key]; 14 | } 15 | 16 | public clear(): void { 17 | this.lookupCache = {}; 18 | } 19 | 20 | /** 21 | * Resolves a renderer from the {lookupCache}. 22 | * @param key The key to look up. 23 | * @returns If saved the render component, otherwise undefined. 24 | */ 25 | public get(key: string): T | null | undefined { 26 | return this.lookupCache[key]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/ComponentStore/__tests__/ComponentStore.spec.ts: -------------------------------------------------------------------------------- 1 | import "jest"; 2 | import "../../__tests__/useFactory"; 3 | 4 | import rdfFactory from "@ontologies/core"; 5 | import * as rdfs from "@ontologies/rdfs"; 6 | import * as schema from "@ontologies/schema"; 7 | 8 | import { RDFStore } from "../../RDFStore"; 9 | import { Schema } from "../../Schema"; 10 | import { getBasicStore } from "../../testUtilities"; 11 | import { DEFAULT_TOPOLOGY, RENDER_CLASS_NAME } from "../../utilities/constants"; 12 | import { ComponentStore } from "../ComponentStore"; 13 | 14 | const DT = rdfFactory.id(DEFAULT_TOPOLOGY); 15 | const RCN = rdfFactory.id(RENDER_CLASS_NAME); 16 | 17 | describe("ComponentStore", () => { 18 | describe("registerRenderer", () => { 19 | it("fails without component", () => { 20 | expect(() => { 21 | ComponentStore.registerRenderer( 22 | undefined, 23 | [schema.Thing.value], 24 | [RCN], 25 | [DT], 26 | ); 27 | }).toThrowError(); 28 | }); 29 | 30 | it("registers with full notation", () => { 31 | const comp = (): string => "a"; 32 | const reg = ComponentStore.registerRenderer( 33 | comp, 34 | [schema.Thing.value], 35 | [RCN], 36 | [DT], 37 | ); 38 | 39 | expect(reg).toEqual([{ 40 | component: comp, 41 | property: RCN, 42 | topology: DT, 43 | type: schema.Thing.value, 44 | }]); 45 | }); 46 | 47 | it ("checks types for undefined values", () => { 48 | expect(() => { 49 | ComponentStore.registerRenderer( 50 | () => undefined, 51 | [schema.Thing.value, undefined!], 52 | [RCN], 53 | [DT], 54 | ); 55 | }).toThrowError(TypeError); 56 | }); 57 | 58 | it ("checks properties for undefined values", () => { 59 | expect(() => { 60 | ComponentStore.registerRenderer( 61 | () => undefined, 62 | [schema.Thing.value], 63 | [RCN, undefined!], 64 | [DT], 65 | ); 66 | }).toThrowError(TypeError); 67 | }); 68 | 69 | it ("checks topologies for undefined values", () => { 70 | expect(() => { 71 | ComponentStore.registerRenderer( 72 | () => undefined, 73 | [schema.Thing.value], 74 | [RCN], 75 | [DT, undefined!], 76 | ); 77 | }).toThrowError(TypeError); 78 | }); 79 | 80 | it ("returns undefined when no property is given", () => { 81 | const store = getBasicStore(); 82 | 83 | expect(store.mapping.registerRenderer( 84 | () => undefined, 85 | schema.Thing.value, 86 | undefined, 87 | undefined, 88 | )).toBeUndefined(); 89 | }); 90 | }); 91 | 92 | describe("getRenderComponent", () => { 93 | it("resolved with unregistered views", () => { 94 | const store = new ComponentStore(new Schema(new RDFStore())); 95 | const unregistered = schema.url.value; 96 | const registered = schema.name.value; 97 | 98 | const comp = (): string => "test"; 99 | store.registerRenderer(comp, schema.BlogPosting.value, registered); 100 | 101 | const lookup = store.getRenderComponent( 102 | [schema.BlogPosting.value], 103 | [unregistered, registered], 104 | DEFAULT_TOPOLOGY.value, 105 | rdfs.Resource.value, 106 | ); 107 | 108 | expect(lookup).toEqual(comp); 109 | }); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /src/LinkedDataAPI.ts: -------------------------------------------------------------------------------- 1 | import { NamedNode, Quadruple } from "@ontologies/core"; 2 | 3 | import { 4 | DataTuple, 5 | DeltaProcessor, 6 | Dispatcher, 7 | EmptyRequestStatus, 8 | FulfilledRequestStatus, 9 | LinkedActionResponse, 10 | PendingRequestStatus, 11 | ResourceQueueItem, 12 | ResponseTransformer, 13 | SomeNode, 14 | } from "./types"; 15 | 16 | export interface APIFetchOpts { 17 | clearPreviousData?: boolean; 18 | force?: boolean; 19 | } 20 | 21 | export interface LinkedDataAPI extends Dispatcher, DeltaProcessor { 22 | 23 | execActionByIRI(subject: SomeNode, dataTuple: DataTuple): Promise; 24 | 25 | /** @private */ 26 | getEntities(resources: ResourceQueueItem[]): Promise; 27 | 28 | /** 29 | * Gets an entity by its SomeNode. 30 | * 31 | * When data is already present for the SomeNode as a subject, the stored data is returned, 32 | * otherwise the SomeNode will be fetched and processed. 33 | * @param iri The SomeNode of the resource 34 | * @param opts The options for fetch-/processing the resource. 35 | * @return A promise with the resulting entity 36 | */ 37 | getEntity(iri: NamedNode, opts?: APIFetchOpts): Promise; 38 | 39 | /** 40 | * Retrieve the (network) status for a resource. 41 | * 42 | * This API is still unstable, but only the latest status should be taken into account. So if a resource was 43 | * successfully fetched at some point, but a retry failed, the result will be failed. 44 | * 45 | * Some cases don't have proper HTTP status codes, but some (unstandardized) codes are very close. 46 | * 47 | * Special errors: 48 | * - Resources which are still loading are given status `202 Accepted`. 49 | * - Resources where fetching timed out are given status `408 - Request Timeout`. 50 | * - Resources where fetching failed due to browser and OS errors are given status `499 - Client Closed Request`. 51 | * - Resources which haven't been requested and aren't scheduled to be requested currently have no status code. 52 | * 53 | * @param iri The resource to get the status on. 54 | */ 55 | getStatus(iri: SomeNode): EmptyRequestStatus | PendingRequestStatus | FulfilledRequestStatus; 56 | 57 | /** @unstable */ 58 | invalidate(iri: string | SomeNode, error?: Error): boolean; 59 | 60 | /** @unstable */ 61 | isInvalid(iri: SomeNode): boolean; 62 | 63 | /** Register a transformer so it can be used to interact with API's. */ 64 | registerTransformer(processor: ResponseTransformer, 65 | mediaType: string | string[], 66 | acceptValue: number): void; 67 | 68 | /** 69 | * Overrides the `Accept` value for when a certain host doesn't respond well to multiple values. 70 | * @param origin The iri of the origin for the requests. 71 | * @param acceptValue The value to use for the `Accept` header. 72 | */ 73 | setAcceptForHost(origin: string, acceptValue: string): void; 74 | } 75 | -------------------------------------------------------------------------------- /src/ProcessBroadcast.ts: -------------------------------------------------------------------------------- 1 | import { SubscriptionRegistrationBase } from "./types"; 2 | 3 | export interface ProcessBroadcastOpts { 4 | bulkSubscriptions: Array>; 5 | /** Ids of the subjects which have been changed in this batch */ 6 | changedSubjects: string[]; 7 | /** 8 | * Subject registrations to call 9 | * It is assumed to only contain subscriptions relevant to the {work}. 10 | */ 11 | subjectSubscriptions: Array>; 12 | timeout: number; 13 | } 14 | 15 | /** 16 | * Tries to schedule updates async if possible. 17 | */ 18 | export class ProcessBroadcast { 19 | private readonly bulkSubscriptions: Array>; 20 | private readonly changedSubjects: string[]; 21 | private readonly hasIdleCallback: boolean; 22 | private readonly hasRequestAnimationFrame: boolean; 23 | private readonly regUpdateTime: number; 24 | private subjectSubscriptions: Array>; 25 | private resolve: () => void; 26 | private readonly timeout: number; 27 | 28 | constructor(opts: ProcessBroadcastOpts) { 29 | this.hasIdleCallback = "requestIdleCallback" in window; 30 | this.hasRequestAnimationFrame = "requestAnimationFrame" in window; 31 | this.resolve = (): void => undefined; 32 | this.timeout = opts.timeout; 33 | this.regUpdateTime = Date.now(); 34 | 35 | this.bulkSubscriptions = opts.bulkSubscriptions; 36 | this.changedSubjects = opts.changedSubjects; 37 | this.subjectSubscriptions = opts.subjectSubscriptions; 38 | 39 | this.queue = this.queue.bind(this); 40 | } 41 | 42 | public done(): boolean { 43 | return this.subjectSubscriptions.length === 0 && this.bulkSubscriptions.length === 0; 44 | } 45 | 46 | public run(): Promise { 47 | if (this.timeout === 0) { 48 | this.queue(); 49 | return Promise.resolve(); 50 | } 51 | 52 | return new Promise((resolve): void => { 53 | this.resolve = resolve; 54 | this.queue(); 55 | }); 56 | } 57 | 58 | /** 59 | * Calls the subscriber callback function {reg} with the correct arguments according to its 60 | * registration settings. 61 | */ 62 | private callSubscriber(reg: SubscriptionRegistrationBase): void { 63 | if (reg.markedForDelete) { 64 | return; 65 | } 66 | reg.callback(this.changedSubjects, this.regUpdateTime); 67 | } 68 | 69 | private process(): void { 70 | if (this.bulkSubscriptions.length > 0) { 71 | this.callSubscriber(this.bulkSubscriptions.pop()!); 72 | } else if (this.subjectSubscriptions.length > 0) { 73 | this.callSubscriber(this.subjectSubscriptions.pop()!); 74 | } 75 | } 76 | 77 | private queue(idleCallback?: IdleDeadline | number): void { 78 | if (this.timeout !== 0 && this.hasIdleCallback) { 79 | while (typeof idleCallback === "object" 80 | && (!this.done() && (idleCallback.timeRemaining() > 0 || idleCallback.didTimeout))) { 81 | this.process(); 82 | } 83 | 84 | if (this.done()) { 85 | return this.resolve(); 86 | } 87 | 88 | window.requestIdleCallback(this.queue, {timeout: this.timeout}); 89 | } else if (this.timeout !== 0 && this.hasRequestAnimationFrame) { 90 | this.process(); 91 | while (typeof idleCallback === "number" && (performance.now() - idleCallback) < 33) { 92 | this.process(); 93 | } 94 | 95 | if (this.done()) { 96 | return this.resolve(); 97 | } 98 | 99 | window.requestAnimationFrame(this.queue); 100 | } else { 101 | while (!this.done()) { 102 | this.process(); 103 | } 104 | 105 | this.resolve(); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Schema.ts: -------------------------------------------------------------------------------- 1 | import * as rdfx from "@ontologies/rdf"; 2 | import * as rdfs from "@ontologies/rdfs"; 3 | 4 | import { DataRecord, Id } from "./datastrucures/DataSlice"; 5 | import { OWL } from "./schema/owl"; 6 | import { RDFS } from "./schema/rdfs"; 7 | import { normalizeType } from "./utilities"; 8 | import { DisjointSet } from "./utilities/DisjointSet"; 9 | 10 | import { RDFStore } from "./RDFStore"; 11 | import { 12 | VocabularyProcessingContext, 13 | VocabularyProcessor, 14 | } from "./types"; 15 | 16 | /** 17 | * Implements some RDF/OWL logic to enhance the functionality of the property lookups. 18 | * 19 | * Basically duplicates some functionality already present in {IndexedFormula} IIRC, but this API should be more 20 | * optimized so it can be used in real-time by low-power devices as well. 21 | */ 22 | export class Schema { 23 | private static vocabularies: VocabularyProcessor[] = [OWL, RDFS]; 24 | 25 | private equivalenceSet: DisjointSet = new DisjointSet(); 26 | // Typescript can't handle generic index types, so it is set to string. 27 | private expansionCache: { [k: string]: string[] }; 28 | private liveStore: RDFStore; 29 | private superMap: Map> = new Map(); 30 | private processedTypes: string[] = []; 31 | 32 | public constructor(liveStore: RDFStore) { 33 | this.liveStore = liveStore; 34 | this.liveStore.getInternalStore().addRecordCallback((recordId: Id): void => { 35 | const record = this.liveStore.getInternalStore().store.getRecord(recordId); 36 | 37 | if (record === undefined) { 38 | return; 39 | } 40 | this.process.call(this, record); 41 | }); 42 | this.expansionCache = {}; 43 | 44 | for (const vocab of Schema.vocabularies) { 45 | this.liveStore.addQuads(vocab.axioms); 46 | } 47 | 48 | const preexisting = liveStore.getInternalStore().store.allRecords(); 49 | for (const record of preexisting) { 50 | this.process(record); 51 | } 52 | } 53 | 54 | public allEquals(recordId: Id, grade = 1.0): Id[] { 55 | if (grade >= 0) { 56 | return this.equivalenceSet.allValues(recordId); 57 | } 58 | 59 | return [recordId]; 60 | } 61 | 62 | /** @private */ 63 | public isInstanceOf(recordId: Id, klass: Id): boolean { 64 | const type = this.liveStore.getInternalStore().store.getField(recordId, rdfx.type.value); 65 | 66 | if (type === undefined) { 67 | return false; 68 | } 69 | 70 | const allCheckTypes = this.expand([klass]); 71 | const allRecordTypes = this.expand(Array.isArray(type) ? type.map((t) => t.value) : [type.value]); 72 | 73 | return allRecordTypes.some((t) => allCheckTypes.includes(t)); 74 | } 75 | 76 | public expand(types: Id[]): Id[] { 77 | if (types.length === 1) { 78 | const existing = this.expansionCache[types[0] as unknown as string]; 79 | this.expansionCache[types[0] as unknown as string] = existing 80 | ? existing 81 | : this.sort(this.mineForTypes(types)); 82 | 83 | return this.expansionCache[types[0] as unknown as string]; 84 | } 85 | 86 | return this.sort(this.mineForTypes(types)); 87 | } 88 | 89 | public getProcessingCtx(): VocabularyProcessingContext { 90 | return { 91 | dataStore: this.liveStore, 92 | equivalenceSet: this.equivalenceSet, 93 | store: this, 94 | superMap: this.superMap, 95 | }; 96 | } 97 | 98 | /** 99 | * Expands the given lookupTypes to include all their equivalent and subclasses. 100 | * This is done in multiple iterations until no new types are found. 101 | * @param lookupTypes The types to look up. Once given, these are assumed to be classes. 102 | */ 103 | public mineForTypes(lookupTypes: string[]): string[] { 104 | if (lookupTypes.length === 0) { 105 | return [rdfs.Resource.value]; 106 | } 107 | 108 | const canonicalTypes: string[] = []; 109 | const lookupTypesExpanded = []; 110 | for (const lookupType of lookupTypes) { 111 | lookupTypesExpanded.push(...this.allEquals(lookupType)); 112 | } 113 | for (const lookupType of lookupTypesExpanded) { 114 | const canon = this.liveStore.getInternalStore().store.primary(lookupType); 115 | 116 | if (!this.processedTypes.includes(canon)) { 117 | for (const vocab of Schema.vocabularies) { 118 | vocab.processType( 119 | canon, 120 | this.getProcessingCtx(), 121 | ); 122 | } 123 | this.processedTypes.push(canon); 124 | } 125 | 126 | if (!canonicalTypes.includes(canon)) { 127 | canonicalTypes.push(canon); 128 | } 129 | } 130 | 131 | const allTypes = canonicalTypes 132 | .reduce( 133 | (a, b) => { 134 | const superSet = this.superMap.get(b); 135 | if (typeof superSet === "undefined") { 136 | return a; 137 | } 138 | 139 | superSet.forEach((s) => { 140 | if (!a.includes(s)) { 141 | a.push(s); 142 | } 143 | }); 144 | 145 | return a; 146 | }, 147 | [...lookupTypes], 148 | ); 149 | 150 | return this.sort(allTypes); 151 | } 152 | 153 | public sort(types: string[]): string[] { 154 | return types.sort((a, b) => { 155 | if (this.isSubclassOf(a, b)) { 156 | return -1; 157 | } else if (this.isSubclassOf(b, a)) { 158 | return 1; 159 | } 160 | 161 | const aDepth = this.superTypeDepth(a); 162 | const bDepth = this.superTypeDepth(b); 163 | if (aDepth < bDepth) { 164 | return 1; 165 | } else if (aDepth > bDepth) { 166 | return -1; 167 | } 168 | 169 | return 0; 170 | }); 171 | } 172 | 173 | /** 174 | * Returns the hierarchical depth of the type, or -1 if unknown. 175 | * @param type the type to check 176 | */ 177 | private superTypeDepth(type: string): number { 178 | const superMap = this.superMap.get(type); 179 | 180 | return superMap ? superMap.size : -1; 181 | } 182 | 183 | private isSubclassOf(resource: string, superClass: string): boolean { 184 | const resourceMap = this.superMap.get(resource); 185 | 186 | if (resourceMap) { 187 | return resourceMap.has(superClass); 188 | } 189 | return false; 190 | } 191 | 192 | private process(record: DataRecord): void { 193 | for (const vocab of Schema.vocabularies) { 194 | for (const [field, values] of Object.entries(record)) { 195 | for (const value of normalizeType(values)) { 196 | vocab.processStatement( 197 | record._id.value, 198 | field, 199 | value, 200 | this.getProcessingCtx(), 201 | ); 202 | } 203 | } 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/TypedRecord.ts: -------------------------------------------------------------------------------- 1 | 2 | // @ts-ignore Used when looking up values. 3 | export class AttributeKey { 4 | private readonly key: string; 5 | 6 | constructor(key: string) { 7 | this.key = key; 8 | } 9 | 10 | public toString(): string { 11 | return this.key; 12 | } 13 | } 14 | 15 | // tslint:disable-next-line:max-classes-per-file 16 | export class TypedRecord implements Map, unknown> { 17 | private records: Map, any> = new Map(); 18 | 19 | public get(key: AttributeKey): T { 20 | return this.records.get(key) as unknown as T; 21 | } 22 | 23 | public get [Symbol.toStringTag](): string { 24 | return this.records[Symbol.toStringTag]; 25 | } 26 | 27 | public get size(): number { 28 | return this.records.size; 29 | } 30 | 31 | public [Symbol.iterator](): IterableIterator<[AttributeKey, unknown]> { 32 | return this.records[Symbol.iterator](); 33 | } 34 | 35 | public clear(): void { 36 | this.records.clear(); 37 | } 38 | 39 | public delete(key: AttributeKey): boolean { 40 | return this.records.delete(key); 41 | } 42 | 43 | public entries(): IterableIterator<[AttributeKey, unknown]> { 44 | return this.records.entries() as unknown as IterableIterator<[AttributeKey, unknown]>; 45 | } 46 | 47 | public forEach( 48 | callbackfn: ( 49 | value: unknown, 50 | key: AttributeKey, 51 | map: Map, unknown>, 52 | ) => void, 53 | thisArg?: any, 54 | ): void { 55 | return this.records.forEach(callbackfn, thisArg); 56 | } 57 | 58 | public has(key: AttributeKey): boolean { 59 | return this.records.has(key); 60 | } 61 | 62 | public keys(): IterableIterator> { 63 | return this.records.keys(); 64 | } 65 | 66 | public set(key: AttributeKey, value: T): this { 67 | this.records.set(key, value); 68 | return this; 69 | } 70 | 71 | public values(): IterableIterator { 72 | return this.records.values(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/__tests__/LinkedRenderStore.spec.ts: -------------------------------------------------------------------------------- 1 | import "jest"; 2 | import "./useFactory"; 3 | 4 | import rdfFactory, { Quadruple } from "@ontologies/core"; 5 | import * as owl from "@ontologies/owl"; 6 | import * as rdf from "@ontologies/rdf"; 7 | import * as schema from "@ontologies/schema"; 8 | import { LinkedRenderStore } from "../LinkedRenderStore"; 9 | 10 | import { getBasicStore } from "../testUtilities"; 11 | 12 | import { example } from "./LinkedRenderStore/fixtures"; 13 | 14 | const defaultGraph = rdfFactory.defaultGraph(); 15 | 16 | describe("LinkedRenderStore", () => { 17 | describe("actions", () => { 18 | it("allows overriding dispach", () => { 19 | const dispatch = jest.fn(); 20 | const lrs = new LinkedRenderStore({ 21 | dispatch, 22 | }); 23 | 24 | expect(lrs.dispatch).toStrictEqual(dispatch); 25 | }); 26 | 27 | it ("prevents premature executions", () => { 28 | const lrs = new LinkedRenderStore(); 29 | 30 | expect(lrs.exec(rdf.type)).rejects.toBeInstanceOf(Error); 31 | }); 32 | }); 33 | 34 | describe("data fetching", () => { 35 | it("allows data reload", async () => { 36 | const apiGetEntity = jest.fn(); 37 | const iri = rdf.type; 38 | // @ts-ignore 39 | const store = getBasicStore({ api: { getEntity: apiGetEntity } }); 40 | 41 | await store.lrs.getEntity(iri, { reload: true }); 42 | 43 | expect(apiGetEntity).toHaveBeenCalledWith( 44 | iri, 45 | { 46 | clearPreviousData: true, 47 | }, 48 | ); 49 | }); 50 | }); 51 | 52 | describe("reasons correctly", () => { 53 | it("combines sameAs declarations", async () => { 54 | const store = getBasicStore(); 55 | 56 | const id = example("sameFirst"); 57 | const idSecond = example("sameSecond"); 58 | const testData: Quadruple[] = [ 59 | [id, rdf.type, schema.CreativeWork, defaultGraph], 60 | [id, schema.text, rdfFactory.literal("text"), defaultGraph], 61 | [id, schema.author, rdfFactory.namedNode("http://example.org/people/0"), defaultGraph], 62 | 63 | [idSecond, rdf.type, schema.CreativeWork, defaultGraph], 64 | [idSecond, schema.name, rdfFactory.literal("other"), defaultGraph], 65 | 66 | [idSecond, owl.sameAs, id, defaultGraph], 67 | ]; 68 | 69 | store.store.addQuads(testData); 70 | const record = store.lrs.getRecord(idSecond)!; 71 | 72 | expect(record[schema.author.value]).toEqual(rdfFactory.namedNode("http://example.org/people/0")); 73 | }); 74 | }); 75 | 76 | describe("#reset", () => { 77 | const store = getBasicStore(); 78 | store.lrs.reset(); 79 | const openStore = store.lrs as any; 80 | 81 | it("reinitialized the store", () => expect(openStore.store).not.toStrictEqual(store.store)); 82 | it("reinitialized the schema", () => expect(openStore.schema === store.schema).toBeFalsy()); 83 | it("reinitialized the mapping", () => expect(openStore.mapping === store.mapping).toBeFalsy()); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /src/__tests__/LinkedRenderStore/components.spec.ts: -------------------------------------------------------------------------------- 1 | import rdfFactory, { NamedNode } from "@ontologies/core"; 2 | import * as rdf from "@ontologies/rdf"; 3 | import * as schema from "@ontologies/schema"; 4 | 5 | import { RENDER_CLASS_NAME } from "../../ComponentStore/ComponentStore"; 6 | import { LinkedRenderStore } from "../../LinkedRenderStore"; 7 | import argu from "../../ontology/argu"; 8 | import { getBasicStore } from "../../testUtilities"; 9 | import { ComponentRegistration, SomeNode } from "../../types"; 10 | import { DEFAULT_TOPOLOGY } from "../../utilities/constants"; 11 | 12 | import { DT, example, RCN } from "./fixtures"; 13 | 14 | describe("LinkedRenderStore", () => { 15 | describe("::registerRenderer", () => { 16 | const func = (): void => undefined; 17 | const type = schema.Thing; 18 | const types = [schema.Thing, schema.Person]; 19 | const prop = schema.name; 20 | const props = [schema.name, schema.text, schema.dateCreated]; 21 | const topology = argu.ns("collection"); 22 | const topologies = [argu.ns("collection"), argu.ns("collection")]; 23 | 24 | function checkRegistration( 25 | r: ComponentRegistration, 26 | c: T, 27 | t: SomeNode, 28 | p: NamedNode, 29 | top: SomeNode, 30 | ): void { 31 | expect(r.component).toEqual(c); 32 | expect(r.type).toEqual(t.value); 33 | expect(r.property).toEqual(p.value); 34 | expect(r.topology).toEqual(top.value); 35 | } 36 | 37 | it("does not register without component", () => { 38 | const defaultMsg = `Undefined component was given for (${type.value}, ${RCN}, ${DT}).`; 39 | try { 40 | LinkedRenderStore.registerRenderer(undefined, type); 41 | expect(true).toBeFalsy(); 42 | } catch (e) { 43 | expect((e as Error).message).toEqual(defaultMsg); 44 | } 45 | }); 46 | 47 | it("registers function type", () => { 48 | const r = LinkedRenderStore.registerRenderer(func, type); 49 | expect(r.length).toEqual(1); 50 | checkRegistration(r[0], func, type, RENDER_CLASS_NAME, DEFAULT_TOPOLOGY); 51 | }); 52 | 53 | it("registers multiple types", () => { 54 | const r = LinkedRenderStore.registerRenderer(func, types); 55 | expect(r.length).toEqual(2); 56 | checkRegistration(r[0], func, types[0], RENDER_CLASS_NAME, DEFAULT_TOPOLOGY); 57 | checkRegistration(r[1], func, types[1], RENDER_CLASS_NAME, DEFAULT_TOPOLOGY); 58 | }); 59 | 60 | it("registers a prop", () => { 61 | const r = LinkedRenderStore.registerRenderer(func, type, prop); 62 | expect(r.length).toEqual(1); 63 | checkRegistration(r[0], func, type, prop, DEFAULT_TOPOLOGY); 64 | }); 65 | 66 | it("registers mutliple props", () => { 67 | const r = LinkedRenderStore.registerRenderer(func, type, props); 68 | expect(r.length).toEqual(3); 69 | checkRegistration(r[0], func, type, props[0], DEFAULT_TOPOLOGY); 70 | checkRegistration(r[1], func, type, props[1], DEFAULT_TOPOLOGY); 71 | checkRegistration(r[2], func, type, props[2], DEFAULT_TOPOLOGY); 72 | }); 73 | 74 | it("registers a topology", () => { 75 | const r = LinkedRenderStore.registerRenderer(func, type, prop, topology); 76 | expect(r.length).toEqual(1); 77 | checkRegistration(r[0], func, type, prop, topology); 78 | }); 79 | 80 | it("registers multiple topologies", () => { 81 | const r = LinkedRenderStore.registerRenderer(func, type, prop, topologies); 82 | expect(r.length).toEqual(2); 83 | checkRegistration(r[0], func, type, prop, topologies[0]); 84 | checkRegistration(r[1], func, type, prop, topologies[1]); 85 | }); 86 | 87 | it("registers combinations", () => { 88 | const r = LinkedRenderStore.registerRenderer(func, types, props, topologies); 89 | expect(r.length).toEqual(12); 90 | }); 91 | }); 92 | 93 | describe("#registerAll", () => { 94 | const reg1 = { 95 | component: (): string => "1", 96 | property: rdfFactory.id(schema.text), 97 | topology: DT, 98 | type: rdfFactory.id(schema.Thing), 99 | } as ComponentRegistration<() => string>; 100 | const reg2 = { 101 | component: (): string => "2", 102 | property: rdfFactory.id(schema.name), 103 | topology: rdfFactory.id(argu.ns("collection")), 104 | type: rdfFactory.id(schema.Person), 105 | } as ComponentRegistration<() => string>; 106 | 107 | it("stores multiple ComponentRegistration objects", () => { 108 | const store = getBasicStore(); 109 | store.lrs.registerAll(reg1, reg2); 110 | expect(store.mapping.publicLookup(reg1.property, reg1.type, reg1.topology)).toEqual(reg1.component); 111 | expect(store.mapping.publicLookup(reg2.property, reg2.type, reg2.topology)).toEqual(reg2.component); 112 | }); 113 | 114 | it("stores ComponentRegistration array", () => { 115 | const store = getBasicStore(); 116 | store.lrs.registerAll([reg1, reg2]); 117 | expect(store.mapping.publicLookup(reg1.property, reg1.type, reg1.topology)).toEqual(reg1.component); 118 | expect(store.mapping.publicLookup(reg2.property, reg2.type, reg2.topology)).toEqual(reg2.component); 119 | }); 120 | 121 | it("stores a single ComponentRegistration object", () => { 122 | const store = getBasicStore(); 123 | store.lrs.registerAll(reg1); 124 | expect(store.mapping.publicLookup(reg1.property, reg1.type, reg1.topology)).toEqual(reg1.component); 125 | expect(store.mapping.publicLookup(reg2.property, reg2.type, reg2.topology)).not.toEqual(reg2.component); 126 | }); 127 | }); 128 | 129 | describe("#resourcePropertyComponent", () => { 130 | const store = getBasicStore(); 131 | const resource = example("test"); 132 | const property = schema.name; 133 | const nameComp = (): undefined => undefined; 134 | 135 | it("returns undefined when no view is registered", () => { 136 | expect(store.lrs.resourcePropertyComponent(resource, property)).toBeUndefined(); 137 | }); 138 | 139 | it("returns the view when one is registered", () => { 140 | store.lrs.registerAll(LinkedRenderStore.registerRenderer(nameComp, schema.Thing, property)); 141 | store.store.add(resource, rdf.type, schema.Thing); 142 | 143 | expect(store.lrs.resourcePropertyComponent(resource, property)).toEqual(nameComp); 144 | }); 145 | 146 | it("returns the view for blank node resources", () => { 147 | const blankResource = rdfFactory.blankNode(); 148 | store.lrs.registerAll(LinkedRenderStore.registerRenderer(nameComp, schema.Thing, property)); 149 | store.store.add(blankResource, rdf.type, schema.Thing); 150 | 151 | expect(store.lrs.resourcePropertyComponent(blankResource, property)).toEqual(nameComp); 152 | }); 153 | }); 154 | 155 | describe("#resourceComponent", () => { 156 | const store = getBasicStore(); 157 | const resource = example("test"); 158 | const thingComp = (): undefined => undefined; 159 | 160 | it("returns undefined when no view is registered", () => { 161 | expect(store.lrs.resourceComponent(resource)).toBeUndefined(); 162 | }); 163 | 164 | it("returns the view when one is registered", () => { 165 | store.lrs.registerAll(LinkedRenderStore.registerRenderer(thingComp, schema.Thing)); 166 | store.store.add(resource, rdf.type, schema.Thing); 167 | 168 | expect(store.lrs.resourceComponent(resource)).toEqual(thingComp); 169 | }); 170 | }); 171 | }); 172 | -------------------------------------------------------------------------------- /src/__tests__/LinkedRenderStore/data.spec.ts: -------------------------------------------------------------------------------- 1 | import rdfFactory, { NamedNode } from "@ontologies/core"; 2 | import * as rdfs from "@ontologies/rdfs"; 3 | import { getBasicStore } from "../../testUtilities"; 4 | import { ResourceQueueItem } from "../../types"; 5 | 6 | import { 7 | creativeWorkStatements, 8 | schemaCW, 9 | schemaT, 10 | thingStatements, 11 | } from "./fixtures"; 12 | 13 | const defaultGraph: NamedNode = rdfFactory.defaultGraph(); 14 | 15 | describe("LinkedRenderStore", () => { 16 | describe("adds new graph items", () => { 17 | it("add a single graph item", () => { 18 | const store = getBasicStore(); 19 | store.lrs.store.addQuads(thingStatements); 20 | expect(store.schema.isInstanceOf(schemaT.value, rdfs.Class.value)).toBeTruthy(); 21 | }); 22 | 23 | it("adds multiple graph items", () => { 24 | const store = getBasicStore(); 25 | store.lrs.store.addQuads(thingStatements.concat(creativeWorkStatements)); 26 | expect(store.schema.isInstanceOf(schemaT.value, rdfs.Class.value)).toBeTruthy(); 27 | expect(store.schema.isInstanceOf(schemaCW.value, rdfs.Class.value)).toBeTruthy(); 28 | }); 29 | }); 30 | 31 | describe("#getResourceProperties", () => { 32 | const store = getBasicStore(); 33 | store.lrs.store.addQuads(thingStatements); 34 | 35 | it("returns empty data for empty subject", () => { 36 | const res = store.lrs.getResourceProperties(undefined, rdfs.label); 37 | expect(res).toEqual([]); 38 | }); 39 | 40 | it("returns empty data for empty property", () => { 41 | const res = store.lrs.getResourceProperties(schemaT, undefined); 42 | expect(res).toEqual([]); 43 | }); 44 | 45 | it("returns data when available", () => { 46 | const res = store.lrs.getResourceProperties(schemaT, rdfs.label); 47 | expect(res).toEqual([rdfFactory.literal("Thing.")]); 48 | }); 49 | }); 50 | 51 | describe("#getResourceProperty", () => { 52 | const store = getBasicStore(); 53 | store.lrs.store.addQuads(thingStatements); 54 | 55 | it("returns empty data for empty subject", () => { 56 | const res = store.lrs.getResourceProperty(undefined, rdfs.label); 57 | expect(res).toEqual(undefined); 58 | }); 59 | 60 | it("returns empty data for empty property", () => { 61 | const res = store.lrs.getResourceProperty(schemaT, undefined); 62 | expect(res).toEqual(undefined); 63 | }); 64 | 65 | it("returns data when available", () => { 66 | const res = store.lrs.getResourceProperty(schemaT, rdfs.label); 67 | expect(res).toEqual(rdfFactory.literal("Thing.")); 68 | }); 69 | }); 70 | 71 | describe("#getResourcePropertyRaw", () => { 72 | const store = getBasicStore(); 73 | store.lrs.store.addQuads(thingStatements); 74 | 75 | it("returns empty data for empty subject", () => { 76 | const res = store.lrs.getResourcePropertyRaw(undefined, rdfs.label); 77 | expect(res).toEqual([]); 78 | }); 79 | 80 | it("returns empty data for empty property", () => { 81 | const res = store.lrs.getResourcePropertyRaw(schemaT, undefined); 82 | expect(res).toEqual([]); 83 | }); 84 | 85 | it("returns data when available", () => { 86 | const res = store.lrs.getResourcePropertyRaw(schemaT, rdfs.label); 87 | expect(res).toEqual([[schemaT, rdfs.label, rdfFactory.literal("Thing."), defaultGraph]]); 88 | }); 89 | }); 90 | 91 | describe("#removeResource", () => { 92 | it("resolves after removal", async () => { 93 | const store = getBasicStore(); 94 | store.store.addQuads([ 95 | ...thingStatements, 96 | ...creativeWorkStatements, 97 | ]); 98 | store.store.flush(); 99 | const res = await store.lrs.removeResource(schemaT); 100 | 101 | expect(res).toBeUndefined(); 102 | }); 103 | 104 | it("removes the resource", async () => { 105 | const store = getBasicStore(); 106 | store.store.addQuads([ 107 | ...thingStatements, 108 | ...creativeWorkStatements, 109 | ]); 110 | store.store.flush(); 111 | await store.lrs.removeResource(schemaT); 112 | 113 | expect(store.lrs.tryEntity(schemaT)).toHaveLength(0); 114 | }); 115 | 116 | it("calls the subscriber", async () => { 117 | const store = getBasicStore(); 118 | const sub = jest.fn(); 119 | store.lrs.subscribe({ 120 | callback: sub, 121 | markedForDelete: false, 122 | subjectFilter: [schemaT.value], 123 | }); 124 | store.store.addQuads([ 125 | ...thingStatements, 126 | ...creativeWorkStatements, 127 | ]); 128 | store.store.flush(); 129 | await store.lrs.removeResource(schemaT, true); 130 | 131 | expect(sub).toHaveBeenCalledTimes(1); 132 | }); 133 | }); 134 | 135 | describe("#processResourceQueue", () => { 136 | it("swaps the queue on processing start", async () => { 137 | const store = getBasicStore(); 138 | const queue: ResourceQueueItem[] = [ 139 | [schemaT, { reload: false }], 140 | ]; 141 | 142 | (store.lrs as any).resourceQueue = queue; 143 | (store.lrs as any).resourceQueueHandle = 0; 144 | await (store.lrs as any).processResourceQueue(); 145 | 146 | expect((store.lrs as any).resourceQueueHandle).toBeUndefined(); 147 | expect((store.lrs as any).resourceQueue).not.toBe(queue); 148 | }); 149 | }); 150 | }); 151 | -------------------------------------------------------------------------------- /src/__tests__/LinkedRenderStore/delta.spec.ts: -------------------------------------------------------------------------------- 1 | import "../useFactory"; 2 | 3 | import rdfFactory, { QuadPosition, Quadruple } from "@ontologies/core"; 4 | import * as rdfs from "@ontologies/rdfs"; 5 | import { LinkedRenderStore } from "../../LinkedRenderStore"; 6 | 7 | import { getBasicStore } from "../../testUtilities"; 8 | import { DeltaProcessor } from "../../types"; 9 | 10 | import { ex, ld, schemaT } from "./fixtures"; 11 | 12 | describe("LinkedRenderStore", () => { 13 | describe("#addDeltaProcessor", () => { 14 | it ("adds the processor", () => { 15 | const processor = jest.fn(); 16 | const { lrs } = getBasicStore(); 17 | 18 | lrs.addDeltaProcessor(processor as unknown as DeltaProcessor); 19 | expect(lrs.deltaProcessors).toContain(processor); 20 | }); 21 | }); 22 | 23 | describe("#processDelta", () => { 24 | const getLabel = (lrs: LinkedRenderStore): string | undefined => lrs 25 | .tryEntity(schemaT) 26 | .find((q) => q[QuadPosition.predicate] === rdfs.label) 27 | ?.[QuadPosition.object] 28 | ?.value; 29 | 30 | it("processes quad delta", () => { 31 | const { lrs } = getBasicStore(); 32 | 33 | lrs.processDelta([ 34 | [schemaT, rdfs.label, rdfFactory.literal("test"), ld.replace], 35 | ], true); 36 | 37 | expect(getLabel(lrs)).toEqual("test"); 38 | }); 39 | 40 | it("processes quadruple delta", () => { 41 | const { lrs } = getBasicStore(); 42 | 43 | lrs.processDelta([ 44 | [schemaT, rdfs.label, rdfFactory.literal("test"), ld.replace], 45 | ], true); 46 | 47 | expect(getLabel(lrs)).toEqual("test"); 48 | }); 49 | }); 50 | 51 | describe("#queueDelta", () => { 52 | const quadDelta = [ 53 | [ex("1"), ex("p"), ex("2"), ld.add], 54 | [ex("1"), ex("t"), rdfFactory.literal("Test"), ld.add], 55 | [ex("2"), ex("t"), rdfFactory.literal("Value"), ld.add], 56 | ] as Quadruple[]; 57 | 58 | it("queues an empty delta", async () => { 59 | const store = getBasicStore(); 60 | 61 | await store.lrs.queueDelta([undefined]); 62 | }); 63 | 64 | it("queues a quadruple delta", async () => { 65 | const processor = { 66 | flush: jest.fn(), 67 | processDelta: jest.fn(), 68 | queueDelta: jest.fn(), 69 | }; 70 | const store = getBasicStore(); 71 | store.lrs.deltaProcessors.push(processor); 72 | 73 | await store.lrs.queueDelta(quadDelta); 74 | 75 | expect(processor.queueDelta).toHaveBeenCalledTimes(1); 76 | expect(processor.queueDelta).toHaveBeenCalledWith(quadDelta); 77 | }); 78 | 79 | it("queues a statement delta", async () => { 80 | const processor = { 81 | flush: jest.fn(), 82 | processDelta: jest.fn(), 83 | queueDelta: jest.fn(), 84 | }; 85 | const store = getBasicStore(); 86 | store.lrs.deltaProcessors.push(processor); 87 | 88 | const delta: Quadruple[] = [ 89 | [ex("1"), ex("p"), ex("2"), ld.add], 90 | [ex("1"), ex("t"), rdfFactory.literal("Test"), ld.add], 91 | [ex("2"), ex("t"), rdfFactory.literal("Value"), ld.add], 92 | ]; 93 | await store.lrs.queueDelta(delta); 94 | 95 | expect(processor.queueDelta).toHaveBeenCalledTimes(1); 96 | expect(processor.queueDelta).toHaveBeenCalledWith(quadDelta); 97 | }); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /src/__tests__/LinkedRenderStore/dig.spec.ts: -------------------------------------------------------------------------------- 1 | import "jest"; 2 | import "../useFactory"; 3 | 4 | import rdfFactory, { NamedNode } from "@ontologies/core"; 5 | 6 | import { getBasicStore } from "../../testUtilities"; 7 | 8 | import { ex } from "./fixtures"; 9 | 10 | const defaultGraph: NamedNode = rdfFactory.defaultGraph(); 11 | 12 | describe("LinkedRenderStore", () => { 13 | describe("#dig", () => { 14 | const store = getBasicStore(); 15 | const start = ex("1"); 16 | const bn = rdfFactory.blankNode(); 17 | store.store.addQuads([ 18 | [start, ex("oneToOne"), ex("1.1"), defaultGraph], 19 | 20 | [start, ex("oneToOneLiteral"), ex("1.2"), defaultGraph], 21 | 22 | [start, ex("oneToOneBN"), bn, defaultGraph], 23 | 24 | [start, ex("oneToOneMissing"), ex("1.3"), defaultGraph], 25 | 26 | [start, ex("oneToMany"), ex("1.4"), defaultGraph], 27 | [start, ex("oneToMany"), ex("1.5"), defaultGraph], 28 | 29 | [start, ex("oneToManyHoley"), ex("1.6"), defaultGraph], 30 | [start, ex("oneToManyHoley"), ex("1.7"), defaultGraph], 31 | [start, ex("oneToManyHoley"), ex("1.8"), defaultGraph], 32 | 33 | [ex("1.2"), ex("p"), rdfFactory.literal("value", "en"), defaultGraph], 34 | 35 | [bn, ex("p"), rdfFactory.literal("test"), defaultGraph], 36 | 37 | [ex("1.2"), ex("p"), rdfFactory.literal("value", "nl"), defaultGraph], 38 | 39 | [ex("1.2"), ex("p"), ex("2.3"), defaultGraph], 40 | 41 | [ex("1.4"), ex("p"), ex("2.4"), defaultGraph], 42 | [ex("1.5"), ex("p"), ex("2.5"), defaultGraph], 43 | 44 | [ex("1.6"), ex("p"), ex("2.6"), defaultGraph], 45 | [ex("1.7"), ex("p"), ex("2.7"), defaultGraph], 46 | [ex("1.8"), ex("p"), ex("2.8"), defaultGraph], 47 | 48 | [ex("2.6"), ex("q"), ex("3.6"), defaultGraph], 49 | [ex("2.7"), ex("other"), ex("3.7"), defaultGraph], 50 | [ex("2.8"), ex("q"), ex("3.8"), defaultGraph], 51 | ]); 52 | store.store.flush(); 53 | 54 | it("is empty without path", () => expect(store.lrs.dig(start, [])).toEqual([])); 55 | 56 | it("resolves oneToOne", () => expect(store.lrs.dig(start, [ex("oneToOne")])).toEqual([ex("1.1")])); 57 | 58 | it("resolves literals through oneToOne", () => { 59 | expect(store.lrs.dig(start, [ex("oneToOneLiteral"), ex("p")])) 60 | .toEqual([ 61 | rdfFactory.literal("value", "en"), 62 | rdfFactory.literal("value", "nl"), 63 | ex("2.3"), 64 | ]); 65 | }); 66 | 67 | it("resolves blank nodes through oneToOne", () => { 68 | expect(store.lrs.dig(start, [ex("oneToOneBN"), ex("p")])) 69 | .toEqual([rdfFactory.literal("test")]); 70 | }); 71 | 72 | it("resolves oneToMany", () => { 73 | expect(store.lrs.dig(start, [ex("oneToMany")])) 74 | .toEqual([ex("1.4"), ex("1.5")]); 75 | }); 76 | 77 | it("resolves values through oneToMany", () => { 78 | expect(store.lrs.dig(start, [ex("oneToMany"), ex("p")])) 79 | .toEqual([ex("2.4"), ex("2.5")]); 80 | }); 81 | 82 | it("resolves values through holey oneToMany", () => { 83 | const [terms, subjects] = store.lrs.digDeeper(start, [ex("oneToManyHoley"), ex("p"), ex("q")]); 84 | 85 | expect(terms).toEqual([ 86 | [ex("2.6"), ex("q"), ex("3.6"), defaultGraph], 87 | [ex("2.8"), ex("q"), ex("3.8"), defaultGraph], 88 | ]); 89 | expect(subjects).toEqual([ 90 | start, 91 | ex("1.6"), 92 | ex("1.7"), 93 | ex("1.8"), 94 | ex("2.6"), 95 | ex("2.7"), 96 | ex("2.8"), 97 | ]); 98 | }); 99 | 100 | it("resolves empty through holey oneToMany without end value", () => { 101 | expect(store.lrs.dig(start, [ex("oneToManyHoley"), ex("p"), ex("nonexistent")])) 102 | .toEqual([]); 103 | }); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /src/__tests__/LinkedRenderStore/findSubject.spec.ts: -------------------------------------------------------------------------------- 1 | import rdfFactory, { NamedNode, Quadruple } from "@ontologies/core"; 2 | import * as rdf from "@ontologies/rdf"; 3 | import * as schema from "@ontologies/schema"; 4 | 5 | import { getBasicStore } from "../../testUtilities"; 6 | 7 | import { ex } from "./fixtures"; 8 | 9 | const defaultGraph: NamedNode = rdfFactory.defaultGraph(); 10 | 11 | describe("LinkedRenderStore", () => { 12 | describe("#findSubject", () => { 13 | const store = getBasicStore(); 14 | const bill = rdfFactory.literal("Bill"); 15 | const bookTitle = rdfFactory.literal("His first work"); 16 | const alternativeTitle = rdfFactory.literal("Some alternative title"); 17 | const testData: Quadruple[] = [ 18 | [ex("1"), rdf.type, ex("Organization"), defaultGraph], 19 | [ex("1"), schema.name, rdfFactory.literal("Some org"), defaultGraph], 20 | [ex("1"), schema.employee, ex("2"), defaultGraph], 21 | 22 | [ex("2"), rdf.type, schema.Person, defaultGraph], 23 | [ex("2"), schema.name, bill, defaultGraph], 24 | [ex("2"), schema.author, ex("3"), defaultGraph], 25 | [ex("2"), schema.author, ex("4"), defaultGraph], 26 | 27 | [ex("3"), rdf.type, schema.Book, defaultGraph], 28 | [ex("3"), schema.name, bookTitle, defaultGraph], 29 | [ex("3"), schema.name, alternativeTitle, defaultGraph], 30 | [ex("3"), schema.numberOfPages, rdfFactory.literal(75), defaultGraph], 31 | 32 | [ex("4"), rdf.type, schema.Book, defaultGraph], 33 | [ex("4"), schema.name, rdfFactory.literal("His second work"), defaultGraph], 34 | [ex("4"), schema.numberOfPages, rdfFactory.literal(475), defaultGraph], 35 | [ex("4"), schema.bookEdition, rdfFactory.literal("1st"), defaultGraph], 36 | ]; 37 | store.store.addQuads(testData); 38 | 39 | it("resolves an empty path to nothing", () => { 40 | const answer = store.lrs.findSubject(ex("1"), [], ex("2")); 41 | expect(answer).toHaveLength(0); 42 | }); 43 | 44 | it("resolves unknown subject to nothing", () => { 45 | const answer = store.lrs.findSubject(ex("x"), [schema.name], bill); 46 | expect(answer).toHaveLength(0); 47 | }); 48 | 49 | it("resolves first order matches", () => { 50 | const answer = store.lrs.findSubject(ex("2"), [schema.name], bill); 51 | expect(answer).toEqual([ex("2")]); 52 | }); 53 | 54 | it("resolves second order matches", () => { 55 | const answer = store.lrs.findSubject( 56 | ex("1"), 57 | [schema.employee, schema.name], 58 | rdfFactory.literal("Bill"), 59 | ); 60 | expect(answer).toEqual([ex("2")]); 61 | }); 62 | 63 | it("resolves third order matches", () => { 64 | const answer = store.lrs.findSubject( 65 | ex("1"), 66 | [schema.employee, schema.author, schema.name], 67 | bookTitle, 68 | ); 69 | expect(answer).toEqual([ex("3")]); 70 | }); 71 | 72 | it("resolves third order array matches", () => { 73 | const answer = store.lrs.findSubject( 74 | ex("1"), 75 | [schema.employee, schema.author, schema.name], 76 | [bill, alternativeTitle], 77 | ); 78 | expect(answer).toEqual([ex("3")]); 79 | }); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /src/__tests__/LinkedRenderStore/fixtures.ts: -------------------------------------------------------------------------------- 1 | import rdfFactory, { createNS, NamedNode, Quadruple } from "@ontologies/core"; 2 | import * as dcterms from "@ontologies/dcterms"; 3 | import * as rdf from "@ontologies/rdf"; 4 | import * as rdfs from "@ontologies/rdfs"; 5 | import * as schema from "@ontologies/schema"; 6 | 7 | import { RENDER_CLASS_NAME } from "../../ComponentStore/ComponentStore"; 8 | import { DEFAULT_TOPOLOGY } from "../../utilities/constants"; 9 | 10 | const defaultGraph: NamedNode = rdfFactory.defaultGraph(); 11 | 12 | export const DT = DEFAULT_TOPOLOGY.value; 13 | export const RCN = RENDER_CLASS_NAME.value; 14 | 15 | export const schemaT = schema.Thing; 16 | export const thingStatements: Quadruple[] = [ 17 | [schemaT, rdf.type, rdfs.Class, defaultGraph], 18 | [schemaT, rdfs.comment, rdfFactory.literal("The most generic type of item."), defaultGraph], 19 | [schemaT, rdfs.label, rdfFactory.literal("Thing."), defaultGraph], 20 | ]; 21 | 22 | export const schemaCW = schema.CreativeWork; 23 | export const creativeWorkStatements: Quadruple[] = [ 24 | [schemaCW, rdf.type, rdfs.Class, defaultGraph], 25 | [schemaCW, rdfs.label, rdfFactory.literal("CreativeWork"), defaultGraph], 26 | [schemaCW, rdfs.subClassOf, schemaT, defaultGraph], 27 | [ 28 | schemaCW, 29 | dcterms.source, 30 | rdfFactory.namedNode("http://www.w3.org/wiki/WebSchemas/SchemaDotOrgSources#source_rNews"), 31 | defaultGraph, 32 | ], 33 | [ 34 | schemaCW, 35 | rdfs.comment, 36 | rdfFactory.literal("The most generic kind of creative work, including books, movies, [...], etc."), 37 | defaultGraph, 38 | ], 39 | ]; 40 | 41 | export const example = createNS("http://example.com/"); 42 | export const ex = createNS("http://example.com/ns#"); 43 | export const ldNS = createNS("http://purl.org/linked-delta/"); 44 | export const ld = { 45 | add: ldNS("add"), 46 | purge: ldNS("purge"), 47 | remove: ldNS("remove"), 48 | replace: ldNS("replace"), 49 | slice: ldNS("slice"), 50 | supplant: ldNS("supplant"), 51 | }; 52 | -------------------------------------------------------------------------------- /src/__tests__/LinkedRenderStore/render.spec.ts: -------------------------------------------------------------------------------- 1 | import * as rdfs from "@ontologies/rdfs"; 2 | import * as schema from "@ontologies/schema"; 3 | 4 | import { LinkedRenderStore } from "../../LinkedRenderStore"; 5 | import { getBasicStore } from "../../testUtilities"; 6 | 7 | import { DT, RCN } from "./fixtures"; 8 | 9 | describe("LinkedRenderStore", () => { 10 | describe("type renderer", () => { 11 | it("registers with full notation", () => { 12 | const store = getBasicStore(); 13 | const comp = (): string => "a"; 14 | store.lrs.registerAll(LinkedRenderStore.registerRenderer(comp, schema.Thing)); 15 | const thingComp = store.mapping.getRenderComponent( 16 | [schema.Thing.value], 17 | [RCN], 18 | DT, 19 | rdfs.Resource.value, 20 | ); 21 | expect(thingComp).toEqual(comp); 22 | }); 23 | 24 | it("registers with multiple types", () => { 25 | const store = getBasicStore(); 26 | const comp = (): string => "a"; 27 | store.lrs.registerAll(LinkedRenderStore.registerRenderer( 28 | comp, 29 | [schema.Thing, schema.CreativeWork], 30 | )); 31 | const thingComp = store.mapping.getRenderComponent( 32 | [schema.Thing.value], 33 | [RCN], 34 | DT, 35 | rdfs.Resource.value, 36 | ); 37 | expect(thingComp).toEqual(comp); 38 | const cwComp = store.mapping.getRenderComponent( 39 | [schema.CreativeWork.value], 40 | [RCN], 41 | DT, 42 | rdfs.Resource.value, 43 | ); 44 | expect(cwComp).toEqual(comp); 45 | }); 46 | }); 47 | 48 | describe("property renderer", () => { 49 | it("registers with full notation", () => { 50 | const store = getBasicStore(); 51 | const ident = (): string => "a"; 52 | store.lrs.registerAll(LinkedRenderStore.registerRenderer(ident, schema.Thing, schema.name)); 53 | const nameComp = store.mapping.getRenderComponent( 54 | [schema.Thing.value], 55 | [schema.name.value], 56 | DT, 57 | rdfs.Resource.value, 58 | ); 59 | expect(nameComp).toEqual(ident); 60 | }); 61 | 62 | it("registers multiple", () => { 63 | const store = getBasicStore(); 64 | const ident = (): string => "a"; 65 | store.lrs.registerAll(LinkedRenderStore.registerRenderer( 66 | ident, 67 | schema.Thing, 68 | [schema.name, rdfs.label], 69 | )); 70 | [schema.name, rdfs.label].forEach((prop) => { 71 | const nameComp = store.mapping.getRenderComponent( 72 | [schema.Thing.value], 73 | [prop.value], 74 | DT, 75 | rdfs.Resource.value, 76 | ); 77 | expect(nameComp).toEqual(ident); 78 | expect(nameComp).not.toEqual((): string => "b"); 79 | }); 80 | }); 81 | }); 82 | 83 | describe("returns renderer for", () => { 84 | it("class renders", () => { 85 | const LRS = new LinkedRenderStore(); 86 | expect(LRS.getComponentForType(schema.Thing)).toBeNull(); 87 | const ident = (a: string): string => a; 88 | const registrations = LinkedRenderStore.registerRenderer(ident, schema.Thing); 89 | LRS.registerAll(registrations); 90 | const klass = LRS.getComponentForType(schema.Thing); 91 | expect(klass).toEqual(ident); 92 | expect(klass).not.toEqual((a: string): string => a); 93 | }); 94 | 95 | it("property renders", () => { 96 | const LRS = new LinkedRenderStore(); 97 | const ident = (a: string): string => a; 98 | const registrations = LinkedRenderStore.registerRenderer( 99 | ident, 100 | schema.Thing, 101 | schema.name, 102 | ); 103 | LRS.registerAll(registrations); 104 | const klass = LRS.getComponentForProperty(schema.Thing, schema.name); 105 | expect(klass).toEqual(ident); 106 | expect(klass).not.toEqual((a: string): string => a); 107 | }); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /src/__tests__/LinkedRenderStore/subscriptions.spec.ts: -------------------------------------------------------------------------------- 1 | import rdfFactory from "@ontologies/core"; 2 | import * as schema from "@ontologies/schema"; 3 | 4 | import { getBasicStore } from "../../testUtilities"; 5 | import { SubscriptionRegistrationBase } from "../../types"; 6 | 7 | import { schemaCW, schemaT } from "./fixtures"; 8 | 9 | jest.useFakeTimers("legacy"); 10 | 11 | describe("LinkedRenderStore", () => { 12 | describe("subscriptions", () => { 13 | describe("in bulk", () => { 14 | it("registers the subscription", async () => { 15 | const store = getBasicStore(); 16 | const callback = jest.fn(); 17 | const reg = { 18 | callback, 19 | markedForDelete: false, 20 | }; 21 | 22 | store.lrs.subscribe(reg); 23 | expect(callback).not.toHaveBeenCalled(); 24 | }); 25 | 26 | it("unregisters the subscription", async () => { 27 | const store = getBasicStore(); 28 | const callback = jest.fn(); 29 | const reg = { 30 | callback, 31 | markedForDelete: false, 32 | }; 33 | 34 | const unregister = store.lrs.subscribe(reg); 35 | expect(reg.markedForDelete).toBeFalsy(); 36 | unregister(); 37 | expect(reg.markedForDelete).toBeTruthy(); 38 | }); 39 | 40 | it("calls the subscription", async () => { 41 | const store = getBasicStore(); 42 | const callback = jest.fn(); 43 | const reg = { 44 | callback, 45 | markedForDelete: false, 46 | }; 47 | 48 | store.lrs.subscribe(reg); 49 | expect(callback).not.toHaveBeenCalled(); 50 | 51 | await store.forceBroadcast(); 52 | expect(callback).toHaveBeenCalled(); 53 | }); 54 | }); 55 | 56 | describe("subject filtered", () => { 57 | it("registers the subscription", async () => { 58 | const store = getBasicStore(); 59 | const callback = jest.fn(); 60 | const reg = { 61 | callback, 62 | markedForDelete: false, 63 | subjectFilter: [schemaT.value], 64 | }; 65 | 66 | store.lrs.subscribe(reg); 67 | expect(callback).not.toHaveBeenCalled(); 68 | }); 69 | 70 | it("unregisters the subscription", async () => { 71 | const store = getBasicStore(); 72 | (store.lrs as any).cleanupTimout = 0; 73 | const callback = jest.fn(); 74 | const reg = { 75 | callback, 76 | markedForDelete: false, 77 | subjectFilter: [schemaT.value], 78 | }; 79 | 80 | const unregister = store.lrs.subscribe(reg); 81 | expect(reg.markedForDelete).toBeFalsy(); 82 | expect(setTimeout).toHaveBeenCalledTimes(0); 83 | unregister(); 84 | expect(setTimeout).toHaveBeenCalledTimes(1); 85 | expect(reg.markedForDelete).toBeTruthy(); 86 | 87 | expect((store.lrs as any).subjectSubscriptions[schemaT.value]).toContain(reg); 88 | jest.runAllTimers(); 89 | expect((store.lrs as any).subjectSubscriptions[schemaT.value]).not.toContain(reg); 90 | }); 91 | 92 | it("skips the subscription when irrelevant", async () => { 93 | const store = getBasicStore(); 94 | const callback = jest.fn(); 95 | const reg = { 96 | callback, 97 | markedForDelete: false, 98 | subjectFilter: [schemaT.value], 99 | }; 100 | 101 | store.lrs.subscribe(reg); 102 | expect(callback).not.toHaveBeenCalled(); 103 | 104 | await store.forceBroadcast(); 105 | expect(callback).not.toHaveBeenCalled(); 106 | }); 107 | 108 | it("calls the subscription when relevant", async () => { 109 | jest.useRealTimers(); 110 | 111 | const store = getBasicStore(); 112 | await store.forceBroadcast(); 113 | const callback = jest.fn(); 114 | const reg: SubscriptionRegistrationBase = { 115 | callback, 116 | markedForDelete: false, 117 | subjectFilter: [schemaT.value], 118 | }; 119 | 120 | store.lrs.subscribe(reg); 121 | expect(callback).not.toHaveBeenCalled(); 122 | 123 | store.store.add(schemaT, schema.name, rdfFactory.literal("Thing")); 124 | await store.forceBroadcast(); 125 | 126 | expect(callback).toHaveBeenCalledTimes(1); 127 | expect(callback.mock.calls[0][0]).toEqual([ 128 | schemaT.value, 129 | ]); 130 | expect(callback.mock.calls[0][1]).toBeGreaterThanOrEqual(reg.subscribedAt!); 131 | expect(callback.mock.calls[0][1]).toBeLessThanOrEqual(Date.now()); 132 | }); 133 | 134 | it("calls the subscription once at most for multiple matches", async () => { 135 | jest.useRealTimers(); 136 | 137 | const store = getBasicStore(); 138 | await store.forceBroadcast(); 139 | const callback = jest.fn(); 140 | const reg: SubscriptionRegistrationBase = { 141 | callback, 142 | markedForDelete: false, 143 | subjectFilter: [schemaT.value], 144 | }; 145 | 146 | store.lrs.subscribe(reg); 147 | expect(callback).not.toHaveBeenCalled(); 148 | 149 | store.store.add(schemaT, schema.name, rdfFactory.literal("Thing")); 150 | store.store.add(schemaCW, schema.name, rdfFactory.literal("CreativeWork")); 151 | await store.forceBroadcast(); 152 | 153 | expect(callback).toHaveBeenCalledTimes(1); 154 | expect(callback.mock.calls[0][0]).toEqual([ 155 | schemaT.value, 156 | schemaCW.value, 157 | ]); 158 | expect(callback.mock.calls[0][1]).toBeGreaterThanOrEqual(reg.subscribedAt!); 159 | expect(callback.mock.calls[0][1]).toBeLessThanOrEqual(Date.now()); 160 | }); 161 | }); 162 | }); 163 | }); 164 | -------------------------------------------------------------------------------- /src/__tests__/ProcessBroadcast.spec.ts: -------------------------------------------------------------------------------- 1 | import "./useFactory"; 2 | 3 | import rdfFactory, { QuadPosition, Quadruple } from "@ontologies/core"; 4 | import * as rdfx from "@ontologies/rdf"; 5 | import * as rdfs from "@ontologies/rdfs"; 6 | import * as schema from "@ontologies/schema"; 7 | import "jest"; 8 | 9 | import ex from "../ontology/ex"; 10 | import example from "../ontology/example"; 11 | import { ProcessBroadcast, ProcessBroadcastOpts } from "../ProcessBroadcast"; 12 | import { SubscriptionRegistrationBase } from "../types"; 13 | 14 | const schemaT = schema.Thing; 15 | // const resource1 = ex.ns("1"); 16 | const resource2 = ex.ns("2"); 17 | const resource3 = ex.ns("3"); 18 | const resource4 = ex.ns("4"); 19 | const resource5 = ex.ns("5"); 20 | const resource6 = ex.ns("6"); 21 | 22 | const mixedWork: Quadruple[] = [ 23 | [resource5, ex.ns("prop"), ex.ns("unknown"), example.ns("why")], 24 | [schemaT, rdfx.type, rdfs.Class, example.ns("why")], 25 | [schemaT, rdfs.label, rdfFactory.literal("A class"), example.ns("why")], 26 | [resource2, schema.name, rdfFactory.literal("resource 1"), example.ns("why")], 27 | [resource2, schema.name, rdfFactory.literal("resource 2"), example.ns("why")], 28 | [resource3, rdfs.label, rdfFactory.literal("D. Adams"), example.ns("why")], 29 | [resource4, schema.name, rdfFactory.literal("Resource Name"), example.ns("why")], 30 | [resource4, schema.text, rdfFactory.literal("Resource text"), example.ns("why")], 31 | [resource4, schema.author, resource3, example.ns("why")], 32 | [ 33 | resource6, 34 | schema.text, 35 | rdfFactory.literal("Should contain only deleted regs"), 36 | example.ns("why"), 37 | ], 38 | ]; 39 | 40 | const getOpts = ( 41 | work: Quadruple[] = [], 42 | bulkSubscriptions: Array> = [], 43 | subjectSubscriptions: Array> = [], 44 | ): ProcessBroadcastOpts => ({ 45 | bulkSubscriptions, 46 | changedSubjects: work.reduce( 47 | (acc, cur) => acc.includes(cur[QuadPosition.subject].value) 48 | ? acc 49 | : acc.concat(cur[QuadPosition.subject].value), 50 | [] as string[], 51 | ), 52 | subjectSubscriptions, 53 | timeout: 10, 54 | }); 55 | 56 | describe("ProcessBroadcast", () => { 57 | describe("without subscribers", () => { 58 | describe("and no work", () => { 59 | const processor = new ProcessBroadcast(getOpts()); 60 | 61 | it("is done", () => expect(processor.done()).toBeTruthy()); 62 | }); 63 | 64 | describe("and work", () => { 65 | const processor = new ProcessBroadcast(getOpts([ 66 | [schemaT, rdfx.type, rdfs.Class, example.ns("why")], 67 | ])); 68 | 69 | it("is done", () => expect(processor.done()).toBeTruthy()); 70 | }); 71 | }); 72 | 73 | describe("with bulk subject combination", () => { 74 | describe("and work", () => { 75 | const bulk1 = jest.fn(); 76 | const bulk2 = jest.fn(); 77 | 78 | const st = jest.fn(); 79 | const stb = jest.fn(); 80 | const r1 = jest.fn(); 81 | const r2 = jest.fn(); 82 | const r4a = jest.fn(); 83 | const r4b = jest.fn(); 84 | const r5 = jest.fn(); 85 | const r6 = jest.fn(); 86 | 87 | const processor = new ProcessBroadcast(getOpts( 88 | mixedWork, 89 | [ 90 | { callback: bulk1, markedForDelete: false }, 91 | { callback: bulk2, markedForDelete: false }, 92 | ], 93 | [ 94 | // schemaT 95 | { callback: st, markedForDelete: false }, 96 | { callback: stb, markedForDelete: false }, 97 | // resource1 98 | { callback: r1, markedForDelete: false }, 99 | // resource2 100 | { callback: r2, markedForDelete: false }, 101 | // resource4 102 | { callback: r4a, markedForDelete: false }, 103 | { callback: r4b, markedForDelete: false }, 104 | // resource5 105 | { callback: r5, markedForDelete: false }, 106 | // resource6 107 | { callback: r6, markedForDelete: true }, 108 | ], 109 | )); 110 | 111 | // it("has no bulk processors", () => expect(processor.bulkLength).toBe(2)); 112 | // it("has subject processors", () => expect(processor.subjectLength).toBe(6)); 113 | it("is done", () => expect(processor.done()).toBeFalsy()); 114 | it("skips the processors on setup", () => { 115 | expect(bulk1).not.toHaveBeenCalled(); 116 | expect(bulk2).not.toHaveBeenCalled(); 117 | 118 | expect(st).not.toHaveBeenCalled(); 119 | expect(stb).not.toHaveBeenCalled(); 120 | expect(r1).not.toHaveBeenCalled(); 121 | expect(r2).not.toHaveBeenCalled(); 122 | expect(r4a).not.toHaveBeenCalled(); 123 | expect(r4b).not.toHaveBeenCalled(); 124 | expect(r5).not.toHaveBeenCalled(); 125 | expect(r6).not.toHaveBeenCalled(); 126 | }); 127 | 128 | it("calls the processors on run", async () => { 129 | await processor.run(); 130 | 131 | expect(bulk1).toHaveBeenCalledTimes(1); 132 | expect(bulk2).toHaveBeenCalledTimes(1); 133 | 134 | expect(st).toHaveBeenCalledTimes(1); 135 | expect(stb).toHaveBeenCalledTimes(1); 136 | expect(r2).toHaveBeenCalledTimes(1); 137 | expect(r4a).toHaveBeenCalledTimes(1); 138 | expect(r4b).toHaveBeenCalledTimes(1); 139 | expect(r5).toHaveBeenCalledTimes(1); 140 | }); 141 | 142 | it("is done after run", async () => { 143 | await processor.run(); 144 | expect(processor.done()).toBeTruthy(); 145 | }); 146 | }); 147 | }); 148 | }); 149 | -------------------------------------------------------------------------------- /src/__tests__/Schema.spec.ts: -------------------------------------------------------------------------------- 1 | import "./useFactory"; 2 | 3 | import rdfFactory, { NamedNode } from "@ontologies/core"; 4 | import * as rdf from "@ontologies/rdf"; 5 | import * as rdfs from "@ontologies/rdfs"; 6 | import * as schemaNS from "@ontologies/schema"; 7 | import "jest"; 8 | 9 | import ex from "../ontology/ex"; 10 | import example from "../ontology/example"; 11 | import { RDFStore } from "../RDFStore"; 12 | import { Schema } from "../Schema"; 13 | 14 | const defaultGraph: NamedNode = rdfFactory.defaultGraph(); 15 | // const resource1: Quadruple[] = [ 16 | // [example.ns("5"), rdf.type, schemaNS.CreativeWork, defaultGraph], 17 | // [example.ns("5"), schemaNS.name, rdfFactory.literal("The name"), defaultGraph], 18 | // [example.ns("5"), schemaNS.text, rdfFactory.literal("Body text"), defaultGraph], 19 | // ]; 20 | 21 | const blankSchema = (): [RDFStore, Schema] => { 22 | const store = new RDFStore(); 23 | const schema = new Schema(store); 24 | return [store, schema]; 25 | }; 26 | 27 | describe("Schema", () => { 28 | it("reads seed data from the store", () => { 29 | const dataStore = new RDFStore({ 30 | data: { 31 | [schemaNS.Thing.value]: { 32 | _id: schemaNS.Thing, 33 | [rdf.type.value]: rdfs.Class, 34 | }, 35 | [schemaNS.CreativeWork.value]: { 36 | _id: schemaNS.CreativeWork, 37 | [rdfs.subClassOf.value]: schemaNS.Thing, 38 | }, 39 | [schemaNS.BlogPosting.value]: { 40 | _id: schemaNS.BlogPosting, 41 | [rdfs.subClassOf.value]: schemaNS.CreativeWork, 42 | }, 43 | [schemaNS.Person.value]: { 44 | _id: schemaNS.Person, 45 | [rdfs.subClassOf.value]: schemaNS.Thing, 46 | }, 47 | }, 48 | }); 49 | 50 | const schema = new Schema(dataStore); 51 | 52 | expect(schema.expand([schemaNS.Thing.value])).toEqual([ 53 | schemaNS.Thing.value, 54 | rdfs.Resource.value, 55 | ]); 56 | expect(schema.expand([schemaNS.BlogPosting.value])).toEqual([ 57 | schemaNS.BlogPosting.value, 58 | schemaNS.CreativeWork.value, 59 | schemaNS.Thing.value, 60 | rdfs.Resource.value, 61 | ]); 62 | expect(schema.expand([schemaNS.Person.value])).toEqual([ 63 | schemaNS.Person.value, 64 | schemaNS.Thing.value, 65 | rdfs.Resource.value, 66 | ]); 67 | expect(schema.expand([schemaNS.BlogPosting.value, schemaNS.Person.value])).toEqual([ 68 | schemaNS.BlogPosting.value, 69 | schemaNS.Person.value, 70 | schemaNS.CreativeWork.value, 71 | schemaNS.Thing.value, 72 | rdfs.Resource.value, 73 | ]); 74 | }); 75 | 76 | describe("when empty", () => { 77 | describe("#mineForTypes", () => { 78 | it("returns the default ", () => { 79 | const [, s] = blankSchema(); 80 | expect(s.mineForTypes([])) 81 | .toEqual([rdfs.Resource.value]); 82 | }); 83 | 84 | it("ensures all have rdfs:Resource as base class", () => { 85 | const [_, schema] = blankSchema(); 86 | const result = [ 87 | schemaNS.CreativeWork.value, 88 | rdfs.Resource.value, 89 | ]; 90 | 91 | expect(schema.mineForTypes([schemaNS.CreativeWork.value])) 92 | .toEqual(result); 93 | }); 94 | 95 | it("adds superclasses", () => { 96 | const [store, schema] = blankSchema(); 97 | const result = [ 98 | schemaNS.BlogPosting.value, 99 | schemaNS.CreativeWork.value, 100 | schemaNS.Thing.value, 101 | rdfs.Resource.value, 102 | ]; 103 | 104 | store.addQuads([ 105 | [schemaNS.CreativeWork, rdfs.subClassOf, schemaNS.Thing, defaultGraph], 106 | [schemaNS.BlogPosting, rdfs.subClassOf, schemaNS.CreativeWork, defaultGraph], 107 | ]); 108 | 109 | expect(schema.mineForTypes([ 110 | schemaNS.BlogPosting.value, 111 | ])).toEqual(result); 112 | }); 113 | }); 114 | }); 115 | 116 | describe("when filled", () => { 117 | const [store, schema] = blankSchema(); 118 | store.addQuads([ 119 | [ex.ns("A"), rdfs.subClassOf, rdfs.Class, defaultGraph], 120 | 121 | [ex.ns("B"), rdfs.subClassOf, ex.ns("A"), defaultGraph], 122 | 123 | [ex.ns("C"), rdfs.subClassOf, ex.ns("A"), defaultGraph], 124 | 125 | [ex.ns("D"), rdfs.subClassOf, ex.ns("C"), defaultGraph], 126 | 127 | [ex.ns("E"), rdfs.subClassOf, rdfs.Class, defaultGraph], 128 | 129 | [ex.ns("F"), rdfs.subClassOf, rdfs.Class, defaultGraph], 130 | 131 | [ex.ns("G"), rdfs.subClassOf, example.ns("E"), defaultGraph], // TODO: check if typo 132 | ]); 133 | 134 | describe("#sort", () => { 135 | it("accounts for class inheritance", () => { 136 | expect(schema.sort([ 137 | ex.ns("D").value, 138 | ex.ns("A").value, 139 | ex.ns("C").value, 140 | ])).toEqual([ 141 | ex.ns("D").value, // 3 142 | ex.ns("C").value, // 2 143 | ex.ns("A").value, // 1 144 | ]); 145 | }); 146 | 147 | it("accounts for supertype depth", () => { 148 | expect(schema.sort([ 149 | ex.ns("G").value, 150 | ex.ns("C").value, 151 | ex.ns("B").value, 152 | ex.ns("A").value, 153 | ex.ns("D").value, 154 | ])).toEqual([ 155 | ex.ns("D").value, // 3 156 | ex.ns("C").value, // 2 157 | ex.ns("B").value, // 2 158 | ex.ns("G").value, // 2 159 | ex.ns("A").value, // 1 160 | ]); 161 | }); 162 | }); 163 | }); 164 | }); 165 | -------------------------------------------------------------------------------- /src/__tests__/TypedRecord.spec.ts: -------------------------------------------------------------------------------- 1 | import "./useFactory"; 2 | 3 | import "jest"; 4 | import { AttributeKey, TypedRecord } from "../TypedRecord"; 5 | 6 | describe("TypedRecord", () => { 7 | const keya = new AttributeKey("a"); 8 | const keyb = new AttributeKey("b"); 9 | const keyc = new AttributeKey("c"); 10 | 11 | it("gets nonexisting key", () => { 12 | const record = new TypedRecord(); 13 | 14 | expect(record.get(keya)).toBeUndefined(); 15 | }); 16 | 17 | it("gets the size", () => { 18 | const record = new TypedRecord(); 19 | expect(record.size).toBe(0); 20 | 21 | record.set(keya, ""); 22 | expect(record.size).toBe(1); 23 | }); 24 | 25 | it("pretty prints toString", () => { 26 | const record = new TypedRecord(); 27 | expect(record.size).toBe(0); 28 | 29 | record.set(keya, ""); 30 | expect(record.toString()).toBe("[object Map]"); 31 | }); 32 | 33 | it("pretty prints AttributeKey toString", () => { 34 | expect(keya.toString()).toBe("a"); 35 | }); 36 | 37 | it("clears the map", () => { 38 | const record = new TypedRecord(); 39 | expect(record.size).toBe(0); 40 | 41 | record.set(keya, ""); 42 | expect(record.size).toBe(1); 43 | 44 | record.clear(); 45 | expect(record.size).toBe(0); 46 | }); 47 | 48 | it("deletes the key", () => { 49 | const record = new TypedRecord(); 50 | expect(record.size).toBe(0); 51 | 52 | record.set(keya, ""); 53 | record.set(keyb, ""); 54 | record.set(keyc, ""); 55 | expect(record.size).toBe(3); 56 | 57 | record.delete(keya); 58 | expect(record.size).toBe(2); 59 | }); 60 | 61 | it("returns the keys", () => { 62 | const record = new TypedRecord(); 63 | 64 | record.set(keya, ""); 65 | record.set(keyb, ""); 66 | record.set(keyc, ""); 67 | expect(Array.from(record.keys())).toEqual([ 68 | keya, 69 | keyb, 70 | keyc, 71 | ]); 72 | }); 73 | 74 | it("returns the values", () => { 75 | const record = new TypedRecord(); 76 | 77 | record.set(keya, "value key"); 78 | record.set(keyb, "value keyb"); 79 | record.set(keyc, "value keyc"); 80 | expect(Array.from(record.values())).toEqual([ 81 | "value key", 82 | "value keyb", 83 | "value keyc", 84 | ]); 85 | }); 86 | 87 | it("handles forEach", () => { 88 | const record = new TypedRecord(); 89 | 90 | record.set(keya, "value key"); 91 | record.set(keyb, "value keyb"); 92 | record.set(keyc, "value keyc"); 93 | 94 | const processed: unknown[] = []; 95 | 96 | record.forEach((item) => { 97 | processed.push(item); 98 | }); 99 | 100 | expect(processed).toEqual([ 101 | "value key", 102 | "value keyb", 103 | "value keyc", 104 | ]); 105 | }); 106 | 107 | it("handles entries", () => { 108 | const record = new TypedRecord(); 109 | 110 | record.set(keya, "value key"); 111 | record.set(keyb, "value keyb"); 112 | record.set(keyc, "value keyc"); 113 | 114 | expect(Array.from(record.entries())).toEqual([ 115 | [keya, "value key"], 116 | [keyb, "value keyb"], 117 | [keyc, "value keyc"], 118 | ]); 119 | }); 120 | 121 | it("handles iterators", () => { 122 | const record = new TypedRecord(); 123 | 124 | record.set(keya, "value key"); 125 | record.set(keyb, "value keyb"); 126 | record.set(keyc, "value keyc"); 127 | 128 | const keys = []; 129 | const values = []; 130 | 131 | for (const [key, item] of record) { 132 | keys.push(key); 133 | values.push(item); 134 | } 135 | 136 | expect(keys).toEqual([ 137 | keya, 138 | keyb, 139 | keyc, 140 | ]); 141 | expect(values).toEqual([ 142 | "value key", 143 | "value keyb", 144 | "value keyc", 145 | ]); 146 | }); 147 | 148 | it("has key existence", () => { 149 | const record = new TypedRecord(); 150 | expect(record.has(keya)).toBe(false); 151 | 152 | record.set(keya, ""); 153 | expect(record.has(keya)).toBe(true); 154 | }); 155 | }); 156 | -------------------------------------------------------------------------------- /src/__tests__/createStore.spec.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "../createStore"; 2 | import { LinkedRenderStore } from "../LinkedRenderStore"; 3 | import ex from "../ontology/ex"; 4 | 5 | describe("createStore", () => { 6 | it("can be called without arguments", () => { 7 | const store = createStore(); 8 | expect(store).toBeInstanceOf(LinkedRenderStore); 9 | }); 10 | 11 | it("passes store options", () => { 12 | const report = jest.fn(); 13 | 14 | const store = createStore({ 15 | report, 16 | }); 17 | 18 | expect(store.report).toBe(report); 19 | }); 20 | 21 | it("prefixes the middleware", () => { 22 | const handler = jest.fn(); 23 | const connector = jest.fn((_) => handler); 24 | const middleware = jest.fn(() => connector); 25 | 26 | const store = createStore({}, [middleware]); 27 | expect(middleware).toHaveBeenCalled(); 28 | 29 | store.dispatch(ex.ns("a")); 30 | expect(handler).toHaveBeenCalledWith(ex.ns("a")); 31 | }); 32 | 33 | it("throws on invalid middleware", () => { 34 | const middlewareHandler = jest.fn(); 35 | const middleware = jest.fn(() => middlewareHandler); 36 | 37 | expect(() => { 38 | createStore({}, [middleware]); 39 | }).toThrow(); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/__tests__/factoryHelpers.spec.ts: -------------------------------------------------------------------------------- 1 | import "jest"; 2 | 3 | import rdfFactory, { 4 | DataFactory, 5 | Feature, 6 | NamedNode, 7 | } from "@ontologies/core"; 8 | import * as schema from "@ontologies/schema"; 9 | import { createEqualComparator, id } from "../factoryHelpers"; 10 | 11 | describe("factoryHelpers", () => { 12 | describe("createEqualComparator", () => { 13 | describe("without idStamp factory", () => { 14 | const factory = { 15 | defaultGraph(): NamedNode { return rdfFactory.namedNode("rdf:defaultGraph"); }, 16 | equals: jest.fn(), 17 | supports: { 18 | [Feature.identity]: false, 19 | [Feature.idStamp]: false, 20 | }, 21 | } as unknown as DataFactory; 22 | const equals = createEqualComparator(factory); 23 | 24 | it("calls the factory comparison method", () => { 25 | equals("a", "b"); 26 | expect(factory.equals).toHaveBeenCalledWith("a", "b"); 27 | }); 28 | }); 29 | 30 | describe("without idStamp factory", () => { 31 | const factory = { 32 | defaultGraph(): NamedNode { return rdfFactory.namedNode("rdf:defaultGraph"); }, 33 | supports: { 34 | [Feature.identity]: false, 35 | [Feature.idStamp]: true, 36 | }, 37 | } as DataFactory; 38 | const equals = createEqualComparator(factory); 39 | 40 | it("compares equal nodes", () => { 41 | expect(equals({ id: 2 }, { id: 2 })).toBeTruthy(); 42 | }); 43 | 44 | it("compares unequal nodes", () => { 45 | expect(equals({ id: 2 }, { id: 3 })).toBeFalsy(); 46 | }); 47 | }); 48 | 49 | describe("with identity factory", () => { 50 | const factory = { 51 | defaultGraph(): NamedNode { return rdfFactory.namedNode("rdf:defaultGraph"); }, 52 | supports: { 53 | [Feature.identity]: true, 54 | [Feature.idStamp]: false, 55 | }, 56 | } as DataFactory; 57 | const equals = createEqualComparator(factory); 58 | 59 | it("compares equal nodes", () => { 60 | const node = {}; 61 | expect(equals(node, node)).toBeTruthy(); 62 | }); 63 | 64 | it("compares unequal nodes", () => { 65 | expect(equals({}, {})).toBeFalsy(); 66 | }); 67 | 68 | it("compares undefined", () => { 69 | expect(equals(undefined, {})).toBeFalsy(); 70 | expect(equals({}, undefined)).toBeFalsy(); 71 | }); 72 | }); 73 | }); 74 | 75 | describe("id", () => { 76 | describe("without identity factory", () => { 77 | it("retrieves node ids", () => { 78 | expect(id(schema.name)).toEqual(""); 79 | }); 80 | 81 | it("throws on undefined", () => { 82 | expect(() => { 83 | id(undefined); 84 | }).toThrow(TypeError); 85 | }); 86 | }); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /src/__tests__/factoryHelpersHashFactory.spec.ts: -------------------------------------------------------------------------------- 1 | import "jest"; 2 | import "./useFactory"; 3 | 4 | import rdfFactory from "@ontologies/core"; 5 | import * as schema from "@ontologies/schema"; 6 | import { equals, id } from "../factoryHelpers"; 7 | 8 | describe("factoryHelpers", () => { 9 | describe("with hash factory", () => { 10 | describe("equals", () => { 11 | it("compares nodes", () => { 12 | expect(equals(schema.name, rdfFactory.namedNode("http://schema.org/name"))).toBeTruthy(); 13 | }); 14 | 15 | it("compares undefined", () => { 16 | expect(equals(undefined, rdfFactory.namedNode("http://schema.org/name"))).toBeFalsy(); 17 | expect(equals(schema.name, undefined)).toBeFalsy(); 18 | }); 19 | }); 20 | 21 | describe("id", () => { 22 | it("retrieves node ids", () => { 23 | expect(id(schema.name)).toEqual(3658353846); 24 | }); 25 | 26 | it("throws on undefined", () => { 27 | expect(() => { 28 | id(undefined); 29 | }).toThrow(TypeError); 30 | }); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/__tests__/linkMiddleware.spec.ts: -------------------------------------------------------------------------------- 1 | import "jest"; 2 | import "./useFactory"; 3 | 4 | import { LinkedRenderStore } from "../LinkedRenderStore"; 5 | import { linkMiddleware } from "../linkMiddleware"; 6 | import example from "../ontology/example"; 7 | import ll from "../ontology/ll"; 8 | import { MiddlewareActionHandler } from "../types"; 9 | 10 | interface ExplodedMiddleware { 11 | dispatch: MiddlewareActionHandler; 12 | execActionByIRI: jest.Mock; 13 | next: jest.Mock; 14 | touch: jest.Mock; 15 | } 16 | 17 | const createTestMiddleware = (catchActions: boolean = true): ExplodedMiddleware => { 18 | const next = jest.fn(); 19 | const touch = jest.fn(); 20 | const execActionByIRI = jest.fn(); 21 | 22 | const mockstore = { execActionByIRI, touch }; 23 | // @ts-ignore 24 | const dispatch = linkMiddleware(catchActions)(mockstore as LinkedRenderStore)(next); 25 | 26 | return { 27 | dispatch, 28 | execActionByIRI, 29 | next, 30 | touch, 31 | }; 32 | }; 33 | 34 | describe("linkMiddleware", () => { 35 | it("has coverage", () => { 36 | // It really has, but doesn't seem to be accounted for since the function is called in a helper function 37 | expect(linkMiddleware()).toBeDefined(); 38 | }); 39 | 40 | it("calls touch for data events", async () => { 41 | const middleware = createTestMiddleware(); 42 | 43 | await middleware.dispatch(ll.ns("data/rdflib/done"), [example.ns("test"), undefined]); 44 | 45 | expect(middleware.execActionByIRI).toHaveBeenCalledTimes(0); 46 | expect(middleware.next).toHaveBeenCalledTimes(0); 47 | expect(middleware.touch).toHaveBeenCalledTimes(1); 48 | }); 49 | 50 | it("passes actions down", async () => { 51 | const middleware = createTestMiddleware(); 52 | 53 | await middleware.dispatch(example.ns("action/1"), undefined); 54 | 55 | expect(middleware.execActionByIRI).toHaveBeenCalledTimes(1); 56 | expect(middleware.next).toHaveBeenCalledTimes(0); 57 | expect(middleware.touch).toHaveBeenCalledTimes(0); 58 | }); 59 | 60 | describe("without catchActions", () => { 61 | it("passes actions down", async () => { 62 | const middleware = createTestMiddleware(false); 63 | 64 | await middleware.dispatch(example.ns("action/1"), undefined); 65 | 66 | expect(middleware.execActionByIRI).toHaveBeenCalledTimes(0); 67 | expect(middleware.next).toHaveBeenCalledTimes(1); 68 | expect(middleware.touch).toHaveBeenCalledTimes(0); 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /src/__tests__/useFactory.ts: -------------------------------------------------------------------------------- 1 | import memoizedFactory from "@ontola/memoized-hash-factory"; 2 | import { setup } from "@ontologies/core"; 3 | 4 | setup(memoizedFactory); 5 | -------------------------------------------------------------------------------- /src/createStore.ts: -------------------------------------------------------------------------------- 1 | import { LinkedDataAPI } from "./LinkedDataAPI"; 2 | import { LinkedRenderStore } from "./LinkedRenderStore"; 3 | import { linkMiddleware } from "./linkMiddleware"; 4 | import { DataProcessor } from "./processor/DataProcessor"; 5 | import { 6 | LinkedRenderStoreOptions, 7 | MiddlewareActionHandler, 8 | MiddlewareFn, 9 | SomeNode, 10 | } from "./types"; 11 | 12 | function applyMiddleware( 13 | lrs: LinkedRenderStore, 14 | ...layers: Array> 15 | ): MiddlewareActionHandler { 16 | const storeBound = layers.map((middleware) => middleware(lrs)); 17 | 18 | const finish: MiddlewareActionHandler = (a: SomeNode, _o: any): Promise => Promise.resolve(a); 19 | 20 | return storeBound.reduceRight((composed, f) => { 21 | const next = f(composed); 22 | if (!next) { 23 | throw new Error("Provided middleware did not return handler."); 24 | } 25 | return next; 26 | }, finish); 27 | } 28 | 29 | /** 30 | * Initializes a {LinkedRenderStore} with tied together middleware. 31 | * @param storeOpts Constructor arguments for the LRS. 32 | * @param middleware Main middleware, to be executed before the {linkMiddleware}. 33 | * @param trailingMiddleware Middleware to be placed after the {linkMiddleware}. Note: defining trailing middleware 34 | * causes actions not to be executed via {LinkedRenderStore#execActionByIRI} anymore, this behaviour can be enabled 35 | * manually in one of the defined middlewares if still desired. 36 | */ 37 | export function createStore( 38 | storeOpts: LinkedRenderStoreOptions = {}, 39 | middleware: Array> = [], 40 | trailingMiddleware: Array> = [], 41 | ): LinkedRenderStore { 42 | const LRS = new LinkedRenderStore(storeOpts); 43 | 44 | LRS.dispatch = applyMiddleware( 45 | LRS, 46 | ...middleware, 47 | linkMiddleware(trailingMiddleware.length === 0), 48 | ...trailingMiddleware, 49 | ); 50 | 51 | return LRS; 52 | } 53 | -------------------------------------------------------------------------------- /src/datastrucures/DataSlice.ts: -------------------------------------------------------------------------------- 1 | import { SomeNode } from "../types"; 2 | 3 | import { FieldValue } from "./Fields"; 4 | 5 | export type Id = string; 6 | 7 | export type FieldSet = Record; 8 | 9 | export type DataRecord = { _id: SomeNode } & FieldSet; 10 | 11 | export type DataSlice = Record; 12 | -------------------------------------------------------------------------------- /src/datastrucures/DataSliceDSL.ts: -------------------------------------------------------------------------------- 1 | import rdfFactory, { SomeTerm } from "@ontologies/core"; 2 | import { idToValue } from "../factoryHelpers"; 3 | 4 | import { SomeNode } from "../types"; 5 | 6 | import { DataRecord, DataSlice } from "./DataSlice"; 7 | 8 | export type OptionalIdOrNode = string | SomeNode | undefined; 9 | 10 | export interface RecordBuilder { 11 | /** Sets the {value} of {field} on the current record */ 12 | field(field: string | SomeNode, value: SomeTerm): this; 13 | /** Returns the id of the current record. */ 14 | id(): SomeNode; 15 | } 16 | 17 | export interface SliceBuilder { 18 | record(id?: OptionalIdOrNode): RecordBuilder; 19 | } 20 | 21 | export type SliceCreator = (slice: SliceBuilder) => void; 22 | 23 | const stringIdOrNewLocal = (id: OptionalIdOrNode): string => { 24 | if (id === undefined) { 25 | return rdfFactory.blankNode().value; 26 | } 27 | 28 | return typeof id === "string" ? id : id.value; 29 | }; 30 | 31 | export const buildSlice = (creator: SliceCreator): DataSlice => { 32 | if (creator === undefined) { 33 | throw new Error("No creator passed"); 34 | } 35 | 36 | const slice: DataSlice = {}; 37 | 38 | const builder: SliceBuilder = { 39 | record(id: OptionalIdOrNode): RecordBuilder { 40 | const stringId = stringIdOrNewLocal(id); 41 | const termId = idToValue(stringId); 42 | 43 | const record: DataRecord = { 44 | _id: termId, 45 | }; 46 | 47 | const recordBuilder: RecordBuilder = { 48 | field(field: string | SomeNode, value: SomeTerm): RecordBuilder { 49 | const fieldName = typeof field === "string" ? field : field.value; 50 | record[fieldName] = value; 51 | 52 | return recordBuilder; 53 | }, 54 | 55 | id(): SomeNode { 56 | return termId; 57 | }, 58 | }; 59 | 60 | slice[stringId] = record; 61 | 62 | return recordBuilder; 63 | }, 64 | }; 65 | 66 | creator(builder); 67 | 68 | return slice; 69 | }; 70 | -------------------------------------------------------------------------------- /src/datastrucures/DeepSlice.ts: -------------------------------------------------------------------------------- 1 | import { SomeTerm } from "@ontologies/core"; 2 | 3 | import { SomeNode } from "../types"; 4 | 5 | import { FieldValue } from "./Fields"; 6 | 7 | export type DeepRecordFieldValue = FieldValue | DeepRecord | Array; 8 | 9 | export type DeepRecord = { _id: SomeNode } & { [k: string]: DeepRecordFieldValue }; 10 | -------------------------------------------------------------------------------- /src/datastrucures/EmpSlice.ts: -------------------------------------------------------------------------------- 1 | export interface Value { 2 | type: "id" | "lid" | "p" | "s" | "dt" | "b" | "i" | "l" | "ls"; 3 | v: string; 4 | } 5 | 6 | export interface GlobalId extends Value { 7 | type: "id"; 8 | v: string; 9 | } 10 | 11 | export interface LocalId extends Value { 12 | type: "lid"; 13 | v: string; 14 | } 15 | 16 | export interface Primitive extends Value { 17 | type: "p"; 18 | v: string; 19 | dt: string; 20 | } 21 | 22 | export interface EmpString extends Value { 23 | type: "s"; 24 | v: string; 25 | } 26 | 27 | export interface EmpBoolean extends Value { 28 | type: "b"; 29 | v: string; 30 | } 31 | 32 | export interface Int extends Value { 33 | type: "i"; 34 | v: string; 35 | } 36 | 37 | export interface Long extends Value { 38 | type: "l"; 39 | v: string; 40 | } 41 | 42 | export interface DateTime extends Value { 43 | type: "dt"; 44 | v: string; 45 | } 46 | 47 | export interface LangString extends Value { 48 | type: "ls"; 49 | v: string; 50 | l: string; 51 | } 52 | 53 | export interface Identifiable { 54 | _id: GlobalId | LocalId; 55 | } 56 | 57 | export type Fields = Record; 58 | 59 | export type SliceDataRecord = Identifiable & Fields; 60 | 61 | export type Slice = Record; 62 | 63 | export type DeepSliceFieldValue = Value | Value[] | DeepSliceDataRecord | DeepSliceDataRecord[]; 64 | 65 | export interface DeepSliceFields { 66 | [field: string]: DeepSliceFieldValue; 67 | } 68 | 69 | export type DeepSliceDataRecord = Identifiable & DeepSliceFields; 70 | 71 | export type DeepSlice = Record; 72 | -------------------------------------------------------------------------------- /src/datastrucures/Fields.ts: -------------------------------------------------------------------------------- 1 | import { SomeTerm } from "@ontologies/core"; 2 | 3 | export type FieldId = string; 4 | 5 | export type MultimapTerm = SomeTerm[]; 6 | 7 | export type FieldValue = SomeTerm | MultimapTerm; 8 | -------------------------------------------------------------------------------- /src/datastrucures/__tests__/DataSliceDSL.spec.ts: -------------------------------------------------------------------------------- 1 | import "../../__tests__/useFactory"; 2 | 3 | import rdfFactory, { TermType } from "@ontologies/core"; 4 | import * as schema from "@ontologies/schema"; 5 | 6 | import { DataSlice } from "../DataSlice"; 7 | import { buildSlice } from "../DataSliceDSL"; 8 | 9 | describe("DataSliceDSL", () => { 10 | describe("id", () => { 11 | const expectId = ( 12 | slice: DataSlice, 13 | termType: TermType, 14 | id: string, 15 | ): void => { 16 | const entries = Object.entries(slice); 17 | expect(entries).toHaveLength(1); 18 | const givenId = entries[0][0]; 19 | expect(typeof givenId).toEqual("string"); 20 | const record = entries[0][1]; 21 | expect(record._id.termType).toEqual(termType); 22 | expect(record._id.value).toEqual(id); 23 | }; 24 | 25 | it("raises without builder argument", () => { 26 | expect(() => { 27 | // @ts-ignore 28 | buildSlice(); 29 | }).toThrow(); 30 | }); 31 | 32 | it("builds an empty slice", () => { 33 | // tslint:disable-next-line:no-empty 34 | expect(buildSlice(() => {})).toEqual({}); 35 | }); 36 | 37 | it("adds an empty record", () => { 38 | const slice = buildSlice((builder) => { 39 | builder.record(); 40 | }); 41 | 42 | expectId(slice, TermType.BlankNode, Object.keys(slice)[0]); 43 | }); 44 | 45 | it("adds an empty record with lid", () => { 46 | const slice = buildSlice((builder) => { 47 | builder.record("_:myLocalId"); 48 | }); 49 | 50 | expectId(slice, TermType.BlankNode, "_:myLocalId"); 51 | }); 52 | 53 | it("adds an empty record with id", () => { 54 | const slice = buildSlice((builder) => { 55 | builder.record(schema.name.value); 56 | }); 57 | 58 | expectId(slice, TermType.NamedNode, schema.name.value); 59 | }); 60 | 61 | it("adds an empty record with term id", () => { 62 | const slice = buildSlice((builder) => { 63 | builder.record(schema.name); 64 | }); 65 | 66 | expectId(slice, TermType.NamedNode, schema.name.value); 67 | }); 68 | }); 69 | 70 | describe("field", () => { 71 | const id = rdfFactory.blankNode(); 72 | const target = rdfFactory.blankNode(); 73 | 74 | it("sets a field by id", () => { 75 | const slice = buildSlice((builder) => { 76 | builder.record(id).field("myField", target); 77 | }); 78 | 79 | expect(slice[id.value].myField).toEqual(target); 80 | }); 81 | 82 | it("sets a field by id", () => { 83 | const slice = buildSlice((builder) => { 84 | builder.record(id).field(schema.name.value, target); 85 | }); 86 | 87 | expect(slice[id.value][schema.name.value]).toEqual(target); 88 | }); 89 | 90 | it("sets a field by term", () => { 91 | const slice = buildSlice((builder) => { 92 | builder.record(id).field(schema.name, target); 93 | }); 94 | 95 | expect(slice[id.value][schema.name.value]).toEqual(target); 96 | }); 97 | 98 | it("sets multiple fields", () => { 99 | const slice = buildSlice((builder) => { 100 | builder.record(id) 101 | .field(schema.name, target) 102 | .field(schema.text.value, rdfFactory.literal("body")); 103 | }); 104 | 105 | expect(slice[id.value][schema.name.value]).toEqual(target); 106 | expect(slice[id.value][schema.text.value]).toEqual(rdfFactory.literal("body")); 107 | }); 108 | 109 | it("allows retrieving the id", () => { 110 | const second = rdfFactory.blankNode(); 111 | 112 | const slice = buildSlice((builder) => { 113 | const creator = builder.record(id) 114 | .field(schema.name, rdfFactory.literal("Bob")) 115 | .id(); 116 | 117 | builder.record(second) 118 | .field(schema.creator, creator); 119 | }); 120 | 121 | expect(slice[id.value]._id).toEqual(id); 122 | expect(slice[id.value][schema.name.value]).toEqual(rdfFactory.literal("Bob")); 123 | 124 | expect(slice[second.value]._id).toEqual(second); 125 | expect(slice[second.value][schema.creator.value]).toEqual(id); 126 | }); 127 | }); 128 | }); 129 | -------------------------------------------------------------------------------- /src/factoryHelpers.ts: -------------------------------------------------------------------------------- 1 | import rdfFactory, { DataFactory, Feature, Quad, Term } from "@ontologies/core"; 2 | 3 | import { Id } from "./datastrucures/DataSlice"; 4 | import { SomeNode } from "./types"; 5 | 6 | export type Comparator = (a: any, b: any) => boolean; 7 | 8 | export const createEqualComparator = (factory: DataFactory): Comparator => factory.supports[Feature.identity] 9 | ? (a: any, b: any): boolean => a === b 10 | : factory.supports[Feature.idStamp] 11 | ? (a: any, b: any): boolean => a?.id === b?.id 12 | : (a: any, b: any): boolean => factory.equals(a, b); 13 | 14 | /** @internal */ 15 | export const equals = createEqualComparator(rdfFactory); 16 | 17 | const noIdError = (obj: any): void => { 18 | throw new TypeError(`Factory has idStamp feature, but the property wasn't present on ${obj}`); 19 | }; 20 | 21 | const noValueError = (obj: any): void => { 22 | throw new TypeError(`Unable to lookup property 'value' on ${obj}.`); 23 | }; 24 | 25 | /** @internal */ 26 | export const id = rdfFactory.supports[Feature.idStamp] 27 | ? (obj?: Term | Quad | any): number => obj?.id || noIdError(obj) 28 | : (obj?: Term | Quad | any): number => rdfFactory.id(obj); 29 | 30 | /** @internal */ 31 | export const value = (obj?: Term): string => (obj as any)?.value ?? noValueError(obj); 32 | 33 | export const idToValue = (recordId: Id): SomeNode => { 34 | if (recordId.includes(":") && !recordId.startsWith("_:")) { 35 | return rdfFactory.namedNode(recordId); 36 | } else { 37 | return rdfFactory.blankNode(recordId); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { LinkedRenderStore } from "./LinkedRenderStore"; 2 | 3 | export { createStore } from "./createStore"; 4 | /** @internal */ 5 | export * from "./factoryHelpers"; 6 | export { linkMiddleware } from "./linkMiddleware"; 7 | export { DataProcessor } from "./processor/DataProcessor"; 8 | export { RequestInitGenerator } from "./processor/RequestInitGenerator"; 9 | export { ProcessorError } from "./processor/ProcessorError"; 10 | export { RDFStore, RDFStoreOpts } from "./RDFStore"; 11 | export { deltaProcessor } from "./store/deltaProcessor"; 12 | export { Schema } from "./Schema"; 13 | export * from "./testUtilities"; 14 | export { 15 | list, 16 | seq, 17 | toGraph, 18 | } from "./processor/DataToGraph"; 19 | export { transformers } from "./transformers/index"; 20 | 21 | export { 22 | OptionalIdOrNode, 23 | SliceCreator, 24 | RecordBuilder, 25 | buildSlice, 26 | SliceBuilder, 27 | } from "./datastrucures/DataSliceDSL"; 28 | 29 | export { 30 | Messages, 31 | IdMessage, 32 | FieldMessage, 33 | FieldSetMessage, 34 | ValueMessage, 35 | Message, 36 | SetRecordMessage, 37 | SetFieldMessage, 38 | AddFieldMessage, 39 | DeleteFieldMessage, 40 | DeleteFieldMatchingMessage, 41 | DeleteAllFieldsMatchingMessage, 42 | InvalidateRecordMessage, 43 | InvalidateAllWithPropertyMessage, 44 | setRecord, 45 | addField, 46 | setField, 47 | deleteAllFieldsMatching, 48 | deleteFieldMatching, 49 | deleteField, 50 | invalidateRecord, 51 | invalidateAllWithProperty, 52 | } from "./messages/message"; 53 | export { 54 | MessageProcessor, 55 | createMessageProcessor, 56 | } from "./messages/messageProcessor"; 57 | 58 | export * from "./types"; 59 | export { 60 | allRDFPropertyStatements, 61 | allRDFValues, 62 | anyRDFValue, 63 | getPropBestLangRaw, 64 | getTermBestLang, 65 | isDifferentOrigin, 66 | normalizeType, 67 | } from "./utilities"; 68 | export { 69 | DEFAULT_TOPOLOGY, 70 | RENDER_CLASS_NAME, 71 | } from "./utilities/constants"; 72 | export { 73 | isGlobalId, 74 | isLocalId, 75 | mergeTerms, 76 | } from "./utilities/slices"; 77 | export * from "./LinkedDataAPI"; 78 | export { 79 | AttributeKey, 80 | TypedRecord, 81 | } from "./TypedRecord"; 82 | export { RecordState } from "./store/RecordState"; 83 | export { RecordStatus } from "./store/RecordStatus"; 84 | export { 85 | hasReferenceTo, 86 | fieldReferences, 87 | findAllReferencingIds, 88 | } from "./store/StructuredStore/references"; 89 | export { 90 | idField, 91 | StructuredStore, 92 | } from "./store/StructuredStore"; 93 | export { RDFAdapter } from "./store/RDFAdapter"; 94 | 95 | export { 96 | LinkedRenderStore, 97 | }; 98 | 99 | export default LinkedRenderStore; // tslint:disable-line no-default-export 100 | export { FieldValue } from "./datastrucures/Fields"; 101 | export { DataSlice } from "./datastrucures/DataSlice"; 102 | export { DataRecord } from "./datastrucures/DataSlice"; 103 | export { FieldSet } from "./datastrucures/DataSlice"; 104 | export { Id } from "./datastrucures/DataSlice"; 105 | export { DeepRecord } from "./datastrucures/DeepSlice"; 106 | export { DeepRecordFieldValue } from "./datastrucures/DeepSlice"; 107 | export { MultimapTerm } from "./datastrucures/Fields"; 108 | export { FieldId } from "./datastrucures/Fields"; 109 | -------------------------------------------------------------------------------- /src/linkMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { LinkedDataAPI } from "./LinkedDataAPI"; 2 | import { LinkedRenderStore } from "./LinkedRenderStore"; 3 | import ll from "./ontology/ll"; 4 | import { DataProcessor } from "./processor/DataProcessor"; 5 | import { 6 | MiddlewareActionHandler, 7 | MiddlewareFn, 8 | MiddlewareWithBoundLRS, 9 | SomeNode, 10 | } from "./types"; 11 | 12 | /** 13 | * Binds various uris to link actions. 14 | * 15 | * @see {createStore} 16 | * @param catchActions {boolean} Set to true to catch all left-over actions to {LinkedRenderStore#execActionByIRI}. 17 | */ 18 | export const linkMiddleware = (catchActions = true): 19 | MiddlewareFn => (lrs: LinkedRenderStore): MiddlewareWithBoundLRS => 20 | (next: MiddlewareActionHandler): MiddlewareActionHandler => 21 | (action: SomeNode, args: any): Promise => { 22 | 23 | if (action.value.startsWith(ll.ns("data/rdflib/").value)) { 24 | return Promise.resolve(lrs.touch(args[0], args[1])); 25 | } 26 | 27 | if (catchActions) { 28 | return lrs.execActionByIRI(action, args); 29 | } 30 | 31 | return next(action, args); 32 | }; 33 | -------------------------------------------------------------------------------- /src/messages/message.ts: -------------------------------------------------------------------------------- 1 | import { SomeTerm } from "@ontologies/core"; 2 | import { FieldSet } from "../datastrucures/DataSlice"; 3 | 4 | export type Messages = SetRecordMessage 5 | | SetFieldMessage 6 | | AddFieldMessage 7 | | DeleteFieldMessage 8 | | DeleteFieldMatchingMessage 9 | | DeleteAllFieldsMatchingMessage 10 | | InvalidateRecordMessage 11 | | InvalidateAllWithPropertyMessage; 12 | 13 | export interface IdMessage { 14 | id: string; 15 | } 16 | 17 | export interface FieldMessage { 18 | field: string; 19 | } 20 | 21 | export interface FieldSetMessage { 22 | fields: FieldSet; 23 | } 24 | 25 | export interface ValueMessage { 26 | value: SomeTerm; 27 | } 28 | 29 | export interface Message { 30 | type: string; 31 | } 32 | 33 | export interface SetRecordMessage extends Message, IdMessage, FieldSetMessage { 34 | type: "SetRecord"; 35 | id: string; 36 | fields: FieldSet; 37 | } 38 | 39 | export interface SetFieldMessage extends Message, IdMessage, FieldMessage, ValueMessage { 40 | type: "SetField"; 41 | id: string; 42 | field: string; 43 | value: SomeTerm; 44 | } 45 | 46 | export interface AddFieldMessage extends Message, IdMessage, FieldMessage, ValueMessage { 47 | type: "AddField"; 48 | id: string; 49 | field: string; 50 | value: SomeTerm; 51 | } 52 | 53 | export interface DeleteFieldMessage extends Message, IdMessage, FieldMessage { 54 | type: "DeleteField"; 55 | id: string; 56 | field: string; 57 | } 58 | 59 | export interface DeleteFieldMatchingMessage extends Message, IdMessage, FieldMessage, ValueMessage { 60 | type: "DeleteFieldMatching"; 61 | id: string; 62 | field: string; 63 | value: SomeTerm; 64 | } 65 | 66 | export interface DeleteAllFieldsMatchingMessage extends Message, FieldMessage, ValueMessage { 67 | type: "DeleteAllFieldsMatching"; 68 | field: string; 69 | value: SomeTerm; 70 | } 71 | 72 | export interface InvalidateRecordMessage extends Message { 73 | type: "InvalidateRecord"; 74 | id: string; 75 | } 76 | 77 | export interface InvalidateAllWithPropertyMessage extends Message, FieldMessage, ValueMessage { 78 | type: "InvalidateAllWithProperty"; 79 | field: string; 80 | value: SomeTerm; 81 | } 82 | 83 | /* tslint:disable object-literal-sort-keys */ 84 | 85 | export const setRecord = (id: string, fields: FieldSet): SetRecordMessage => ({ 86 | type: "SetRecord", 87 | id, 88 | fields, 89 | }); 90 | 91 | export const addField = (id: string, field: string, value: SomeTerm): AddFieldMessage => ({ 92 | type: "AddField", 93 | id, 94 | field, 95 | value, 96 | }); 97 | 98 | export const setField = (id: string, field: string, value: SomeTerm): SetFieldMessage => ({ 99 | type: "SetField", 100 | id, 101 | field, 102 | value, 103 | }); 104 | 105 | export const deleteAllFieldsMatching = (field: string, value: SomeTerm): DeleteAllFieldsMatchingMessage => ({ 106 | type: "DeleteAllFieldsMatching", 107 | field, 108 | value, 109 | }); 110 | 111 | export const deleteFieldMatching = (id: string, field: string, value: SomeTerm): DeleteFieldMatchingMessage => ({ 112 | type: "DeleteFieldMatching", 113 | id, 114 | field, 115 | value, 116 | }); 117 | 118 | export const deleteField = (id: string, field: string): DeleteFieldMessage => ({ 119 | type: "DeleteField", 120 | id, 121 | field, 122 | }); 123 | 124 | export const invalidateRecord = (id: string): InvalidateRecordMessage => ({ 125 | type: "InvalidateRecord", 126 | id, 127 | }); 128 | 129 | export const invalidateAllWithProperty = (field: string, value: SomeTerm): InvalidateAllWithPropertyMessage => ({ 130 | type: "InvalidateAllWithProperty", 131 | field, 132 | value, 133 | }); 134 | 135 | /* tslint:enable object-literal-sort-keys */ 136 | -------------------------------------------------------------------------------- /src/messages/messageProcessor.ts: -------------------------------------------------------------------------------- 1 | import rdf, { NamedNode, QuadPosition } from "@ontologies/core"; 2 | import * as rdfx from "@ontologies/rdf"; 3 | 4 | import { LinkedRenderStore } from "../LinkedRenderStore"; 5 | import { isGlobalId } from "../utilities/slices"; 6 | 7 | import { Messages } from "./message"; 8 | 9 | export type MessageProcessor = (m: Messages) => void; 10 | 11 | export const createMessageProcessor = (lrs: LinkedRenderStore): ((m: Messages) => void) => { 12 | const store = lrs.store.getInternalStore().store; 13 | 14 | return (message: Messages): void => { 15 | switch (message.type) { 16 | case "SetRecord": { 17 | store.setRecord(message.id, message.fields); 18 | break; 19 | } 20 | 21 | case "AddField": 22 | case "SetField": { 23 | const isPartial = isGlobalId(message.id) 24 | && message.field !== rdfx.type.value 25 | && !store.getField(message.id, rdfx.type.value); 26 | 27 | if (isPartial) { 28 | lrs.queueEntity(rdf.namedNode(message.id), { reload: true }); 29 | } else if (message.type === "AddField") { 30 | store.addField( 31 | message.id, 32 | message.field, 33 | message.value, 34 | ); 35 | } else if (message.type === "SetField") { 36 | store.setField( 37 | message.id, 38 | message.field, 39 | message.value, 40 | ); 41 | } 42 | 43 | break; 44 | } 45 | 46 | case "DeleteField": { 47 | store.deleteField(message.id, message.field); 48 | break; 49 | } 50 | 51 | case "DeleteFieldMatching": { 52 | store.deleteFieldMatching( 53 | message.id, 54 | message.field, 55 | message.value, 56 | ); 57 | break; 58 | } 59 | 60 | case "DeleteAllFieldsMatching": { 61 | const matches = lrs.store.match( 62 | null, 63 | rdf.namedNode(message.field), 64 | message.value, 65 | ); 66 | 67 | for (const match of matches) { 68 | store.deleteFieldMatching( 69 | match[0].value, 70 | message.field, 71 | message.value, 72 | ); 73 | } 74 | 75 | break; 76 | } 77 | 78 | case "InvalidateRecord": { 79 | if (isGlobalId(message.id)) { 80 | lrs.queueEntity(rdf.namedNode(message.id), { reload: true }); 81 | } 82 | 83 | break; 84 | } 85 | 86 | case "InvalidateAllWithProperty": { 87 | const matches = lrs.store.match( 88 | null, 89 | rdf.namedNode(message.field), 90 | message.value, 91 | ); 92 | 93 | for (const match of matches) { 94 | const id = match[QuadPosition.subject]; 95 | 96 | if (isGlobalId(id.value)) { 97 | lrs.queueEntity(id as NamedNode, { reload: true }); 98 | } 99 | } 100 | 101 | break; 102 | } 103 | 104 | default: { 105 | const error = new Error(`Unknown message: ${JSON.stringify(message)}`); 106 | lrs.report(error); 107 | throw error; 108 | } 109 | } 110 | }; 111 | }; 112 | -------------------------------------------------------------------------------- /src/ontology/argu.ts: -------------------------------------------------------------------------------- 1 | import { createNS } from "@ontologies/core"; 2 | 3 | const example = createNS("https://argu.co/ns/core#"); 4 | 5 | export default { 6 | ns: example, 7 | }; 8 | -------------------------------------------------------------------------------- /src/ontology/ex.ts: -------------------------------------------------------------------------------- 1 | import { createNS } from "@ontologies/core"; 2 | 3 | const ex = createNS("http://example.com/ns#"); 4 | 5 | export default { 6 | ns: ex, 7 | }; 8 | -------------------------------------------------------------------------------- /src/ontology/example.ts: -------------------------------------------------------------------------------- 1 | import { createNS } from "@ontologies/core"; 2 | 3 | const example = createNS("http://example.com/"); 4 | 5 | export default { 6 | ns: example, 7 | }; 8 | -------------------------------------------------------------------------------- /src/ontology/http.ts: -------------------------------------------------------------------------------- 1 | import { createNS } from "@ontologies/core"; 2 | 3 | const http = createNS("http://www.w3.org/2011/http#"); 4 | 5 | export default { 6 | ns: http, 7 | 8 | /* properties */ 9 | status: http("status"), 10 | statusCode: http("statusCode"), 11 | }; 12 | -------------------------------------------------------------------------------- /src/ontology/http07.ts: -------------------------------------------------------------------------------- 1 | import { createNS } from "@ontologies/core"; 2 | 3 | const http = createNS("http://www.w3.org/2007/ont/http#"); 4 | 5 | export default { 6 | ns: http, 7 | 8 | /* properties */ 9 | status: http("status"), 10 | statusCode: http("statusCode"), 11 | }; 12 | -------------------------------------------------------------------------------- /src/ontology/httph.ts: -------------------------------------------------------------------------------- 1 | import { createNS } from "@ontologies/core"; 2 | 3 | const httph = createNS("http://www.w3.org/2007/ont/httph#"); 4 | 5 | export default { 6 | "ns": httph, 7 | 8 | /* properties */ 9 | "Exec-Action": httph("Exec-Action"), 10 | "date": httph("date"), 11 | "status": httph("status"), 12 | }; 13 | -------------------------------------------------------------------------------- /src/ontology/ld.ts: -------------------------------------------------------------------------------- 1 | import { createNS } from "@ontologies/core"; 2 | 3 | const ld = createNS("http://purl.org/linked-delta/"); 4 | 5 | export default { 6 | ns: ld, 7 | 8 | /** 9 | * Adds the statement to the store, without duplication. 10 | */ 11 | // eslint-disable-next-line sort-keys 12 | add: ld("add"), 13 | /** 14 | * Removes the entire subject from the store. 15 | */ 16 | purge: ld("purge"), 17 | /*** 18 | * Removes all (subject,predicate,) matches from the store. 19 | * 20 | * @see slice 21 | */ 22 | remove: ld("remove"), 23 | /*** 24 | * Replaces the (subject, predicate,) with the one(s) in this delta. 25 | */ 26 | replace: ld("replace"), 27 | /** 28 | * Removes all (subject,predicate,object) matches from the store. 29 | * 30 | * @see remove 31 | */ 32 | slice: ld("slice"), 33 | /** 34 | * Removes all statements of (subject,,) from the store and replaces them with those in the delta. 35 | */ 36 | supplant: ld("supplant"), 37 | }; 38 | -------------------------------------------------------------------------------- /src/ontology/link.ts: -------------------------------------------------------------------------------- 1 | import { createNS } from "@ontologies/core"; 2 | 3 | const link = createNS("http://www.w3.org/2007/ont/link#"); 4 | 5 | export default { 6 | ns: link, 7 | 8 | Document: link("Document"), 9 | 10 | requestedURI: link("requestedURI"), 11 | response: link("response"), 12 | }; 13 | -------------------------------------------------------------------------------- /src/ontology/ll.ts: -------------------------------------------------------------------------------- 1 | import { createNS } from "@ontologies/core"; 2 | 3 | const ll = createNS("http://purl.org/link-lib/"); 4 | 5 | export default { 6 | ns: ll, 7 | 8 | defaultTopology: ll("defaultTopology"), 9 | graph: ll("graph"), 10 | meta: ll("meta"), 11 | nop: ll("nop"), 12 | targetResource: ll("targetResource"), 13 | typeRenderClass: ll("typeRenderClass"), 14 | 15 | ClientError: ll("ClientError"), 16 | ErrorResource: ll("ErrorResource"), 17 | ServerError: ll("ServerError"), 18 | 19 | /** @deprecated */ 20 | add: ll("add"), 21 | /** @deprecated */ 22 | purge: ll("purge"), 23 | /** @deprecated */ 24 | remove: ll("remove"), 25 | /** @deprecated */ 26 | replace: ll("replace"), 27 | /** @deprecated */ 28 | slice: ll("slice"), 29 | }; 30 | -------------------------------------------------------------------------------- /src/processor/DataToGraph.ts: -------------------------------------------------------------------------------- 1 | import rdfFactory, { 2 | Literal, 3 | NamedNode, 4 | Node, 5 | TermType, 6 | } from "@ontologies/core"; 7 | import * as rdf from "@ontologies/rdf"; 8 | 9 | import ll from "../ontology/ll"; 10 | import { RDFAdapter } from "../store/RDFAdapter"; 11 | 12 | import { 13 | DataObject, 14 | DataTuple, 15 | NamedBlobTuple, 16 | NamespaceMap, 17 | ParsedObject, 18 | SerializableDataTypes, 19 | SomeNode, 20 | } from "../types"; 21 | import { MAIN_NODE_DEFAULT_IRI, NON_DATA_OBJECTS_CTORS } from "../utilities/constants"; 22 | import { expandProperty } from "../utilities/memoizedNamespace"; 23 | 24 | const BASE = 36; 25 | const DEC_CUTOFF = 2; 26 | const IRI_LEN = 20; 27 | 28 | function isPlainObject(o: any): o is DataObject { 29 | return typeof o === "object" 30 | && o !== null 31 | && !NON_DATA_OBJECTS_CTORS.find((c) => typeof o.prototype !== "undefined" && o instanceof c) 32 | && !Object.prototype.hasOwnProperty.call(o, "termType"); 33 | } 34 | 35 | function isIterable(o: any): o is any[] | Set { 36 | return Array.isArray(o) || o instanceof Set; 37 | } 38 | 39 | function uploadIRI(): NamedNode { 40 | return ll.ns(`blobs/a${Math.random().toString(BASE).substr(DEC_CUTOFF, IRI_LEN)}`); 41 | } 42 | 43 | /** 44 | * Converts an array to an RDF list-shaped {DataObject} for serialization. 45 | */ 46 | export function list(arr: SerializableDataTypes[]): DataObject { 47 | // @ts-ignore 48 | return arr.reduceRight((acc: DataObject, next: SerializableDataTypes) => ({ 49 | [rdf.first.toString()]: next, 50 | [rdf.rest.toString()]: acc, 51 | }), rdf.nil); 52 | } 53 | 54 | /** 55 | * Converts an array to an RDF sequence-shaped {DataObject} for serialization. 56 | */ 57 | export function seq(arr: T[], id?: SomeNode): DataObject { 58 | const base: DataObject = { [rdf.type.toString()]: rdf.Seq }; 59 | if (id) { 60 | base["@id"] = id; 61 | } 62 | 63 | return arr.reduce( 64 | (acc, next, n) => Object.assign(acc, { [rdf.ns(`_${n}`).toString()]: next }), 65 | base, 66 | ); 67 | } 68 | 69 | const isFile = (value: any): value is File => typeof File !== "undefined" && value instanceof File; 70 | 71 | /** @private */ 72 | export function processObject(subject: Node, 73 | predicate: NamedNode, 74 | datum: DataObject | SerializableDataTypes | null | undefined, 75 | store: RDFAdapter, 76 | ns?: NamespaceMap): NamedBlobTuple[] { 77 | let blobs: NamedBlobTuple[] = []; 78 | 79 | if (isIterable(datum)) { 80 | for (const subResource of datum) { 81 | blobs = blobs.concat(processObject(subject, predicate, subResource, store, ns)); 82 | } 83 | } else if (typeof datum === "string" 84 | || typeof datum === "number" 85 | || typeof datum === "boolean" 86 | || datum instanceof Date) { 87 | store.store.addField(subject.value, predicate.value, rdfFactory.literal(datum)); 88 | } else if (isFile(datum)) { 89 | const f = uploadIRI(); 90 | blobs.push([f, datum as File]); 91 | store.store.addField(subject.value, predicate.value, f); 92 | } else if (isPlainObject(datum)) { 93 | const id = datum["@id"] as SomeNode | undefined || rdfFactory.blankNode(); 94 | blobs = blobs.concat(processDataObject(id, datum, store, ns)); 95 | store.store.addField(subject.value, predicate.value, id); 96 | } else if (datum && datum.termType === TermType.NamedNode) { 97 | store.store.addField(subject.value, predicate.value, rdfFactory.namedNode(datum.value)); 98 | } else if (datum && datum.termType === TermType.Literal) { 99 | store.store.addField( 100 | subject.value, 101 | predicate.value, 102 | rdfFactory.literal( 103 | datum.value, 104 | (datum as Literal).language || rdfFactory.namedNode((datum as Literal).datatype.value), 105 | ), 106 | ); 107 | } else if (datum !== null && datum !== undefined) { 108 | store.add(subject, predicate, rdfFactory.literal(datum)); 109 | } 110 | 111 | return blobs; 112 | } 113 | 114 | function processDataObject(subject: Node, data: DataObject, store: RDFAdapter, ns?: NamespaceMap): NamedBlobTuple[] { 115 | let blobs: NamedBlobTuple[] = []; 116 | const keys = Object.keys(data); 117 | for (const key of keys) { 118 | if (key === "@id") { continue; } 119 | const predicate = expandProperty(key, ns || {}); 120 | const datum = data[key]; 121 | 122 | if (predicate === undefined) { 123 | throw new Error(`Unknown predicate ${key} given (for subject '${subject}').`); 124 | } 125 | 126 | blobs = blobs.concat(processObject(subject, predicate, datum, store, ns)); 127 | } 128 | 129 | return blobs; 130 | } 131 | 132 | export function dataToGraphTuple(data: DataObject, ns?: NamespaceMap): DataTuple { 133 | const store = new RDFAdapter(); 134 | const blobs = processDataObject(MAIN_NODE_DEFAULT_IRI, data, store, ns); 135 | 136 | return [store, blobs]; 137 | } 138 | 139 | /** 140 | * Convert a DataObject into a graph. Useful for writing test data in semi-plain JS objects 141 | * @param iriOrData The data object or an iri for the top-level object. 142 | * @param data The data object if an IRI was passed. 143 | * @param store A graph to write the statements into. 144 | * @param ns Namespace mapping for converting shortened keys. 145 | */ 146 | export function toGraph( 147 | iriOrData: SomeNode | DataObject, 148 | data?: DataObject, 149 | store?: RDFAdapter, 150 | ns?: NamespaceMap, 151 | ): ParsedObject { 152 | 153 | const passedIRI = iriOrData.termType === TermType.BlankNode || iriOrData.termType === TermType.NamedNode; 154 | if (passedIRI && !data) { 155 | throw new TypeError("Only an IRI was passed to `toObject`, a valid data object has to be the second argument"); 156 | } 157 | const embeddedIRI = ((passedIRI ? data : iriOrData) as DataObject)!["@id"]; 158 | let iri; 159 | if (embeddedIRI) { 160 | if (typeof embeddedIRI !== "string") { 161 | throw new TypeError("Embedded IRI (`@id`) value must be of type string"); 162 | } 163 | iri = rdfFactory.namedNode(embeddedIRI); 164 | } else { 165 | iri = passedIRI ? (iriOrData as SomeNode) : rdfFactory.blankNode(); 166 | } 167 | const dataObj = passedIRI ? data! : (iriOrData as DataObject); 168 | 169 | const s = store || new RDFAdapter(); 170 | 171 | const blobs = processDataObject(iri, dataObj, s, ns); 172 | 173 | return [iri, s, blobs]; 174 | } 175 | -------------------------------------------------------------------------------- /src/processor/ProcessorError.ts: -------------------------------------------------------------------------------- 1 | export class ProcessorError extends Error { 2 | public response?: Response; 3 | 4 | constructor(msg: string, response?: Response) { 5 | super(msg); 6 | this.response = response; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/processor/RequestInitGenerator.ts: -------------------------------------------------------------------------------- 1 | export interface RequestInitGeneratorOpts { 2 | credentials: "include" | "same-origin" | "omit" | undefined; 3 | csrfFieldName: string; 4 | headers?: { [k: string]: string }; 5 | mode: "same-origin" | "navigate" | "no-cors" | "cors" | undefined; 6 | xRequestedWith: string; 7 | } 8 | 9 | export class RequestInitGenerator { 10 | public readonly credentials: "include" | "same-origin" | "omit" | undefined; 11 | public readonly csrfFieldName: string; 12 | public readonly baseHeaders: { [k: string]: string }; 13 | public readonly mode: "same-origin" | "navigate" | "no-cors" | "cors" | undefined; 14 | public readonly xRequestedWith: string; 15 | 16 | constructor(opts: Partial = {}) { 17 | this.baseHeaders = opts.headers || {}; 18 | this.csrfFieldName = opts.csrfFieldName ?? "csrf-token"; 19 | this.credentials = opts.credentials ?? "include"; 20 | this.mode = opts.mode ?? "same-origin"; 21 | this.xRequestedWith = opts.xRequestedWith ?? "XMLHttpRequest"; 22 | } 23 | 24 | public authenticityHeader(options = {}): Record { 25 | return Object.assign({}, options, { 26 | "X-CSRF-Token": this.getAuthenticityToken(), 27 | "X-Requested-With": this.xRequestedWith, 28 | }); 29 | } 30 | 31 | public generate(method = "GET", accept = "text/turtle", body?: BodyInit|null): RequestInit { 32 | const isFormEncoded = body instanceof URLSearchParams; 33 | const headers = this.getHeaders(accept); 34 | if (isFormEncoded) { 35 | headers["Content-Type"] = "application/x-www-form-urlencoded; charset=UTF-8"; 36 | } 37 | 38 | return { 39 | body: isFormEncoded ? body!.toString() : body, 40 | credentials: this.credentials, 41 | headers, 42 | method: method.toUpperCase(), 43 | mode: this.mode, 44 | }; 45 | } 46 | 47 | private getHeaders(accept: string): Record { 48 | const acceptHeader = Object.assign({}, this.baseHeaders, { Accept: accept }); 49 | if (this.credentials === "include" || this.credentials === "same-origin") { 50 | return this.authenticityHeader(acceptHeader); 51 | } 52 | 53 | return acceptHeader; 54 | } 55 | 56 | private getMetaContent(name: string): string | null { 57 | const header = document.head && document.head.querySelector(`meta[name="${name}"]`); 58 | return header && header.getAttribute("content"); 59 | } 60 | 61 | private getAuthenticityToken(): string { 62 | return this.getMetaContent(this.csrfFieldName) || ""; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/processor/__tests__/ProcessorError.spec.ts: -------------------------------------------------------------------------------- 1 | /* @globals set, generator, init */ 2 | import "../../__tests__/useFactory"; 3 | 4 | import "jest"; 5 | 6 | import { ProcessorError } from "../ProcessorError"; 7 | 8 | const getMessage = (msg: string, response?: Response): ProcessorError => new ProcessorError(msg, response); 9 | 10 | const r = new Response(); 11 | 12 | describe("ProcessorError", () => { 13 | describe("with message", () => { 14 | it("has a message", () => { 15 | expect(getMessage("info", undefined)).toHaveProperty("message", "info"); 16 | expect(getMessage("info", undefined)).not.toHaveProperty("response", r); 17 | }); 18 | }); 19 | 20 | describe("with response", () => { 21 | it("has a message", () => { 22 | expect(getMessage("info", r)).toHaveProperty("response", r); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/processor/__tests__/RequestInitGenerator.spec.ts: -------------------------------------------------------------------------------- 1 | /* @globals set, generator, init */ 2 | import "jest"; 3 | 4 | import { RequestInitGenerator, RequestInitGeneratorOpts } from "../RequestInitGenerator"; 5 | 6 | const getGenerator = (opts?: Partial): RequestInitGenerator => 7 | new RequestInitGenerator(opts as RequestInitGeneratorOpts); 8 | 9 | describe("RequestInitGenerator", () => { 10 | describe("#constructor", () => { 11 | it("sets the mode", () => { 12 | const subject = getGenerator({ csrfFieldName: "custom-element ", mode: "no-cors" }); 13 | expect(subject).toHaveProperty("mode", "no-cors"); 14 | }); 15 | }); 16 | 17 | describe("#authenticityHeader", () => { 18 | it("has the correct X-Requested-With header", () => { 19 | expect(getGenerator()).toHaveProperty("xRequestedWith", "XMLHttpRequest"); 20 | }); 21 | 22 | it("has no X-CSRF-Token header", () => { 23 | expect(getGenerator()).not.toHaveProperty("X-CSRF-Token"); 24 | }); 25 | }); 26 | 27 | describe("#generate", () => { 28 | describe("with empty parameters", () => { 29 | const subject = getGenerator().generate(undefined, undefined); 30 | 31 | it("sets the credentials option", () => expect(subject).toHaveProperty("credentials", "include")); 32 | it("sets the method option", () => expect(subject).toHaveProperty("method", "GET")); 33 | it("sets the mode option", () => expect(subject).toHaveProperty("mode", "same-origin")); 34 | 35 | const headers = subject.headers; 36 | it("sets the Accept header", () => expect(headers).toHaveProperty("Accept", "text/turtle")); 37 | it("sets the X-Requested-With header", () => { 38 | expect(headers).toHaveProperty("X-Requested-With", "XMLHttpRequest"); 39 | }); 40 | }); 41 | 42 | describe("with arguments", () => { 43 | const subject = getGenerator().generate("POST", "application/n-quads"); 44 | 45 | it("sets the method option", () => expect(subject).toHaveProperty("method", "POST")); 46 | 47 | const headers = subject.headers; 48 | it("sets the Accept header", () => expect(headers).toHaveProperty("Accept", "application/n-quads")); 49 | 50 | it("sets the CSRF header", () => expect(headers).toHaveProperty("X-CSRF-Token")); 51 | }); 52 | 53 | describe("without credentials", () => { 54 | const subject = getGenerator({ credentials: "omit" }).generate("POST", "application/n-quads"); 55 | 56 | it("sets the method option", () => expect(subject).toHaveProperty("method", "POST")); 57 | 58 | const headers = subject.headers; 59 | it("sets the Accept header", () => expect(headers).toHaveProperty("Accept", "application/n-quads")); 60 | 61 | it("skips the CSRF header", () => expect(headers).not.toHaveProperty("X-CSRF-Token")); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /src/rdf.ts: -------------------------------------------------------------------------------- 1 | import { NamedNode, Node, SomeTerm } from "@ontologies/core"; 2 | 3 | export type OptionalNode = Node | null; 4 | 5 | export type OptionalNamedNode = NamedNode | null; 6 | 7 | export type OptionalTerm = SomeTerm | null; 8 | -------------------------------------------------------------------------------- /src/schema/__tests__/rdfs.spec.ts: -------------------------------------------------------------------------------- 1 | import "../../__tests__/useFactory"; 2 | 3 | import rdfFactory, { Node } from "@ontologies/core"; 4 | import * as rdfs from "@ontologies/rdfs"; 5 | import * as schemaNS from "@ontologies/schema"; 6 | import "jest"; 7 | 8 | import { RDFStore } from "../../RDFStore"; 9 | import { Schema } from "../../Schema"; 10 | import { VocabularyProcessingContext } from "../../types"; 11 | import { RDFS } from "../rdfs"; 12 | 13 | describe("RDFS", () => { 14 | const expectSuperMap = (ctx: VocabularyProcessingContext, mapItem: Node, equalValues: Node[]): void => { 15 | expect(ctx.superMap.get(mapItem.value)) 16 | .toEqual(new Set(equalValues.map((v) => v.value))); 17 | }; 18 | 19 | describe("#processStatement", () => { 20 | it("adds superclasses to the superMap", () => { 21 | const schema = new Schema(new RDFStore()); 22 | 23 | const ctx = schema.getProcessingCtx(); 24 | 25 | expect(ctx.superMap.get(schemaNS.CreativeWork.value)).toBeUndefined(); 26 | 27 | RDFS.processStatement( 28 | schemaNS.BlogPosting.value, 29 | rdfs.subClassOf.value, 30 | schemaNS.CreativeWork, 31 | ctx, 32 | ); 33 | expectSuperMap(ctx, schemaNS.BlogPosting, [ 34 | schemaNS.BlogPosting, 35 | schemaNS.CreativeWork, 36 | rdfs.Resource, 37 | ]); 38 | 39 | RDFS.processStatement( 40 | schemaNS.CreativeWork.value, 41 | rdfs.subClassOf.value, 42 | schemaNS.Thing, 43 | ctx, 44 | ); 45 | expectSuperMap(ctx, schemaNS.CreativeWork, [ 46 | schemaNS.CreativeWork, 47 | schemaNS.Thing, 48 | rdfs.Resource, 49 | ]); 50 | expectSuperMap(ctx, schemaNS.BlogPosting, [ 51 | schemaNS.BlogPosting, 52 | schemaNS.CreativeWork, 53 | schemaNS.Thing, 54 | rdfs.Resource, 55 | ]); 56 | }); 57 | }); 58 | 59 | describe("#processType", () => { 60 | /** 61 | * "All other classes are subclasses of this class" 62 | * https://www.w3.org/TR/2014/REC-rdf-schema-20140225/#ch_resource 63 | */ 64 | it("marks the resource to be a subclass of rdfs:Resource", () => { 65 | const schema = new Schema(new RDFStore()); 66 | 67 | const ctx = schema.getProcessingCtx(); 68 | expect(ctx.superMap.get(rdfFactory.id(schemaNS.CreativeWork))).toBeUndefined(); 69 | RDFS.processType(schemaNS.CreativeWork.value, ctx); 70 | 71 | expectSuperMap( 72 | ctx, 73 | schemaNS.CreativeWork, 74 | [ 75 | schemaNS.CreativeWork, 76 | rdfs.Resource, 77 | ], 78 | ); 79 | }); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /src/schema/owl.ts: -------------------------------------------------------------------------------- 1 | import { SomeTerm } from "@ontologies/core"; 2 | import { sameAs } from "@ontologies/owl"; 3 | 4 | import { Id } from "../datastrucures/DataSlice"; 5 | import { VocabularyProcessingContext, VocabularyProcessor } from "../types"; 6 | 7 | const nsOWLsameAs = sameAs.value; 8 | 9 | export const OWL: VocabularyProcessor = { 10 | axioms: [], 11 | 12 | processStatement( 13 | recordId: Id, 14 | field: Id, 15 | value: SomeTerm, 16 | ctx: VocabularyProcessingContext, 17 | ): void { 18 | if (field === nsOWLsameAs && recordId !== value.value) { 19 | const a = ctx.equivalenceSet.add(value.value); 20 | const b = ctx.equivalenceSet.add(recordId); 21 | ctx.equivalenceSet.union(a, b); 22 | } 23 | }, 24 | 25 | processType(_: Id, __: VocabularyProcessingContext): boolean { 26 | return false; 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /src/schema/rdfs.ts: -------------------------------------------------------------------------------- 1 | import rdfFactory, { NamedNode, SomeTerm, TermType } from "@ontologies/core"; 2 | import * as rdf from "@ontologies/rdf"; 3 | import * as rdfs from "@ontologies/rdfs"; 4 | 5 | import { Id } from "../datastrucures/DataSlice"; 6 | import { VocabularyProcessingContext, VocabularyProcessor } from "../types"; 7 | 8 | const defaultGraph: NamedNode = rdfFactory.defaultGraph(); 9 | 10 | /** 11 | * Implements the RDF/RDFS axioms and rules. 12 | */ 13 | export const RDFS: VocabularyProcessor = { 14 | axioms: [ 15 | [rdf.type, rdfs.domain, rdfs.Resource, defaultGraph], 16 | [rdfs.domain, rdfs.domain, rdf.Property, defaultGraph], 17 | [rdfs.range, rdfs.domain, rdf.Property, defaultGraph], 18 | [rdfs.subPropertyOf, rdfs.domain, rdf.Property, defaultGraph], 19 | [rdfs.subClassOf, rdfs.domain, rdfs.Class, defaultGraph], 20 | [rdf.subject, rdfs.domain, rdf.Statement, defaultGraph], 21 | [rdf.predicate, rdfs.domain, rdf.Statement, defaultGraph], 22 | [rdf.object, rdfs.domain, rdf.Statement, defaultGraph], 23 | [rdfs.member, rdfs.domain, rdfs.Resource, defaultGraph], 24 | [rdf.first, rdfs.domain, rdf.List, defaultGraph], 25 | [rdf.rest, rdfs.domain, rdf.List, defaultGraph], 26 | [rdfs.seeAlso, rdfs.domain, rdfs.Resource, defaultGraph], 27 | [rdfs.isDefinedBy, rdfs.domain, rdfs.Resource, defaultGraph], 28 | [rdfs.comment, rdfs.domain, rdfs.Resource, defaultGraph], 29 | [rdfs.label, rdfs.domain, rdfs.Resource, defaultGraph], 30 | [rdf.value, rdfs.domain, rdfs.Resource, defaultGraph], 31 | 32 | [rdf.type, rdfs.range, rdfs.Class, defaultGraph], 33 | [rdfs.domain, rdfs.range, rdfs.Class, defaultGraph], 34 | [rdfs.range, rdfs.range, rdfs.Class, defaultGraph], 35 | [rdfs.subPropertyOf, rdfs.range, rdf.Property, defaultGraph], 36 | [rdfs.subClassOf, rdfs.range, rdfs.Class, defaultGraph], 37 | [rdf.subject, rdfs.range, rdfs.Resource, defaultGraph], 38 | [rdf.predicate, rdfs.range, rdfs.Resource, defaultGraph], 39 | [rdf.object, rdfs.range, rdfs.Resource, defaultGraph], 40 | [rdfs.member, rdfs.range, rdfs.Resource, defaultGraph], 41 | [rdf.first, rdfs.range, rdfs.Resource, defaultGraph], 42 | [rdf.rest, rdfs.range, rdf.List, defaultGraph], 43 | [rdfs.seeAlso, rdfs.range, rdfs.Resource, defaultGraph], 44 | [rdfs.isDefinedBy, rdfs.range, rdfs.Resource, defaultGraph], 45 | [rdfs.comment, rdfs.range, rdfs.Literal, defaultGraph], 46 | [rdfs.label, rdfs.range, rdfs.Literal, defaultGraph], 47 | [rdf.value, rdfs.range, rdfs.Resource, defaultGraph], 48 | 49 | [rdf.Alt, rdfs.subClassOf, rdfs.Container, defaultGraph], 50 | [rdf.Bag, rdfs.subClassOf, rdfs.Container, defaultGraph], 51 | [rdf.Seq, rdfs.subClassOf, rdfs.Container, defaultGraph], 52 | [rdfs.ContainerMembershipProperty, rdfs.subClassOf, rdf.Property, defaultGraph], 53 | 54 | [rdfs.isDefinedBy, rdfs.subPropertyOf, rdfs.seeAlso, defaultGraph], 55 | 56 | [rdfs.Datatype, rdfs.subClassOf, rdfs.Class, defaultGraph], 57 | 58 | [rdfs.Resource, rdf.type, rdfs.Class, defaultGraph], 59 | [rdfs.Class, rdf.type, rdfs.Class, defaultGraph], 60 | ], 61 | 62 | processStatement( 63 | recordId: Id, 64 | field: Id, 65 | value: SomeTerm, 66 | ctx: VocabularyProcessingContext, 67 | ): void { 68 | switch (field) { 69 | case rdfs.subClassOf.value: { // C1 rdfs:subClassOf C2 70 | const objectType = value.termType; 71 | if (!(objectType === TermType.NamedNode || objectType === TermType.BlankNode)) { 72 | throw new Error("Object of subClassOf statement must be a NamedNode"); 73 | } 74 | 75 | const iSubject = recordId; 76 | const iObject = value.value; 77 | if (!ctx.superMap.has(iObject)) { 78 | ctx.superMap.set(iObject, new Set([rdfs.Resource.value])); 79 | } 80 | 81 | let parents = ctx.superMap.get(iObject); 82 | if (parents === undefined) { 83 | parents = new Set(); 84 | ctx.superMap.set(iObject, parents); 85 | } 86 | parents.add(iObject); 87 | const itemVal = ctx.superMap.get(iSubject) || new Set([iSubject]); 88 | 89 | parents.forEach((i) => itemVal.add(i)); 90 | 91 | ctx.superMap.set(iSubject, itemVal); 92 | ctx.superMap.forEach((v, k) => { 93 | if (k !== iSubject && v.has(iSubject)) { 94 | itemVal.forEach(v.add, v); 95 | } 96 | }); 97 | break; 98 | } 99 | } 100 | }, 101 | 102 | processType(type: Id, ctx: VocabularyProcessingContext): boolean { 103 | RDFS.processStatement(type, rdfs.subClassOf.value, rdfs.Resource, ctx); 104 | return false; 105 | }, 106 | }; 107 | -------------------------------------------------------------------------------- /src/store/RDFAdapter.ts: -------------------------------------------------------------------------------- 1 | import rdfFactory, { 2 | DataFactory, 3 | isNode, 4 | NamedNode, 5 | Quad, 6 | QuadPosition, 7 | Quadruple, 8 | SomeTerm, 9 | } from "@ontologies/core"; 10 | import { sameAs } from "@ontologies/owl"; 11 | 12 | import { DataRecord, Id } from "../datastrucures/DataSlice"; 13 | import { SomeNode } from "../types"; 14 | import { isGlobalId, isLocalId } from "../utilities/slices"; 15 | 16 | import { idField, StructuredStore } from "./StructuredStore"; 17 | 18 | const EMPTY_ST_ARR: ReadonlyArray = Object.freeze([]); 19 | 20 | export interface RDFAdapterOpts { 21 | data?: Record; 22 | quads: Quadruple[]; 23 | dataCallback: (quad: Quadruple) => void; 24 | onChange: (docId: string) => void; 25 | rdfFactory: DataFactory; 26 | } 27 | 28 | export class RDFAdapter { 29 | public readonly rdfFactory: DataFactory; 30 | 31 | public readonly recordCallbacks: Array<(recordId: Id) => void>; 32 | 33 | /** @private */ 34 | public store: StructuredStore; 35 | /** @private */ 36 | public storeGraph: NamedNode; 37 | 38 | constructor(opts: Partial = {}) { 39 | this.recordCallbacks = []; 40 | this.store = new StructuredStore( 41 | "rdf:defaultGraph", 42 | opts.data, 43 | (recordId: Id): void => { 44 | if (opts.onChange) { 45 | opts.onChange(recordId); 46 | } 47 | 48 | this.recordCallbacks.forEach((cb) => cb(recordId)); 49 | }, 50 | ); 51 | this.rdfFactory = opts.rdfFactory ?? rdfFactory; 52 | opts.quads?.forEach((q) => this.add( 53 | q[QuadPosition.subject], 54 | q[QuadPosition.predicate], 55 | q[QuadPosition.object], 56 | q[QuadPosition.graph], 57 | )); 58 | this.addRecordCallback(this.handleAlias.bind(this)); 59 | this.storeGraph = this.rdfFactory.namedNode(this.store.base); 60 | } 61 | 62 | /** @deprecated */ 63 | public get quads(): Quadruple[] { 64 | const qdrs = []; 65 | const data = this.store.data; 66 | 67 | for (const recordId in data) { 68 | if (!data.hasOwnProperty(recordId)) { 69 | continue; 70 | } 71 | 72 | qdrs.push(...this.quadsForRecord(recordId)); 73 | } 74 | 75 | return qdrs; 76 | } 77 | 78 | /** @deprecated */ 79 | public add( 80 | subject: SomeNode, 81 | predicate: NamedNode, 82 | object: SomeTerm, 83 | _graph: SomeNode = this.rdfFactory.defaultGraph(), 84 | ): Quadruple { 85 | const asQuadruple: Quadruple = [subject, predicate, object, _graph]; 86 | 87 | this.store.addField(subject.value, predicate.value, object); 88 | 89 | return asQuadruple; 90 | } 91 | 92 | public addRecordCallback(callback: (recordId: Id) => void): void { 93 | this.recordCallbacks.push(callback); 94 | } 95 | 96 | public deleteRecord(subject: SomeNode): void { 97 | this.store.deleteRecord(subject.value); 98 | } 99 | 100 | /** 101 | * Remove a quad from the store 102 | * @deprecated 103 | */ 104 | public remove(st: Quadruple): this { 105 | const value = this.store.getField(st[QuadPosition.subject].value, st[QuadPosition.predicate].value); 106 | if (value === undefined) { 107 | throw new Error(`Quad to be removed is not on store: ${st}`); 108 | } 109 | this.store.deleteFieldMatching( 110 | st[QuadPosition.subject].value, 111 | st[QuadPosition.predicate].value, 112 | st[QuadPosition.object], 113 | ); 114 | 115 | return this; 116 | } 117 | 118 | /** @deprecated */ 119 | public removeQuads(quads: Quadruple[]): this { 120 | // Ensure we don't loop over the array we're modifying. 121 | const toRemove = quads.slice(); 122 | for (const quad of toRemove) { 123 | this.remove(quad); 124 | } 125 | return this; 126 | } 127 | 128 | /** @deprecated */ 129 | public match( 130 | aSubject: SomeNode | null, 131 | aPredicate: NamedNode | null, 132 | aObject: SomeTerm | null, 133 | justOne: boolean = false, 134 | ): Quadruple[] { 135 | const subject = aSubject ? this.primary(aSubject) : null; 136 | const predicate = aPredicate ? this.primary(aPredicate) as NamedNode : null; 137 | const object = aObject 138 | ? (isNode(aObject) ? this.primary(aObject) : aObject) 139 | : null; 140 | 141 | let quads: Quadruple[]; 142 | 143 | if (subject && predicate) { 144 | const value = this.store.getField(subject.value, predicate.value); 145 | 146 | if (Array.isArray(value)) { 147 | quads = value.map((v) => [subject, predicate, v, this.storeGraph]); 148 | } else if (value) { 149 | quads = [[subject, predicate, value, this.storeGraph]]; 150 | } else { 151 | quads = EMPTY_ST_ARR as unknown as Quadruple[]; 152 | } 153 | } else if (subject) { 154 | quads = this.quadsForRecord(subject.value); 155 | } else { 156 | quads = this.quads; 157 | } 158 | 159 | const filter = (q: Quadruple): boolean => 160 | (subject === null || q[QuadPosition.subject] === subject) 161 | && (predicate === null || q[QuadPosition.predicate] === predicate) 162 | && (object === null || q[QuadPosition.object] === object); 163 | 164 | if (justOne) { 165 | const res = quads.find(filter); 166 | return res ? [res] : EMPTY_ST_ARR as unknown as Quadruple[]; 167 | } 168 | 169 | return quads.filter(filter); 170 | } 171 | 172 | /** @deprecated */ 173 | public quadsForRecord(recordId: Id): Quadruple[] { 174 | const factory = this.rdfFactory; 175 | const record = this.store.getRecord(recordId); 176 | 177 | if (record === undefined) { 178 | return EMPTY_ST_ARR as unknown as Quadruple[]; 179 | } 180 | 181 | const subject = isGlobalId(recordId) 182 | ? factory.namedNode(recordId) 183 | : factory.blankNode(recordId); 184 | 185 | const quadruples: Quadruple[] = []; 186 | 187 | for (const field in record) { 188 | if (!record.hasOwnProperty(field) || field === idField) { 189 | continue; 190 | } 191 | 192 | const value = record[field]; 193 | const fieldTerm = factory.namedNode(field); 194 | if (Array.isArray(value)) { 195 | for (const v of value) { 196 | quadruples.push([subject, fieldTerm, v, this.storeGraph] as Quadruple); 197 | } 198 | } else { 199 | quadruples.push([subject, fieldTerm, value, this.storeGraph] as Quadruple); 200 | } 201 | } 202 | 203 | return quadruples; 204 | } 205 | 206 | /** @private */ 207 | public primary(node: SomeNode): SomeNode { 208 | const p = this.store.primary(node.value); 209 | 210 | if (isLocalId(p)) { 211 | return this.rdfFactory.blankNode(p); 212 | } else { 213 | return this.rdfFactory.namedNode(p); 214 | } 215 | } 216 | 217 | private handleAlias(recordId: Id): void { 218 | const rawRecord = this.store.data[recordId]; 219 | if (rawRecord === undefined) { 220 | return; 221 | } 222 | const sameAsValue = rawRecord[sameAs.value]; 223 | if (sameAsValue) { 224 | this.store.setAlias( 225 | rawRecord._id.value, 226 | (Array.isArray(sameAsValue) ? sameAsValue[0] : sameAsValue).value, 227 | ); 228 | } 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/store/RecordJournal.ts: -------------------------------------------------------------------------------- 1 | import { Id } from "../datastrucures/DataSlice"; 2 | 3 | import { RecordState } from "./RecordState"; 4 | import { RecordStatus } from "./RecordStatus"; 5 | 6 | const doc = (recordId: Id): Id => { 7 | return recordId.split("#")[0]; 8 | }; 9 | 10 | const absentStatus: RecordStatus = { 11 | current: RecordState.Absent, 12 | lastUpdate: -1, 13 | previous: RecordState.Absent, 14 | }; 15 | 16 | // tslint:disable member-ordering 17 | export class RecordJournal { 18 | private readonly data: Record = {}; 19 | private readonly onChange: (docId: string) => void = () => undefined; 20 | 21 | constructor(onChange: (docId: string) => void, data?: Record | undefined) { 22 | this.onChange = onChange; 23 | if (data) { 24 | this.data = data; 25 | } 26 | } 27 | 28 | public copy(onChange: ((docId: string) => void) | null = null): RecordJournal { 29 | return new RecordJournal(onChange ?? this.onChange, JSON.parse(JSON.stringify(this.data))); 30 | } 31 | 32 | /** 33 | * Get the [RecordStatus] for the [recordId]. 34 | * Will return an invalid status when passing a local id. 35 | */ 36 | public get(recordId: Id): RecordStatus { 37 | return this.data[doc(recordId)] ?? absentStatus; 38 | } 39 | 40 | public touch(recordId: Id): void { 41 | const docId = doc(recordId); 42 | if (this.data[docId] === undefined) { 43 | this.data[docId] = { 44 | current: RecordState.Absent, 45 | lastUpdate: Date.now(), 46 | previous: RecordState.Absent, 47 | }; 48 | } else { 49 | this.data[docId].lastUpdate = Date.now(); 50 | } 51 | this.onChange(docId); 52 | this.onChange(recordId); 53 | } 54 | 55 | public transition(recordId: Id, state: RecordState): void { 56 | const docId = doc(recordId); 57 | const existing = this.data[docId]; 58 | const previous = existing !== undefined 59 | ? existing.current 60 | : RecordState.Absent; 61 | 62 | this.data[docId] = { 63 | current: state, 64 | lastUpdate: Date.now(), 65 | previous, 66 | }; 67 | this.onChange(docId); 68 | if (docId !== recordId) { 69 | this.onChange(recordId); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/store/RecordState.ts: -------------------------------------------------------------------------------- 1 | export enum RecordState { 2 | Absent, 3 | Queued, 4 | Requested, 5 | Receiving, 6 | Present, 7 | } 8 | -------------------------------------------------------------------------------- /src/store/RecordStatus.ts: -------------------------------------------------------------------------------- 1 | import { RecordState } from "./RecordState"; 2 | 3 | export interface RecordStatus { 4 | lastUpdate: number; 5 | current: RecordState; 6 | previous: RecordState; 7 | } 8 | -------------------------------------------------------------------------------- /src/store/StructuredStore/references.ts: -------------------------------------------------------------------------------- 1 | import { TermType } from "@ontologies/core"; 2 | 3 | import { DataRecord, DataSlice, Id } from "../../datastrucures/DataSlice"; 4 | import { FieldValue } from "../../datastrucures/Fields"; 5 | import { idField } from "../StructuredStore"; 6 | 7 | export const fieldReferences = (values: FieldValue, referenced: Id): boolean => { 8 | if (Array.isArray(values)) { 9 | for (const value of values) { 10 | if (value.termType !== TermType.Literal && value.value === referenced) { 11 | return true; 12 | } 13 | } 14 | } else { 15 | if (values.termType !== TermType.Literal && values.value === referenced) { 16 | return true; 17 | } 18 | } 19 | 20 | return false; 21 | }; 22 | 23 | export const hasReferenceTo = (record: DataRecord, referenced: Id): boolean => { 24 | for (const field in record) { 25 | if (!record.hasOwnProperty(field) || field === idField) { 26 | continue; 27 | } 28 | 29 | const values = record[field]; 30 | if (fieldReferences(values, referenced)) { 31 | return true; 32 | } 33 | } 34 | 35 | return false; 36 | }; 37 | 38 | export const findAllReferencingIds = (data: DataSlice, referenced: Id): Id[] => { 39 | const found = []; 40 | 41 | for (const id in data) { 42 | if (!data.hasOwnProperty(id)) { 43 | continue; 44 | } 45 | 46 | const record = data[id]; 47 | if (hasReferenceTo(record, referenced)) { 48 | found.push(id); 49 | } 50 | } 51 | 52 | return found; 53 | }; 54 | -------------------------------------------------------------------------------- /src/store/__tests__/RDFAdapter.spec.ts: -------------------------------------------------------------------------------- 1 | import "../../__tests__/useFactory"; 2 | 3 | import rdf, { DataFactory, NamedNode, Quad, Quadruple, Term } from "@ontologies/core"; 4 | import * as owl from "@ontologies/owl"; 5 | import * as rdfx from "@ontologies/rdf"; 6 | import * as rdfs from "@ontologies/rdfs"; 7 | import * as schema from "@ontologies/schema"; 8 | import "jest"; 9 | 10 | import { RDFAdapter } from "../RDFAdapter"; 11 | 12 | const defaultGraph: NamedNode = rdf.defaultGraph(); 13 | 14 | describe("RDFAdapter", () => { 15 | describe("constructor", () => { 16 | describe("without arguments", () => { 17 | const store = new RDFAdapter(); 18 | 19 | it("defaults dataCallbacks", () => expect(store.recordCallbacks).toHaveLength(1)); 20 | it("defaults quads", () => expect(store.quads).toEqual([])); 21 | it("defaults rdfFactory", () => expect(store.rdfFactory).toEqual(rdf)); 22 | }); 23 | 24 | describe("with arguments", () => { 25 | it("sets quads", () => { 26 | const quads: Quadruple[] = [ 27 | [schema.Person, schema.name, rdf.literal("Person"), defaultGraph], 28 | ]; 29 | const store = new RDFAdapter({ quads, rdfFactory: rdf }); 30 | 31 | expect(store.quads).toEqual(quads); 32 | }); 33 | it("sets rdfFactory", () => { 34 | const rdfFactory = { 35 | defaultGraph(): NamedNode { return rdf.namedNode("rdf:defaultGraph"); }, 36 | namedNode(v: string): NamedNode { return rdf.namedNode(v); }, 37 | quad(subject: Node, predicate: NamedNode, object: Term, graph?: NamedNode): Quad { 38 | return rdf.quad(subject, predicate, object, graph); 39 | }, 40 | } as unknown as DataFactory; 41 | const store = new RDFAdapter({ rdfFactory }); 42 | 43 | expect(store.rdfFactory).toEqual(rdfFactory); 44 | }); 45 | }); 46 | }); 47 | 48 | describe("match", () => { 49 | const store = new RDFAdapter(); 50 | store.add(schema.Person, rdfx.type, schema.Thing); 51 | store.add(schema.Person, rdfx.type, rdfs.Resource); 52 | store.add(schema.Person, rdfs.label, rdf.literal("Person class")); 53 | 54 | store.add(schema.name, rdfx.type, rdfx.Property); 55 | store.add(schema.name, rdfs.label, rdf.literal("Object name")); 56 | const blank = rdf.blankNode(); 57 | store.add(blank, schema.description, rdf.literal("The name of an object")); 58 | store.add(blank, owl.sameAs, schema.name); 59 | 60 | it("returns a all quads", () => { 61 | expect(store.match(schema.Person, rdfx.type, null)) 62 | .toEqual([ 63 | [schema.Person, rdfx.type, schema.Thing, defaultGraph], 64 | [schema.Person, rdfx.type, rdfs.Resource, defaultGraph], 65 | ]); 66 | }); 67 | 68 | it("returns a single quad", () => { 69 | const value = store.match(schema.Person, rdfx.type, null, true); 70 | expect(value) 71 | .toEqual([[schema.Person, rdfx.type, schema.Thing, defaultGraph]]); 72 | }); 73 | 74 | it("wildcards subject", () => { 75 | expect(store.match(null, rdfx.type, schema.Thing)) 76 | .toEqual([[schema.Person, rdfx.type, schema.Thing, defaultGraph]]); 77 | }); 78 | 79 | it("wildcards predicate", () => { 80 | expect(store.match(schema.Person, null, schema.Thing)) 81 | .toEqual([[schema.Person, rdfx.type, schema.Thing, defaultGraph]]); 82 | }); 83 | 84 | it("wildcards object", () => { 85 | expect(store.match(schema.Person, rdfx.type, null)) 86 | .toEqual([ 87 | [schema.Person, rdfx.type, schema.Thing, defaultGraph], 88 | [schema.Person, rdfx.type, rdfs.Resource, defaultGraph], 89 | ]); 90 | }); 91 | 92 | // it("wildcards graph", () => { 93 | // expect(store.match(schema.Person, rdfx.type, schema.Thing, null)) 94 | // .toEqual([rdf.quad(schema.Person, rdfx.type, schema.Thing)]); 95 | // }); 96 | }); 97 | 98 | describe("remove", () => { 99 | const store = new RDFAdapter(); 100 | it("throws when no quads match", () => { 101 | expect(() => { 102 | store.remove(rdf.quad()); 103 | }).toThrow(); 104 | }); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /src/store/__tests__/RDFIndex.spec.ts: -------------------------------------------------------------------------------- 1 | import "../../__tests__/useFactory"; 2 | 3 | import rdf from "@ontologies/core"; 4 | import * as owl from "@ontologies/owl"; 5 | import * as rdfx from "@ontologies/rdf"; 6 | import * as rdfs from "@ontologies/rdfs"; 7 | import * as schema from "@ontologies/schema"; 8 | import "jest"; 9 | 10 | import { RDFAdapter } from "../RDFAdapter"; 11 | 12 | describe("RDFAdapter", () => { 13 | describe("match", () => { 14 | const store = new RDFAdapter({ onChange: (): void => undefined }); 15 | store.add(schema.Person, rdfx.type, schema.Thing); 16 | store.add(schema.Person, rdfx.type, rdfs.Resource); 17 | store.add(schema.Person, rdfs.label, rdf.literal("Person class")); 18 | 19 | store.add(schema.name, rdfx.type, rdfx.Property); 20 | store.add(schema.name, rdfs.label, rdf.literal("Object name")); 21 | const blank = rdf.blankNode(); 22 | store.add(schema.name, owl.sameAs, blank); 23 | store.add(blank, schema.description, rdf.literal("The name of an object")); 24 | 25 | it ("queries through owl:sameAs", () => { 26 | expect(store.match(schema.name, schema.description, null)) 27 | .toHaveLength(1); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/store/__tests__/RecordJournal.spec.ts: -------------------------------------------------------------------------------- 1 | import "../../__tests__/useFactory"; 2 | 3 | import { RecordJournal } from "../RecordJournal"; 4 | import { RecordState } from "../RecordState"; 5 | import { RecordStatus } from "../RecordStatus"; 6 | 7 | describe("RecordJournal", () => { 8 | it("sets the seed data on initialization", () => { 9 | const initial: Record = { 10 | "/resources/4": { 11 | current: RecordState.Present, 12 | lastUpdate: 0, 13 | previous: RecordState.Absent, 14 | }, 15 | }; 16 | const journal = new RecordJournal(jest.fn(), initial); 17 | 18 | expect((journal as any).data).toBe(initial); 19 | }); 20 | 21 | it("updates local ids", () => { 22 | const journal = new RecordJournal(jest.fn()); 23 | 24 | expect(journal.get("_:b0")).toEqual({ 25 | current: RecordState.Absent, 26 | lastUpdate: -1, 27 | previous: RecordState.Absent, 28 | }); 29 | 30 | journal.transition("_:b0", RecordState.Present); 31 | 32 | expect(journal.get("_:b0").current).toEqual(RecordState.Present); 33 | expect(journal.get("_:b0").previous).toEqual(RecordState.Absent); 34 | expect(journal.get("_:b0").lastUpdate).not.toEqual(-1); 35 | }); 36 | 37 | it("copy accepts a new callback", () => { 38 | const callback = jest.fn(); 39 | const callback2 = jest.fn(); 40 | const journal = new RecordJournal(callback); 41 | 42 | expect((journal.copy() as any).onChange).toBe(callback); 43 | expect((journal.copy(callback2) as any).onChange).toBe(callback2); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/store/__tests__/deltaProcessor.spec.ts: -------------------------------------------------------------------------------- 1 | import "../../__tests__/useFactory"; 2 | 3 | import rdfFactory, { Quadruple } from "@ontologies/core"; 4 | import * as ld from "@ontologies/ld"; 5 | import * as rdf from "@ontologies/rdf"; 6 | import * as schema from "@ontologies/schema"; 7 | import "jest"; 8 | 9 | import ex from "../../ontology/ex"; 10 | import { deltaProcessor } from "../deltaProcessor"; 11 | import { RDFAdapter } from "../RDFAdapter"; 12 | 13 | describe("deltaProcessor", () => { 14 | const alice = ex.ns("person/alice"); 15 | const bob = ex.ns("person/bob"); 16 | const erin = ex.ns("person/erin"); 17 | 18 | const defaultProcessor = deltaProcessor( 19 | [ld.add], 20 | [ 21 | rdfFactory.defaultGraph(), 22 | ld.replace, 23 | rdfFactory.namedNode("chrome:theSession"), 24 | ], 25 | [ld.remove], 26 | [ld.purge], 27 | [ld.slice], 28 | ); 29 | 30 | const filledStore = (): RDFAdapter => { 31 | const store = new RDFAdapter(); 32 | 33 | store.add(bob, rdf.type, schema.Person); 34 | store.add(bob, schema.name, rdfFactory.literal("bob")); 35 | store.add(bob, schema.children, alice); 36 | store.add(bob, schema.children, ex.ns("person/charlie")); 37 | store.add(bob, schema.children, ex.ns("person/dave")); 38 | 39 | store.add(alice, rdf.type, schema.Person); 40 | store.add(alice, schema.name, rdfFactory.literal("Alice")); 41 | 42 | return store; 43 | }; 44 | const initialCount = 7; 45 | 46 | const testDelta = (delta: Quadruple[], [adds, replaces, removes]: [number, number, number]): void => { 47 | const store = filledStore(); 48 | const processor = defaultProcessor(store); 49 | 50 | const [ addable, replaceable, removable ] = processor(delta); 51 | 52 | expect(addable).toHaveLength(adds); 53 | expect(replaceable).toHaveLength(replaces); 54 | expect(removable).toHaveLength(removes); 55 | expect((store as any).quads).toHaveLength(initialCount); 56 | }; 57 | 58 | it("handles empty values", () => { 59 | const store = filledStore(); 60 | const processor = defaultProcessor(store); 61 | 62 | const [ addable, replaceable, removable ] = processor(new Array(1)); 63 | expect(addable).toEqual([]); 64 | expect(replaceable).toEqual([]); 65 | expect(removable).toEqual([]); 66 | expect((store as any).quads).toHaveLength(initialCount); 67 | }); 68 | 69 | it("requires explicit graph names", () => { 70 | expect(() => { 71 | deltaProcessor([], [], [], [], []); 72 | }).toThrow("Pass a default graph explicitly"); 73 | }); 74 | 75 | it("ignores unknown methods", () => { 76 | testDelta([ [bob, schema.children, alice, ld.ns("unknown")] ], [0, 0, 0]); 77 | }); 78 | 79 | describe("with an existing value", () => { 80 | it("add", () => { 81 | testDelta([ [bob, schema.children, alice, ld.add] ], [1, 0, 0]); 82 | }); 83 | 84 | it("replace", () => { 85 | testDelta([ [bob, schema.children, alice, ld.replace] ], [0, 1, 0]); 86 | }); 87 | 88 | it("remove", () => { 89 | testDelta([ [bob, schema.children, alice, ld.remove] ], [0, 0, 3]); 90 | }); 91 | 92 | it("purge", () => { 93 | testDelta([ [bob, schema.children, alice, ld.purge] ], [0, 0, 5]); 94 | }); 95 | 96 | it("slice", () => { 97 | testDelta([ [bob, schema.children, alice, ld.slice] ], [0, 0, 1]); 98 | }); 99 | }); 100 | 101 | describe("with a new value", () => { 102 | it("add", () => { 103 | testDelta([ [bob, schema.children, erin, ld.add] ], [1, 0, 0]); 104 | }); 105 | 106 | it("replace", () => { 107 | testDelta([ [bob, schema.children, erin, ld.replace] ], [0, 1, 0]); 108 | }); 109 | 110 | it("remove", () => { 111 | testDelta([ [bob, schema.children, erin, ld.remove] ], [0, 0, 3]); 112 | }); 113 | 114 | it("purge", () => { 115 | testDelta([ [bob, schema.children, erin, ld.purge] ], [0, 0, 5]); 116 | }); 117 | 118 | it("slice", () => { 119 | testDelta([ [bob, schema.children, erin, ld.slice] ], [0, 0, 0]); 120 | }); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /src/store/deltaProcessor.ts: -------------------------------------------------------------------------------- 1 | import rdfFactory, { 2 | NamedNode, 3 | Node, 4 | QuadPosition, 5 | Quadruple, 6 | } from "@ontologies/core"; 7 | 8 | import { equals } from "../factoryHelpers"; 9 | import { StoreProcessor, StoreProcessorResult } from "../types"; 10 | import { RDFAdapter } from "./RDFAdapter"; 11 | 12 | const matchSingle = (graphIRI: NamedNode): (graph: Node) => boolean => { 13 | const value = graphIRI.value; 14 | return (graph: Node): boolean => equals(graph, graphIRI) || graph.value.startsWith(value); 15 | }; 16 | 17 | const isInGraph = (graphIRIS: NamedNode[]): (graph: Node) => boolean => { 18 | if (graphIRIS.length === 0) { 19 | throw new Error("Pass a default graph explicitly"); 20 | } 21 | const matchers = graphIRIS.map((iri) => matchSingle(iri)); 22 | 23 | return (graph: Node): boolean => matchers.some((matcher) => matcher(graph)); 24 | }; 25 | 26 | const pushQuadruple = (arr: Quadruple[], quadruple: Quadruple, graph: NamedNode): void => { 27 | arr.push([ 28 | quadruple[QuadPosition.subject], 29 | quadruple[QuadPosition.predicate], 30 | quadruple[QuadPosition.object], 31 | graph, 32 | ]); 33 | }; 34 | 35 | export const deltaProcessor = ( 36 | addGraphIRIS: NamedNode[], 37 | replaceGraphIRIS: NamedNode[], 38 | removeGraphIRIS: NamedNode[], 39 | purgeGraphIRIS: NamedNode[], 40 | sliceGraphIRIS: NamedNode[], 41 | ): (store: RDFAdapter) => StoreProcessor => { 42 | const defaultGraph = rdfFactory.defaultGraph(); 43 | 44 | const isAdd = isInGraph(addGraphIRIS); 45 | const isReplace = isInGraph(replaceGraphIRIS); 46 | const isRemove = isInGraph(removeGraphIRIS); 47 | const isPurge = isInGraph(purgeGraphIRIS); 48 | const isSlice = isInGraph(sliceGraphIRIS); 49 | 50 | return (store: RDFAdapter): StoreProcessor => (delta: Quadruple[]): StoreProcessorResult => { 51 | const addable: Quadruple[] = []; 52 | const replaceable: Quadruple[] = []; 53 | const removable: Quadruple[] = []; 54 | 55 | for (const quad of delta) { 56 | if (!quad) { 57 | continue; 58 | } 59 | 60 | const g = new URL(quad[QuadPosition.graph].value).searchParams.get("graph"); 61 | const graph = g ? rdfFactory.termFromNQ(g) : defaultGraph; 62 | if (isAdd(quad[QuadPosition.graph])) { 63 | pushQuadruple(addable, quad, graph); 64 | } else if (isReplace(quad[QuadPosition.graph])) { 65 | pushQuadruple(replaceable, quad, graph); 66 | } else if (isRemove(quad[QuadPosition.graph])) { 67 | removable.push(...store.match( 68 | quad[QuadPosition.subject], 69 | quad[QuadPosition.predicate], 70 | null, 71 | )); 72 | } else if (isPurge(quad[QuadPosition.graph])) { 73 | removable.push(...store.match( 74 | quad[QuadPosition.subject], 75 | null, 76 | null, 77 | )); 78 | } else if (isSlice(quad[QuadPosition.graph])) { 79 | removable.push(...store.match( 80 | quad[QuadPosition.subject], 81 | quad[QuadPosition.predicate], 82 | quad[QuadPosition.object], 83 | )); 84 | } 85 | } 86 | 87 | return [ 88 | addable, 89 | replaceable, 90 | removable, 91 | ]; 92 | }; 93 | }; 94 | -------------------------------------------------------------------------------- /src/testUtilities.ts: -------------------------------------------------------------------------------- 1 | import { ComponentStore } from "./ComponentStore/ComponentStore"; 2 | import { createStore } from "./createStore"; 3 | import { Id } from "./datastrucures/DataSlice"; 4 | import { LinkedRenderStore } from "./LinkedRenderStore"; 5 | import { DataProcessor } from "./processor/DataProcessor"; 6 | import { RDFStore } from "./RDFStore"; 7 | import { Schema } from "./Schema"; 8 | import { 9 | DataProcessorOpts, 10 | LinkedRenderStoreOptions, 11 | MiddlewareActionHandler, 12 | } from "./types"; 13 | 14 | export type BasicComponent = () => string | undefined; 15 | 16 | export class ComponentStoreTestProxy extends ComponentStore { 17 | public publicLookup( 18 | predicate: Id, 19 | obj: Id, 20 | topology: Id, 21 | ): T | undefined { 22 | return this.lookup(predicate, obj, topology); 23 | } 24 | } 25 | 26 | export interface ExplodedLRS { 27 | api: DataProcessor; 28 | apiOpts: Partial; 29 | dispatch: MiddlewareActionHandler; 30 | forceBroadcast: () => Promise; 31 | processor: DataProcessor; 32 | lrs: LinkedRenderStore; 33 | mapping: ComponentStoreTestProxy; 34 | store: RDFStore; 35 | schema: Schema; 36 | } 37 | 38 | export type GetBasicStoreOpts = Partial>; 39 | 40 | export const getBasicStore = (opts: GetBasicStoreOpts = {}): ExplodedLRS => { 41 | const report = (e: unknown): void => { throw e; }; 42 | const store = opts.store ?? new RDFStore(); 43 | const processor = opts.processor ?? new DataProcessor({ report, store, ...opts.apiOpts }); 44 | const api = opts.api ?? processor; 45 | const schema = opts.schema ?? new Schema(store); 46 | const mapping = opts.mapping ?? new ComponentStoreTestProxy(schema); 47 | 48 | const conf = { 49 | api, 50 | mapping, 51 | report, 52 | schema, 53 | store, 54 | } as LinkedRenderStoreOptions; 55 | const lrs = createStore(conf); 56 | (lrs as any).resourceQueueFlushTimer = 0; 57 | 58 | return { 59 | dispatch: lrs.dispatch, 60 | forceBroadcast: (): Promise => (lrs as any).broadcast(false, 0), 61 | lrs, 62 | mapping, 63 | processor, 64 | schema, 65 | store, 66 | } as ExplodedLRS; 67 | }; 68 | -------------------------------------------------------------------------------- /src/transformers/hextuples.ts: -------------------------------------------------------------------------------- 1 | import { Quadruple } from "@ontologies/core"; 2 | import { hextuplesTransformer } from "hextuples"; 3 | 4 | import { LinkedRenderStore } from "../LinkedRenderStore"; 5 | import { 6 | ResponseAndFallbacks, 7 | ResponseTransformer, 8 | } from "../types"; 9 | 10 | export const hextupleProcessor = { 11 | acceptValue: 1.0, 12 | mediaTypes: ["application/hex+x-ndjson"], 13 | 14 | transformer: (store: LinkedRenderStore): ResponseTransformer => 15 | (res: ResponseAndFallbacks): Promise => { 16 | const isExpedited = res.hasOwnProperty("expedite") 17 | ? (res as any).expedite 18 | : false; 19 | 20 | return hextuplesTransformer(res) 21 | .then((delta) => store.queueDelta(delta, isExpedited)) 22 | .then(() => []); 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /src/transformers/index.ts: -------------------------------------------------------------------------------- 1 | import { hextupleProcessor } from "./hextuples"; 2 | import { linkedDeltaProcessor } from "./linked-delta"; 3 | import { createProcessRDF } from "./rdf-formats-common"; 4 | 5 | export const transformers = { 6 | createProcessRDF, 7 | hextupleProcessor, 8 | linkedDeltaProcessor, 9 | }; 10 | -------------------------------------------------------------------------------- /src/transformers/linked-delta.ts: -------------------------------------------------------------------------------- 1 | import { Quadruple } from "@ontologies/core"; 2 | import { NQuadsParser } from "n-quads-parser"; 3 | 4 | import { LinkedRenderStore } from "../LinkedRenderStore"; 5 | import { 6 | ExtensionResponse, 7 | RDFLibFetcherRequest, 8 | ResponseAndFallbacks, 9 | ResponseTransformer, 10 | } from "../types"; 11 | 12 | /** 13 | * Processes linked-delta responses. 14 | */ 15 | 16 | export function linkedDeltaProcessor(lrs: LinkedRenderStore): ResponseTransformer { 17 | return async function processLinkedDelta(response: ResponseAndFallbacks): Promise { 18 | let data: string; 19 | if (response instanceof Response) { 20 | data = response.bodyUsed ? "" : await response.text(); 21 | } else if (typeof XMLHttpRequest !== "undefined" && response instanceof XMLHttpRequest) { 22 | data = response.responseText; 23 | } else { 24 | data = (response as RDFLibFetcherRequest | ExtensionResponse).body; 25 | } 26 | 27 | if (!data || data.length === 0) { 28 | return []; 29 | } 30 | 31 | const parser = new NQuadsParser((lrs as any).store.getInternalStore()); 32 | const quads = parser.parseString(data) as Array; 33 | const expedite = response.hasOwnProperty("expedite") ? (response as any).expedite : false; 34 | 35 | lrs.queueDelta(quads, expedite); 36 | 37 | // TODO: Resolve the statements in this request 38 | return []; 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /src/transformers/rdf-formats-common.ts: -------------------------------------------------------------------------------- 1 | import { Quadruple } from "@ontologies/core"; 2 | import { RDFAdapter } from "../store/RDFAdapter"; 3 | import { RDFLibFetcherResponse, ResponseAndFallbacks } from "../types"; 4 | 5 | import { getContentType, getURL } from "../utilities/responses"; 6 | 7 | const isRdfLibResponse = (res: any): res is RDFLibFetcherResponse => 8 | typeof res.req !== "undefined" && typeof res.req.termType !== "undefined"; 9 | 10 | export type RDFLibParse = (str: string, 11 | kb: RDFAdapter, 12 | base: string, 13 | contentType: string, 14 | callback: () => void) => void; 15 | 16 | /** 17 | * Processes a range of media types with parsers from the 18 | * [rdflib.js package](https://www.npmjs.com/package/rdflib). 19 | */ 20 | export const createProcessRDF = (rdfParse: RDFLibParse): (response: ResponseAndFallbacks) => Promise => { 21 | return async function processRDF(response: ResponseAndFallbacks): Promise { 22 | let data: string; 23 | if (isRdfLibResponse(response)) { 24 | data = response.responseText; 25 | } else if (response instanceof Response) { 26 | data = response.bodyUsed ? "" : await response.text(); 27 | } else if (response instanceof XMLHttpRequest) { 28 | data = response.responseText; 29 | } else { 30 | data = response.body; 31 | } 32 | 33 | const format = getContentType(response); 34 | const g = new RDFAdapter(); 35 | 36 | await new Promise((resolve): void => { 37 | rdfParse(data, g, getURL(response), format, () => { 38 | resolve(); 39 | }); 40 | }); 41 | 42 | return g.quads; 43 | }; 44 | }; 45 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BlankNode, 3 | CustomPredicateCreator, 4 | Literal, 5 | NamedNode, 6 | Quadruple, SomeTerm, 7 | } from "@ontologies/core"; 8 | 9 | import { ComponentStore } from "./ComponentStore/ComponentStore"; 10 | import { DataRecord, Id } from "./datastrucures/DataSlice"; 11 | import { LinkedDataAPI } from "./LinkedDataAPI"; 12 | import { LinkedRenderStore } from "./LinkedRenderStore"; 13 | import { DataProcessor } from "./processor/DataProcessor"; 14 | import { RequestInitGenerator } from "./processor/RequestInitGenerator"; 15 | import { RDFStore } from "./RDFStore"; 16 | import { Schema } from "./Schema"; 17 | import { RDFAdapter } from "./store/RDFAdapter"; 18 | import { DisjointSet } from "./utilities/DisjointSet"; 19 | 20 | export type Optional = T | null | undefined; 21 | 22 | export type OneOrMoreOrNothing = T | T[] | undefined; 23 | 24 | export type SubscriptionCallback = (v: T, lastUpdateAt?: number) => void; 25 | 26 | export type Indexable = number | string; 27 | 28 | export interface ComponentMapping { 29 | /** The registration type, either a field identifier or the TYPE_RENDER_CLASS identifier */ 30 | [type: string]: { 31 | /** The type of the object */ 32 | [klass: string]: { 33 | [topology: string]: T, 34 | }, 35 | }; 36 | } 37 | 38 | export interface SubscriptionRegistrationBase { 39 | callback: SubscriptionCallback; 40 | index?: number; 41 | lastUpdateAt?: number; 42 | markedForDelete: boolean; 43 | subjectFilter?: string[]; 44 | subscribedAt?: number; 45 | } 46 | 47 | export interface ComponentRegistration { 48 | component: T; 49 | property: Id; 50 | topology: Id; 51 | type: Id; 52 | } 53 | 54 | export type ResponseTransformer = (response: ResponseAndFallbacks) => Promise; 55 | 56 | export interface ErrorResponse { 57 | errors?: Array<{ message: string }>; 58 | } 59 | 60 | export interface FailedResponse { 61 | message: string; 62 | res: Response | undefined; 63 | } 64 | 65 | export type ErrorReporter = (e: unknown, ...args: any) => void; 66 | 67 | export interface FetchOpts { 68 | /** Force-reload the resource discarding any previously held data. */ 69 | reload: boolean; 70 | } 71 | 72 | export type SomeNode = NamedNode | BlankNode; 73 | 74 | export interface LinkedRenderStoreOptions { 75 | api?: API | undefined; 76 | apiOpts?: Partial | undefined; 77 | data?: Record; 78 | defaultType?: NamedNode | undefined; 79 | dispatch?: MiddlewareActionHandler; 80 | mapping?: ComponentStore | undefined; 81 | rehydration?: {} | undefined; 82 | report?: ErrorReporter; 83 | schema?: Schema | undefined; 84 | store?: RDFStore | undefined; 85 | } 86 | 87 | export interface DeltaProcessor { 88 | queueDelta: (delta: Quadruple[]) => void; 89 | /** 90 | * Process all queued deltas 91 | * @note: Be sure to assign a new buffer array before starting processing to prevent infinite loops. 92 | */ 93 | flush: () => Set; 94 | processDelta: (delta: Quadruple[]) => void; 95 | } 96 | 97 | export type StoreProcessorResult = [Quadruple[], Quadruple[], Quadruple[]]; 98 | export type StoreProcessor = (delta: Quadruple[]) => StoreProcessorResult; 99 | 100 | export interface Dispatcher { 101 | dispatch: MiddlewareActionHandler; 102 | } 103 | 104 | export type MiddlewareFn = (store: LinkedRenderStore) => 105 | MiddlewareWithBoundLRS; 106 | 107 | export type MiddlewareWithBoundLRS = (next: MiddlewareActionHandler) => MiddlewareActionHandler; 108 | 109 | export type MiddlewareActionHandler = (action: SomeNode, args?: any) => Promise; 110 | 111 | export interface NamespaceMap { 112 | [k: string]: CustomPredicateCreator; 113 | } 114 | 115 | export type LazyNNArgument = NamedNode | NamedNode[]; 116 | 117 | export type NamedBlobTuple = [SomeNode, File]; 118 | 119 | export type SerializablePrimitives = boolean | DataObject | Date | File | number | string 120 | | NamedNode | BlankNode | Literal; 121 | 122 | export type SerializableDataTypes = SerializablePrimitives | SerializablePrimitives[]; 123 | 124 | export interface DataObject { 125 | [k: string]: SerializableDataTypes; 126 | } 127 | 128 | export type DataTuple = [RDFAdapter, NamedBlobTuple[]]; 129 | export type ParsedObject = [SomeNode, RDFAdapter, NamedBlobTuple[]]; 130 | 131 | export interface ChangeBuffer { 132 | changeBuffer: Quadruple[]; 133 | changeBufferCount: number; 134 | } 135 | 136 | export interface LinkedActionResponse { 137 | /** The IRI of the created resource, based from the Location header. */ 138 | iri: NamedNode | null; 139 | data: Quadruple[]; 140 | } 141 | 142 | export interface ExtensionResponse { 143 | body: string; 144 | headers: { [k: string]: string }; 145 | status: number; 146 | url: string; 147 | } 148 | 149 | export interface RDFLibFetcherResponse extends Response { 150 | responseText: string; 151 | req: BlankNode; 152 | } 153 | 154 | export interface RDFLibFetcherRequest { 155 | body: string; 156 | headers: { [k: string]: string }; 157 | requestedURI: string; 158 | status: number; 159 | } 160 | 161 | export interface RequestStatus { 162 | lastRequested: Date | null; 163 | requested: boolean; 164 | status: number | null; 165 | subject: NamedNode; 166 | timesRequested: number; 167 | } 168 | 169 | export interface EmptyRequestStatus extends RequestStatus { 170 | lastRequested: null; 171 | requested: false; 172 | status: null; 173 | timesRequested: 0; 174 | } 175 | 176 | export interface PendingRequestStatus extends RequestStatus { 177 | lastRequested: Date; 178 | lastResponseHeaders: null; 179 | requested: true; 180 | status: null; 181 | timesRequested: number; 182 | } 183 | 184 | export interface FulfilledRequestStatus extends RequestStatus { 185 | lastRequested: Date; 186 | lastResponseHeaders: BlankNode | null; 187 | requested: true; 188 | status: number; 189 | } 190 | 191 | export type SomeRequestStatus = EmptyRequestStatus | PendingRequestStatus | FulfilledRequestStatus; 192 | 193 | export type ResponseAndFallbacks = Response 194 | | XMLHttpRequest 195 | | ExtensionResponse 196 | | RDFLibFetcherRequest 197 | | RDFLibFetcherResponse; 198 | 199 | export interface WorkerMessageBase { 200 | method: string; 201 | params: object; 202 | } 203 | 204 | export interface VocabularyProcessingContext { 205 | dataStore: RDFStore; 206 | equivalenceSet: DisjointSet; 207 | superMap: Map>; 208 | store: Schema; 209 | } 210 | 211 | export interface VocabularyProcessor { 212 | axioms: Quadruple[]; 213 | 214 | processStatement: ( 215 | recordId: Id, 216 | field: Id, 217 | value: SomeTerm, 218 | ctx: VocabularyProcessingContext, 219 | ) => void; 220 | 221 | /** 222 | * Processes class instances (object to rdf:type). If an IRI is given, processors must assume the resource to be an 223 | * instance of rdfs:Class. 224 | */ 225 | processType: (type: string, ctx: VocabularyProcessingContext) => boolean; 226 | } 227 | 228 | export interface TransformerRegistrationRequest { 229 | acceptValue: number; 230 | mediaType: string | string[]; 231 | transformer: ResponseTransformer; 232 | } 233 | 234 | export interface DataProcessorOpts { 235 | accept?: { [k: string]: string }; 236 | bulkEndpoint?: string; 237 | dispatch?: MiddlewareActionHandler; 238 | requestInitGenerator?: RequestInitGenerator; 239 | fetch?: (input: RequestInfo, init?: RequestInit) => Promise; 240 | mapping?: { [k: string]: ResponseTransformer[] }; 241 | transformers?: TransformerRegistrationRequest[]; 242 | report: ErrorReporter; 243 | store: RDFStore; 244 | } 245 | 246 | export type ResourceQueueItem = [NamedNode, FetchOpts|undefined]; 247 | -------------------------------------------------------------------------------- /src/utilities.ts: -------------------------------------------------------------------------------- 1 | /* global chrome */ 2 | import rdfFactory, { 3 | BlankNode, 4 | isLiteral, 5 | Literal, 6 | NamedNode, 7 | QuadPosition, 8 | Quadruple, 9 | SomeTerm, 10 | Term, 11 | TermType, 12 | } from "@ontologies/core"; 13 | import * as rdf from "@ontologies/rdf"; 14 | import * as rdfs from "@ontologies/rdfs"; 15 | 16 | import { SomeNode } from "./types"; 17 | 18 | const memberPrefix = rdf.ns("_").value; 19 | 20 | const find = (x: SomeTerm | undefined, langPrefs: string[]): number => { 21 | const language = isLiteral(x) ? x.language : null; 22 | const index = langPrefs.findIndex((pref) => pref === language); 23 | 24 | return index !== -1 ? index : Infinity; 25 | }; 26 | 27 | /** 28 | * Filters {obj} to only include statements where the subject equals {predicate}. 29 | * @param obj The statements to filter. 30 | * @param predicate The subject to filter for. 31 | * @return A possibly empty filtered array of statements. 32 | */ 33 | export function allRDFPropertyStatements( 34 | obj: Quadruple[] | undefined, 35 | predicate: SomeNode): Quadruple[] { 36 | 37 | if (typeof obj === "undefined") { 38 | return []; 39 | } 40 | 41 | if (rdfFactory.equals(predicate, rdfs.member)) { 42 | return obj.filter((s) => 43 | rdfFactory.equals(s[QuadPosition.predicate], rdfs.member) 44 | || s[QuadPosition.predicate].value.startsWith(memberPrefix)); 45 | } 46 | 47 | return obj.filter((s) => rdfFactory.equals(s[QuadPosition.predicate], predicate)); 48 | } 49 | 50 | /** 51 | * Filters {obj} on subject {predicate} returning the resulting statements' objects. 52 | * @see allRDFPropertyStatements 53 | */ 54 | export function allRDFValues(obj: Quadruple[], predicate: SomeNode): Term[] { 55 | const props = allRDFPropertyStatements(obj, predicate); 56 | if (props.length === 0) { 57 | return []; 58 | } 59 | 60 | return props.map((s) => s[QuadPosition.object]); 61 | } 62 | 63 | /** 64 | * Resolve {predicate} to any value, if any. If present, additional values are ignored. 65 | */ 66 | export function anyRDFValue(obj: Quadruple[] | undefined, predicate: SomeNode): Term | undefined { 67 | if (!Array.isArray(obj)) { 68 | return undefined; 69 | } 70 | 71 | const match = rdfFactory.equals(predicate, rdfs.member) 72 | ? obj.find((s) => s[QuadPosition.predicate].value.startsWith(memberPrefix)) 73 | : obj.find((s) => rdfFactory.equals(s[QuadPosition.predicate], predicate)); 74 | 75 | if (typeof match === "undefined") { 76 | return undefined; 77 | } 78 | 79 | return match[QuadPosition.object]; 80 | } 81 | 82 | export function doc(iri: T): T { 83 | if (iri.value.includes("#")) { 84 | return rdfFactory.namedNode(iri.value.split("#").shift()!); 85 | } 86 | 87 | return iri; 88 | } 89 | 90 | export function getPropBestLang(rawProp: Quadruple[], langPrefs: string[]): T { 91 | if (rawProp.length === 1) { 92 | return rawProp[0][QuadPosition.object] as T; 93 | } 94 | 95 | return sortByBestLang(rawProp, langPrefs)[0][QuadPosition.object] as T; 96 | } 97 | 98 | export function getPropBestLangRaw(statements: Quadruple[], langPrefs: string[]): Quadruple { 99 | if (statements.length === 1) { 100 | return statements[0]; 101 | } 102 | 103 | return sortByBestLang(statements, langPrefs)[0]; 104 | } 105 | 106 | export function getTermBestLang(rawTerm: Term | Term[], langPrefs: string[]): Term { 107 | if (!Array.isArray(rawTerm)) { 108 | return rawTerm; 109 | } 110 | if (rawTerm.length === 1) { 111 | return rawTerm[0]; 112 | } 113 | for (const langPref of langPrefs) { 114 | const pIndex = rawTerm.findIndex((p) => "language" in p && (p as Literal).language === langPref); 115 | if (pIndex >= 0) { 116 | return rawTerm[pIndex]; 117 | } 118 | } 119 | 120 | return rawTerm[0]; 121 | } 122 | 123 | export function sortByBestLang(statements: Quadruple[], langPrefs: string[]): Quadruple[] { 124 | return statements.sort((a, b) => 125 | find(a[QuadPosition.object], langPrefs) - find(b[QuadPosition.object], langPrefs)); 126 | } 127 | 128 | /** 129 | * Checks if the origin of {href} matches current origin from {window.location} 130 | * @returns `true` if matches, `false` otherwise. 131 | */ 132 | export function isDifferentOrigin(href: SomeNode | string): boolean { 133 | if (typeof href !== "string" && href.termType === TermType.BlankNode) { 134 | return false; 135 | } 136 | const origin = typeof href !== "string" ? href.value : href; 137 | 138 | return !origin.startsWith(self.location.origin + "/"); 139 | } 140 | 141 | export function normalizeType(type: T1 | T1[]): T1[] { 142 | return Array.isArray(type) ? type : [type]; 143 | } 144 | -------------------------------------------------------------------------------- /src/utilities/DisjointSet.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * DisjointSet 3 | * Module port from https://github.com/mljs/disjoint-set/ 4 | */ 5 | 6 | /** 7 | * @class DisjointSet 8 | */ 9 | export class DisjointSet { 10 | private nodes: Map>; 11 | 12 | constructor() { 13 | this.nodes = new Map(); 14 | } 15 | 16 | /** 17 | * Adds an element as a new set 18 | * @param {*} value 19 | * @return {DisjointSetNode} Object holding the element 20 | */ 21 | public add(value: T): DisjointSetNode { 22 | let node = this.nodes.get(value); 23 | if (!node) { 24 | node = new DisjointSetNode(value); 25 | this.nodes.set(value, node); 26 | } 27 | 28 | return node; 29 | } 30 | 31 | public allValues(value: T): T[] { 32 | const node = this.nodes.get(value); 33 | if (!node) { 34 | return [value]; 35 | } 36 | let currentParent = this.find(node); 37 | const entries = this.nodes.entries(); 38 | const parents: Array> = []; 39 | const values: T[] = []; 40 | 41 | const process = (obj: DisjointSetNode): void => { 42 | for (const [k, v] of entries) { 43 | if (v.parent === obj && !parents.includes(v)) { 44 | values.push(k); 45 | parents.push(v); 46 | } 47 | } 48 | }; 49 | process(currentParent); 50 | 51 | for (let i = 0; i < parents.length; i++) { 52 | currentParent = parents[i]; 53 | process(currentParent); 54 | } 55 | 56 | return values; 57 | } 58 | 59 | /** 60 | * Merges the sets that contain x and y 61 | * @param {DisjointSetNode} x 62 | * @param {DisjointSetNode} y 63 | */ 64 | public union(x: DisjointSetNode, y: DisjointSetNode): void { 65 | const rootX = this.find(x); 66 | const rootY = this.find(y); 67 | if (rootX === rootY) { 68 | return; 69 | } 70 | if (rootX.rank < rootY.rank) { 71 | rootX.parent = rootY; 72 | } else if (rootX.rank > rootY.rank) { 73 | rootY.parent = rootX; 74 | } else { 75 | rootY.parent = rootX; 76 | rootX.rank++; 77 | } 78 | } 79 | 80 | /** 81 | * Finds and returns the root node of the set that contains node 82 | * @param {DisjointSetNode} node 83 | * @return {DisjointSetNode} 84 | */ 85 | public find(node: DisjointSetNode): DisjointSetNode { 86 | let rootX = node; 87 | while (rootX.parent !== null) { 88 | rootX = rootX.parent; 89 | } 90 | let toUpdateX = node; 91 | while (toUpdateX.parent !== null) { 92 | const toUpdateParent = toUpdateX; 93 | toUpdateX = toUpdateX.parent; 94 | toUpdateParent.parent = rootX; 95 | } 96 | 97 | return rootX; 98 | } 99 | 100 | /** 101 | * Returns true if x and y belong to the same set 102 | * @param {DisjointSetNode} x 103 | * @param {DisjointSetNode} y 104 | */ 105 | public connected(x: DisjointSetNode, y: DisjointSetNode): boolean { 106 | return this.find(x) === this.find(y); 107 | } 108 | } 109 | 110 | // tslint:disable-next-line max-classes-per-file 111 | export class DisjointSetNode { 112 | public parent: DisjointSetNode | null; 113 | public rank: number; 114 | public value: T; 115 | 116 | public constructor(value: T) { 117 | this.value = value; 118 | this.parent = null; 119 | this.rank = 0; 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /src/utilities/__tests__/memoizedNamespace.spec.ts: -------------------------------------------------------------------------------- 1 | import "../../__tests__/useFactory"; 2 | 3 | import rdfFactory, { NamedNode } from "@ontologies/core"; 4 | 5 | import ex from "../../ontology/ex"; 6 | import { expandProperty } from "../memoizedNamespace"; 7 | 8 | describe("memoizedNamespace", () => { 9 | describe("expandProperty", () => { 10 | it("returns identity when passed undefined", () => { 11 | expect(expandProperty(undefined)).toBeUndefined(); 12 | }); 13 | 14 | it("returns identity when passed NamedNode", () => { 15 | const n = rdfFactory.namedNode("http://example.com"); 16 | expect(expandProperty(n)).toEqual(n); 17 | }); 18 | 19 | it("returns a NamedNode when passed a plain NN object", () => { 20 | const n = { 21 | termType: "NamedNode", 22 | value: "http://example.com/ns#1", 23 | }; 24 | expect(expandProperty(n)).toEqual(ex.ns("1")); 25 | }); 26 | 27 | it("returns a NamedNode when passed a plain NN object with prototype interface properties", () => { 28 | const proto = { termType: "NamedNode" }; 29 | const n = Object.create(proto); 30 | n.value = "http://example.com/ns#1"; 31 | 32 | expect(expandProperty(n)).toEqual(ex.ns("1")); 33 | }); 34 | 35 | it("returns undefined when passed a random plain object", () => { 36 | const n = { 37 | termType: "Whatever", 38 | value: "http://example.com/ns#1", 39 | }; 40 | expect(expandProperty((n as NamedNode))).toBeUndefined(); 41 | }); 42 | 43 | it("parses url strings to NamedNodes", () => { 44 | expect(expandProperty("http://example.com/ns#1")).toEqual(ex.ns("1")); 45 | }); 46 | 47 | it("parses n-quads formatted strings to NamedNodes", () => { 48 | expect(expandProperty("")).toEqual(ex.ns("1")); 49 | }); 50 | 51 | it("parses shorthand strings to NamedNodes", () => { 52 | expect(expandProperty("ex:1", { ex })).toEqual(ex.ns("1")); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/utilities/__tests__/responses.spec.ts: -------------------------------------------------------------------------------- 1 | import "jest"; 2 | import "../../__tests__/useFactory"; 3 | 4 | import { ResponseAndFallbacks } from "../../types"; 5 | 6 | import { 7 | contentTypeByExtention, 8 | contentTypeByMimeString, 9 | getContentType, 10 | getHeader, 11 | } from "../responses"; 12 | 13 | describe("responses", () => { 14 | describe("#contentTypeByExtention", () => { 15 | it("detects turtle", () => { 16 | expect(contentTypeByExtention("ttl")).toEqual("text/turtle"); 17 | }); 18 | 19 | it("detects ntriples", () => { 20 | expect(contentTypeByExtention("nt")).toEqual("application/n-triples"); 21 | expect(contentTypeByExtention("ntriples")).toEqual("application/n-triples"); 22 | }); 23 | 24 | it("detects n3", () => { 25 | expect(contentTypeByExtention("n3")).toEqual("text/n3"); 26 | }); 27 | 28 | it("detects json-ld", () => { 29 | expect(contentTypeByExtention("jsonld")).toEqual("application/ld+json"); 30 | }); 31 | }); 32 | 33 | describe("#contentTypeByMimeString", () => { 34 | it("returns undefined for unknown content-types", () => { 35 | expect(contentTypeByMimeString("text/html", ".html")).toBeUndefined(); 36 | }); 37 | 38 | it("detects old-school turtle", () => { 39 | expect(contentTypeByMimeString("application/x-turtle", "")).toEqual("text/turtle"); 40 | }); 41 | it("detects old-school ntriples", () => { 42 | expect(contentTypeByMimeString("text/ntriples", "")).toEqual("application/n-triples"); 43 | }); 44 | 45 | it("detects plaintext ntriples by ext", () => { 46 | expect(contentTypeByMimeString("text/plain", "nt")).toEqual("application/n-triples"); 47 | }); 48 | 49 | it("detects n3", () => { 50 | expect(contentTypeByMimeString("text/n3", "")).toEqual("text/n3"); 51 | }); 52 | 53 | it("detects json-ld", () => { 54 | expect(contentTypeByMimeString("application/ld+json", "")).toEqual("application/ld+json"); 55 | }); 56 | }); 57 | 58 | describe("#getContentType", () => { 59 | it("returns the content-type for known extensions", () => { 60 | const response = { 61 | body: "", 62 | headers: {"Content-Type": "*/*"}, 63 | requestedURI: "http://example.com/test.ttl", 64 | status: 200, 65 | }; 66 | expect(getContentType(response)).toEqual("text/turtle"); 67 | }); 68 | 69 | it("returns the content-type for known values", () => { 70 | const response = { 71 | body: "", 72 | headers: {"Content-Type": "application/rdf+xml; text/xml; application/xml"}, 73 | requestedURI: "http://example.com/", 74 | status: 200, 75 | }; 76 | expect(getContentType(response)).toEqual("application/rdf+xml"); 77 | }); 78 | 79 | it("returns the first content-type", () => { 80 | const response = { 81 | body: "", 82 | headers: {"Content-Type": "text/html; application/xhtml+xml"}, 83 | requestedURI: "http://example.com/", 84 | status: 200, 85 | }; 86 | expect(getContentType(response)).toEqual("text/html"); 87 | }); 88 | }); 89 | 90 | describe("#getHeader", () => { 91 | it("works with fetch responses", () => { 92 | const response = new Response(null, { 93 | headers: { 94 | "Content-Type": "text/turtle", 95 | }, 96 | }); 97 | // expect(typeof response.headers.get).toEqual("function"); 98 | expect(getHeader(response, "Content-Type")).toEqual("text/turtle"); 99 | }); 100 | 101 | it("works with response mocks", () => { 102 | const response = { 103 | body: "", 104 | headers: {"Content-Type": "*/*"}, 105 | requestedURI: "http://example.com/test.ttl", 106 | status: 200, 107 | }; 108 | expect(getHeader(response, "Content-Type")).toEqual("*/*"); 109 | }); 110 | 111 | it("returns null when no headers were found", () => { 112 | const response = { 113 | body: "", 114 | requestedURI: "http://example.com/test.ttl", 115 | status: 200, 116 | }; 117 | expect(getHeader((response as ResponseAndFallbacks), "Content-Type")).toBeNull(); 118 | }); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /src/utilities/__tests__/slices.spec.ts: -------------------------------------------------------------------------------- 1 | import "../../__tests__/useFactory"; 2 | 3 | import rdfFactory from "@ontologies/core"; 4 | import "jest"; 5 | 6 | import { isGlobalId, isLocalId, mergeTerms } from "../slices"; 7 | 8 | describe("slices", () => { 9 | describe("isLocalId", () => { 10 | it("detects local ids", () => { 11 | expect(isLocalId("_:b123")).toEqual(true); 12 | }); 13 | 14 | it("rejects global ids", () => { 15 | expect(isLocalId("http://example.com/test:_")).toEqual(false); 16 | }); 17 | }); 18 | 19 | describe("isGlobalId", () => { 20 | it("rejects local ids", () => { 21 | expect(isGlobalId("_:b123")).toEqual(false); 22 | }); 23 | 24 | it("detects fully qualified global ids", () => { 25 | expect(isGlobalId("http://example.com/test:_")).toEqual(true); 26 | }); 27 | 28 | it("detects absolute global ids", () => { 29 | expect(isGlobalId("/test:_")).toEqual(true); 30 | }); 31 | }); 32 | 33 | describe("mergeTerms", () => { 34 | const a = rdfFactory.blankNode(); 35 | const b = rdfFactory.blankNode(); 36 | const c = rdfFactory.blankNode(); 37 | 38 | it("merges nodes", () => { 39 | expect(mergeTerms(a, b)).toEqual([a, b]); 40 | }); 41 | 42 | it("collapses undefined start", () => { 43 | expect(mergeTerms(undefined, b)).toEqual(b); 44 | }); 45 | 46 | it("removes duplicates", () => { 47 | expect(mergeTerms(b, b)).toEqual([b]); 48 | }); 49 | 50 | it("merges multimap", () => { 51 | expect(mergeTerms([a, b], c)).toEqual([a, b, c]); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/utilities/constants.ts: -------------------------------------------------------------------------------- 1 | import { NamedNode } from "@ontologies/core"; 2 | 3 | import ll from "../ontology/ll"; 4 | 5 | export const F_NTRIPLES = "application/n-triples"; 6 | export const F_NQUADS = "application/n-quads"; 7 | export const F_TURTLE = "text/turtle"; 8 | export const F_N3 = "text/n3"; 9 | export const F_PLAIN = "text/plain"; 10 | export const F_JSON = "application/json"; 11 | export const F_JSONLD = "application/ld+json"; 12 | export const F_RDF_XML = "application/rdf+xml"; 13 | 14 | export const F_NTRIPLES_DEP = "text/ntriples"; 15 | export const F_TURTLE_DEP = "application/x-turtle"; 16 | 17 | export const NON_CONTENT_EXTS = ["php", "asp", "aspx", "cgi", "jsp"]; 18 | 19 | export const DEFAULT_TOPOLOGY: NamedNode = ll.defaultTopology; 20 | 21 | /** Constant used to determine that a class is used to render a type rather than a property. */ 22 | export const RENDER_CLASS_NAME: NamedNode = ll.typeRenderClass; 23 | 24 | export const MAIN_NODE_DEFAULT_IRI = ll.targetResource; 25 | // tslint:disable-next-line ban-types 26 | export const NON_DATA_OBJECTS_CTORS: Function[] = [ 27 | Array, 28 | ArrayBuffer, 29 | Boolean, 30 | DataView, 31 | Date, 32 | Error, 33 | EvalError, 34 | Float32Array, 35 | Float64Array, 36 | Int16Array, 37 | Int32Array, 38 | Int8Array, 39 | Intl.Collator, 40 | Intl.DateTimeFormat, 41 | Intl.NumberFormat, 42 | Map, 43 | Number, 44 | Promise, 45 | /* istanbul ignore next */ 46 | (typeof Proxy !== "undefined" ? Proxy : undefined)!, 47 | RangeError, 48 | ReferenceError, 49 | RegExp, 50 | Set, 51 | ].filter(Boolean); 52 | export const MSG_BAD_REQUEST = "Request failed with bad status code"; 53 | export const MSG_INCORRECT_TARGET = "Collections or Literals can't be the target"; 54 | export const MSG_URL_UNDEFINED = "No url given with action."; 55 | export const MSG_URL_UNRESOLVABLE = "Can't execute action with non-named-node url."; 56 | -------------------------------------------------------------------------------- /src/utilities/memoizedNamespace.ts: -------------------------------------------------------------------------------- 1 | import rdfFactory, { NamedNode, Namespace, Term } from "@ontologies/core"; 2 | 3 | import { NamespaceMap } from "../types"; 4 | 5 | const CI_MATCH_PREFIX = 0; 6 | const CI_MATCH_SUFFIX = 1; 7 | 8 | /** 9 | * Expands a property if it's in short-form while preserving long-form. 10 | * Note: The vocabulary needs to be present in the store prefix mapping 11 | * @param prop The short- or long-form property 12 | * @param namespaces Object of namespaces by their abbreviation. 13 | * @returns The (expanded) property 14 | */ 15 | export function expandProperty(prop: NamedNode | Term | string | undefined, 16 | namespaces: NamespaceMap = {}): NamedNode | undefined { 17 | if (!prop) { 18 | return prop as undefined; 19 | } 20 | if (typeof prop !== "string" 21 | && Object.prototype.hasOwnProperty.call(prop, "termType") 22 | && (prop as Term).termType === "NamedNode") { 23 | 24 | return rdfFactory.namedNode(prop.value); 25 | } 26 | if (typeof prop === "object") { 27 | if (prop.termType === "NamedNode") { 28 | return rdfFactory.namedNode(prop.value); 29 | } 30 | 31 | return undefined; 32 | } 33 | 34 | if (prop.indexOf("/") >= 1) { 35 | if (prop.startsWith("<") && prop.endsWith(">")) { 36 | return rdfFactory.namedNode(prop.slice(1, -1)); 37 | } 38 | return rdfFactory.namedNode(prop); 39 | } 40 | const matches = prop.split(":"); 41 | const constructor: Namespace | undefined = namespaces[matches[CI_MATCH_PREFIX]]?.ns; 42 | 43 | return constructor && constructor(matches[CI_MATCH_SUFFIX]); 44 | } 45 | -------------------------------------------------------------------------------- /src/utilities/responses.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ErrorResponse, 3 | ExtensionResponse, 4 | RDFLibFetcherRequest, 5 | ResponseAndFallbacks, 6 | } from "../types"; 7 | 8 | import { 9 | F_JSONLD, 10 | F_N3, 11 | F_NTRIPLES, 12 | F_NTRIPLES_DEP, 13 | F_PLAIN, 14 | F_RDF_XML, 15 | F_TURTLE, 16 | F_TURTLE_DEP, 17 | NON_CONTENT_EXTS, 18 | } from "./constants"; 19 | 20 | /** 21 | * Extracts the content type from a request. 22 | * The content-type value has precedence if it contains a known type. 23 | * Otherwise it returns the extension if present, or the content-type without the encoding. 24 | * 25 | * @summary Extracts the content type from a request. 26 | */ 27 | export function getContentType(res: ResponseAndFallbacks): string { 28 | const contentTypeRaw = getHeader(res, "Content-Type"); 29 | if (contentTypeRaw === undefined || contentTypeRaw === null) { 30 | return ""; 31 | } 32 | const contentType: string = contentTypeRaw.split(";")[0]; 33 | const url = getURL(res); 34 | const urlMatch = url && new URL(url).href.match(/\.([a-zA-Z0-9]{1,8})($|\?|#)/); 35 | const ext = urlMatch ? urlMatch[1] : ""; 36 | if (contentType) { 37 | const matched = contentTypeByMimeString(contentType, ext); 38 | if (matched) { 39 | return matched; 40 | } 41 | } 42 | if (ext && !NON_CONTENT_EXTS.includes(ext)) { 43 | return contentTypeByExtention(ext); 44 | } 45 | 46 | return contentTypeRaw.split(";")[0]; 47 | } 48 | 49 | export async function getJSON(res: ResponseAndFallbacks): Promise { 50 | if (res instanceof Response) { 51 | return res.json(); 52 | } else if (res instanceof XMLHttpRequest) { 53 | return JSON.parse(res.responseText); 54 | } 55 | 56 | return JSON.parse(res.body); 57 | } 58 | 59 | export function getHeader(res: ResponseAndFallbacks, header: string): string | null { 60 | if (res instanceof Response) { 61 | return res.headers.get(header); 62 | } else if (typeof XMLHttpRequest !== "undefined" && res instanceof XMLHttpRequest) { 63 | return (res as XMLHttpRequest).getResponseHeader(header) || null; 64 | } else if (res && (res as ExtensionResponse | RDFLibFetcherRequest).headers) { 65 | const headerValue = (res as ExtensionResponse | RDFLibFetcherRequest).headers[header]; 66 | return headerValue || null; 67 | } 68 | 69 | return null; 70 | } 71 | 72 | export function getURL(res: ResponseAndFallbacks): string { 73 | if (typeof XMLHttpRequest !== "undefined" && res instanceof XMLHttpRequest) { 74 | return res.responseURL; 75 | } else if ("requestedURI" in res) { 76 | return res.requestedURI; 77 | } 78 | 79 | return (res as Response | ExtensionResponse).url; 80 | } 81 | 82 | export function contentTypeByExtention(ext: string): string { 83 | if (["ttl"].includes(ext)) { 84 | return F_TURTLE; 85 | } else if (["ntriples", "nt"].includes(ext)) { 86 | return F_NTRIPLES; 87 | } else if (["jsonld"].includes(ext)) { 88 | return F_JSONLD; 89 | } else if (["n3"].includes(ext)) { 90 | return F_N3; 91 | } 92 | 93 | return ext; 94 | } 95 | 96 | export function contentTypeByMimeString(contentType: string, ext: string): string | undefined { 97 | if (contentType.includes(F_NTRIPLES) || contentType.includes(F_NTRIPLES_DEP)) { 98 | return F_NTRIPLES; 99 | } else if (contentType.includes(F_PLAIN) && ["ntriples", "nt"].indexOf(ext) >= 0) { 100 | return F_NTRIPLES; 101 | } else if (contentType.includes(F_TURTLE) || contentType.includes(F_TURTLE_DEP)) { 102 | return F_TURTLE; 103 | } else if (contentType.includes(F_N3)) { 104 | return F_N3; 105 | } else if (contentType.includes(F_JSONLD)) { 106 | return F_JSONLD; 107 | } else if (contentType.includes(F_RDF_XML)) { 108 | return F_RDF_XML; 109 | } 110 | 111 | return undefined; 112 | } 113 | -------------------------------------------------------------------------------- /src/utilities/slices.ts: -------------------------------------------------------------------------------- 1 | import { SomeTerm } from "@ontologies/core"; 2 | 3 | import { Id } from "../datastrucures/DataSlice"; 4 | import { MultimapTerm } from "../datastrucures/Fields"; 5 | 6 | import { normalizeType } from "../utilities"; 7 | 8 | export const mergeTerms = ( 9 | a: SomeTerm | MultimapTerm | undefined, 10 | b: SomeTerm | MultimapTerm, 11 | ): SomeTerm | MultimapTerm => { 12 | if (Array.isArray(a)) { 13 | return Array.from(new Set([...a, ...normalizeType(b)])); 14 | } else if (a) { 15 | return Array.from(new Set([a, ...normalizeType(b)])); 16 | } else { 17 | return b; 18 | } 19 | }; 20 | 21 | export const isLocalId = (id: Id): boolean => id.startsWith("_:"); 22 | 23 | export const isGlobalId = (id: Id): boolean => id.startsWith("/") || (id.includes(":") && !id.startsWith("_:")); 24 | -------------------------------------------------------------------------------- /src/worker/DataWorkerLoader.ts: -------------------------------------------------------------------------------- 1 | import { WorkerMessageBase } from "../types"; 2 | import { 3 | FETCH_RESOURCE, 4 | GET_ENTITY, 5 | SET_ACCEPT_HOST, 6 | } from "./messages"; 7 | 8 | function sendMessage(worker: Worker, message: WorkerMessageBase): Promise { 9 | return new Promise((): void => { 10 | const messageChannel = new MessageChannel(); 11 | messageChannel.port1.onmessage = (event: MessageEvent): void => { 12 | if (event.data.error) { 13 | throw event.data.error; 14 | } else { 15 | return event.data.data; 16 | } 17 | }; 18 | worker.postMessage(message, [messageChannel.port2]); 19 | }); 20 | } 21 | 22 | export class DataWorkerLoader { 23 | public static registerTransformer(): void { 24 | throw new Error("Transformers should be registered directly, since they aren't cloneable nor transferable"); 25 | } 26 | 27 | private worker: Worker; 28 | 29 | public constructor(worker: Worker) { 30 | this.worker = worker; 31 | } 32 | 33 | public fetchResource(iri: string): Promise { 34 | return sendMessage( 35 | this.worker, 36 | { 37 | method: FETCH_RESOURCE, 38 | params: { 39 | iri, 40 | }, 41 | }, 42 | ); 43 | } 44 | 45 | public getEntity(iri: string): Promise { 46 | return sendMessage( 47 | this.worker, 48 | { 49 | method: GET_ENTITY, 50 | params: { 51 | iri, 52 | }, 53 | }, 54 | ); 55 | } 56 | 57 | public setAcceptForHost(origin: string, acceptValue: string): Promise { 58 | return sendMessage( 59 | this.worker, 60 | { 61 | method: SET_ACCEPT_HOST, 62 | params: { 63 | acceptValue, 64 | origin, 65 | }, 66 | }, 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/worker/messages.ts: -------------------------------------------------------------------------------- 1 | export const DATA_ACQUIRED = "DATA_ACQUIRED"; 2 | export const FETCH_RESOURCE = "FETCH_RESOURCE"; 3 | export const FETCH_EXT = "FETCH_EXT"; 4 | export const GET_ENTITY = "GET_ENTITY"; 5 | export const SET_ACCEPT_HOST = "SET_ACCEPT_HOST"; 6 | export const STORE_UPDATE = "STORE_UPDATE"; 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "moduleResolution": "node", 5 | "declaration": false, 6 | "forceConsistentCasingInFileNames": true, 7 | "module": "esnext", 8 | "lib": [ 9 | "esnext", 10 | "dom" 11 | ], 12 | "noImplicitReturns": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "outDir": "dist/es", 16 | "sourceMap": true, 17 | "strict": true, 18 | "target": "es2020" 19 | }, 20 | "exclude": [ 21 | "**/__tests__/*" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "lintOptions": { 3 | "typeCheck": true 4 | }, 5 | "extends": "tslint:recommended", 6 | "rules": { 7 | "interface-name": [true, "never-prefix"], 8 | "linebreak-style": [true, "LF"], 9 | "no-implicit-dependencies": [true, "dev"], 10 | "no-inferrable-types": [{"severity": "warning"}], 11 | "no-null-keyword": false, 12 | "only-arrow-functions": false, 13 | // for-of is a lot slower then an iota for 14 | "prefer-for-of": false, 15 | "typedef": [ 16 | true, 17 | "call-signature", 18 | "arrow-call-signature", 19 | "property-declaration", 20 | "member-variable-declaration" 21 | ], 22 | "variable-name": [true, "allow-leading-underscore"] 23 | } 24 | } 25 | --------------------------------------------------------------------------------