├── docs
├── .nojekyll
├── _sidebar.md
├── index.html
├── deep-dive.md
├── named-graphs.md
├── rdf-lists.md
├── traversal.md
├── README.md
├── tagged-literals.md
├── manipulation.md
├── context.md
└── api.md
├── codecov.yml
├── .gitignore
├── lib
├── environment.js
├── namespace.js
├── toArray.js
├── toTermArray.js
├── fromPrimitive.js
├── languageTag.js
├── term.js
├── Context.js
└── Clownface.js
├── .eslintrc.json
├── test
├── support
│ ├── factory.js
│ ├── parse.js
│ ├── example.js
│ ├── namespace.js
│ └── CustomDataFactory.js
├── Context.test.js
├── Clownface
│ ├── term.test.js
│ ├── values.test.js
│ ├── value.test.js
│ ├── terms.test.js
│ ├── dataset.test.js
│ ├── datasets.test.js
│ ├── toArray.test.js
│ ├── isList.test.js
│ ├── toString.test.js
│ ├── any.test.js
│ ├── factory.test.js
│ ├── forEach.test.js
│ ├── map.test.js
│ ├── deleteList.test.js
│ ├── filter.test.js
│ ├── in.test.js
│ ├── blankNode.test.js
│ ├── deleteIn.test.js
│ ├── namedNode.test.js
│ ├── has.test.js
│ ├── deleteOut.test.js
│ ├── addList.test.js
│ ├── list.test.js
│ ├── constructor.test.js
│ ├── node.test.js
│ ├── literal.test.js
│ ├── addIn.test.js
│ ├── out.test.js
│ └── addOut.test.js
├── Factory.test.js
├── term.test.js
├── filter.test.js
└── fromPrimitive.test.js
├── .changeset
├── config.json
└── README.md
├── Factory.js
├── .github
└── workflows
│ ├── add-to-backlog.yml
│ ├── ci.yaml
│ └── release.yml
├── filter.js
├── CHANGELOG.md
├── index.js
├── examples
└── namespace.js
├── package.json
└── README.md
/docs/.nojekyll:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | ignore:
2 | - examples
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | coverage
2 | docs/api/
3 | node_modules
4 |
--------------------------------------------------------------------------------
/lib/environment.js:
--------------------------------------------------------------------------------
1 | import Environment from '@rdfjs/environment'
2 | import NamespaceFactory from '@rdfjs/namespace/Factory.js'
3 | import DataFactory from '@rdfjs/data-model/Factory.js'
4 |
5 | export default new Environment([
6 | NamespaceFactory,
7 | DataFactory,
8 | ])
9 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [ "@tpluscode/eslint-config/js" ],
3 | "env": {
4 | "mocha": true
5 | },
6 | "overrides": [
7 | {
8 | "files": ["examples/**"],
9 | "rules": {
10 | "no-console": "off"
11 | }
12 | }
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/test/support/factory.js:
--------------------------------------------------------------------------------
1 | // RDF-Ext
2 | import factory from 'rdf-ext'
3 |
4 | /*
5 | // Reference implementations of Data Model and Dataset
6 | import dataset from '@rdfjs/dataset'
7 | import model from '@rdfjs/data-model'
8 |
9 | const factory = { ...model, ...dataset }
10 | */
11 |
12 | export default factory
13 |
--------------------------------------------------------------------------------
/.changeset/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
3 | "changelog": "@changesets/cli/changelog",
4 | "commit": false,
5 | "fixed": [],
6 | "linked": [],
7 | "access": "public",
8 | "baseBranch": "master",
9 | "updateInternalDependencies": "patch",
10 | "ignore": []
11 | }
12 |
--------------------------------------------------------------------------------
/lib/namespace.js:
--------------------------------------------------------------------------------
1 | export default (factory) => {
2 | const xsd = factory.namespace('http://www.w3.org/2001/XMLSchema#')
3 | const rdf = factory.namespace('http://www.w3.org/1999/02/22-rdf-syntax-ns#')
4 |
5 | return {
6 | first: rdf.first,
7 | nil: rdf.nil,
8 | rest: rdf.rest,
9 | langString: rdf.langString,
10 | xsd,
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/lib/toArray.js:
--------------------------------------------------------------------------------
1 | export default function toArray(value, defaultValue) {
2 | if (typeof value === 'undefined' || value === null) {
3 | return defaultValue
4 | }
5 |
6 | if (Array.isArray(value)) {
7 | return value
8 | }
9 |
10 | if (typeof value !== 'string' && value[Symbol.iterator]) {
11 | return [...value]
12 | }
13 |
14 | return [value]
15 | }
16 |
--------------------------------------------------------------------------------
/test/support/parse.js:
--------------------------------------------------------------------------------
1 | import $rdf from 'rdf-ext'
2 | import toStream from 'string-to-stream'
3 | import Parser from '@rdfjs/parser-n3'
4 | import cf from '../..//index.js'
5 |
6 | const parser = new Parser()
7 |
8 | export default async function parse(string) {
9 | const dataset = await $rdf.dataset().import(parser.import(toStream(string)))
10 |
11 | return cf({ dataset })
12 | }
13 |
--------------------------------------------------------------------------------
/Factory.js:
--------------------------------------------------------------------------------
1 | import clownface from './index.js'
2 |
3 | class ClownfaceFactory {
4 | clownface({ ...args } = {}) {
5 | if (!args.dataset && typeof this.dataset === 'function') {
6 | args.dataset = this.dataset()
7 | }
8 |
9 | return clownface({ ...args, factory: this })
10 | }
11 | }
12 |
13 | ClownfaceFactory.exports = ['clownface']
14 |
15 | export default ClownfaceFactory
16 |
--------------------------------------------------------------------------------
/docs/_sidebar.md:
--------------------------------------------------------------------------------
1 | * Getting started
2 | * [About](/)
3 | * [Deep dive](deep-dive.md)
4 | * User guide
5 | * [The context](context.md)
6 | * [Navigating the graph](traversal.md)
7 | * [Manipulating data](manipulation.md)
8 | * [Working with named graphs](named-graphs.md)
9 | * [RDF Lists](rdf-lists.md)
10 | * [Tagged literals](tagged-literals.md)
11 | * Reference
12 | * [JSDoc](api.md)
13 | * [TypeScript](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/clownface)
14 |
--------------------------------------------------------------------------------
/.github/workflows/add-to-backlog.yml:
--------------------------------------------------------------------------------
1 | name: Add issues to shared backlog
2 |
3 | on:
4 | issues:
5 | types:
6 | - opened
7 | pull_request:
8 | branches-ignore:
9 | - "dependabot/**/*"
10 | types:
11 | - opened
12 |
13 | jobs:
14 | add-to-project:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/add-to-project@v0.5.0
18 | with:
19 | project-url: https://github.com/orgs/zazuko/projects/23
20 | github-token: ${{ secrets.BACKLOG_PAT }}
21 |
--------------------------------------------------------------------------------
/.changeset/README.md:
--------------------------------------------------------------------------------
1 | # Changesets
2 |
3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4 | with multi-package repos, or single-package repos to help you version and publish your code. You can
5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6 |
7 | We have a quick list of common questions to get you started engaging with this project in
8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
9 |
--------------------------------------------------------------------------------
/test/Context.test.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert'
2 | import { describe, it } from 'mocha'
3 | import Context from '../lib/Context.js'
4 | import { schema } from './support/namespace.js'
5 | import rdf from './support/factory.js'
6 |
7 | describe('Context', () => {
8 | describe('out', () => {
9 | it('can be called without language', () => {
10 | const context = new Context({ dataset: rdf.dataset() })
11 |
12 | assert.doesNotThrow(() => {
13 | context.out([schema.knows])
14 | })
15 | })
16 | })
17 | })
18 |
--------------------------------------------------------------------------------
/lib/toTermArray.js:
--------------------------------------------------------------------------------
1 | import term from './term.js'
2 | import toArray from './toArray.js'
3 |
4 | export default function toTermArray(items, type, languageOrDatatype, factory) {
5 | if ((typeof items === 'undefined' || items === null) && !type) {
6 | return items
7 | }
8 |
9 | return (toArray(items) || [undefined]).reduce((all, item) => {
10 | if (typeof item === 'object' && item.terms) {
11 | return all.concat(item.terms)
12 | }
13 |
14 | all.push(term(item, type, languageOrDatatype, factory))
15 |
16 | return all
17 | }, [])
18 | }
19 |
--------------------------------------------------------------------------------
/test/support/example.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs'
2 | import path from 'path'
3 | import module from 'module'
4 | import N3Parser from '@rdfjs/parser-n3'
5 | import fromStream from 'rdf-dataset-ext/fromStream.js'
6 | import rdf from './factory.js'
7 |
8 | const require = module.createRequire(import.meta.url)
9 |
10 | export default function init() {
11 | const filename = path.join(path.dirname(require.resolve('tbbt-ld')), 'dist/tbbt.nq')
12 | const input = fs.createReadStream(filename)
13 | const parser = new N3Parser()
14 |
15 | return fromStream(rdf.dataset(), parser.import(input))
16 | }
17 |
--------------------------------------------------------------------------------
/filter.js:
--------------------------------------------------------------------------------
1 | import { filterTaggedLiterals } from './lib/languageTag.js'
2 |
3 | /**
4 | * Returns a function to be used as callback to `Clownface#filter`.
5 | * It will return only literals which match the given `language(s)`
6 | *
7 | * @param {string | string[]} language
8 | * @returns {function(Clownface, number, Clownface[]): boolean}
9 | */
10 | export function taggedLiteral(language) {
11 | return (current, index, pointers) => {
12 | const found = filterTaggedLiterals(pointers.map(ptr => ptr.term), { language })
13 |
14 | return found.some(term => current.term.equals(term))
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/test/support/namespace.js:
--------------------------------------------------------------------------------
1 | import rdf from 'rdf-ext'
2 |
3 | export const first = rdf.namedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#first')
4 | export const list = rdf.namedNode('http://example.org/list')
5 | export const nil = rdf.namedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#nil')
6 | export const rest = rdf.namedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#rest')
7 | export const ex = rdf.namespace('http://example.org/')
8 | export const rdfs = rdf.namespace('http://www.w3.org/2000/01/rdf-schema#')
9 | export const schema = rdf.namespace('http://schema.org/')
10 | export const xsd = rdf.namespace('http://www.w3.org/2001/XMLSchema#')
11 | export const tbbtp = rdf.namespace('http://localhost:8080/data/person/')
12 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # clownface
2 |
3 | ## 2.0.3
4 |
5 | ### Patch Changes
6 |
7 | - e9da738: chore: update package links
8 |
9 | ## 2.0.2
10 |
11 | ### Patch Changes
12 |
13 | - 25f4bf9: Adjust to `@rdfjs/environment@v1` (still compatible with v0)
14 |
15 | ## 2.0.1
16 |
17 | ### Patch Changes
18 |
19 | - 810ede5: Fixes a problem that in some scenarios when operating on a `MultiPointer` would use clownface's default environment which causes clashing blank node being generated
20 |
21 | ## 2.0.0
22 |
23 | ### Major Changes
24 |
25 | - c1441a4: Update to ESM
26 |
27 | ### Minor Changes
28 |
29 | - c1441a4: Uses `@rdfjs/environment` instead of directly importing individual packages
30 | - 1caa1ae: Added RDF/JS environment factory module
31 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | - push
5 | - pull_request
6 |
7 | jobs:
8 | test:
9 | runs-on: ubuntu-latest
10 | strategy:
11 | matrix:
12 | node:
13 | - "18"
14 | - "20"
15 | steps:
16 | - uses: actions/checkout@v4
17 | - uses: actions/setup-node@v3
18 | with:
19 | node-version: ${{ matrix.node }}
20 | - run: npm ci
21 | - run: npm test
22 | - uses: codecov/codecov-action@v2
23 |
24 | lint:
25 | runs-on: ubuntu-latest
26 | steps:
27 | - uses: actions/checkout@v4
28 | - uses: actions/setup-node@v3
29 | with:
30 | node-version: 20
31 | - run: npm install --ci
32 | - run: npm run lint
33 |
--------------------------------------------------------------------------------
/test/support/CustomDataFactory.js:
--------------------------------------------------------------------------------
1 | import rdf from 'rdf-ext'
2 |
3 | export default class {
4 | quad(s, p, o, g) {
5 | const quad = rdf.quad(s, p, o, g)
6 | quad.testProperty = 'test'
7 | return quad
8 | }
9 |
10 | literal(value, languageOrDatatype) {
11 | const node = rdf.literal(value, languageOrDatatype)
12 | node.testProperty = 'test'
13 | return node
14 | }
15 |
16 | blankNode(value) {
17 | const node = rdf.blankNode(value)
18 | node.testProperty = 'test'
19 | return node
20 | }
21 |
22 | namedNode(value) {
23 | const node = rdf.namedNode(value)
24 | node.testProperty = 'test'
25 | return node
26 | }
27 |
28 | static get exports() {
29 | return ['quad', 'literal', 'namedNode', 'blankNode']
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | clownface - Simple but powerful graph traversing library
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import Clownface from './lib/Clownface.js'
2 | import environment from './lib/environment.js'
3 |
4 | /**
5 | * Factory to create graph pointer objects
6 | *
7 | * @param {Object} init
8 | * @param {DatasetCore} init.dataset an RDF/JS dataset
9 | * @param {string|Term} [init.graph] graph URI
10 | * @param {Term|Term[]} [init.term] one or more RDF/JS term(s) which will be the pointer's context
11 | * @param {string} [init.value] one or more raw values which will create literal node as the pointer's context
12 | * @param {DataFactory} [init.factory=@rdfjs/environment] an RDF/JS factory which will be used to create nodes
13 | * @param {Context} [init._context] an existing clownface context. takes precedence before other params
14 | * @returns {Clownface}
15 | */
16 | export default function factory({ dataset, graph, term, value, factory = environment, _context }) {
17 | return new Clownface({ dataset, graph, term, value, factory, _context })
18 | }
19 |
--------------------------------------------------------------------------------
/test/Clownface/term.test.js:
--------------------------------------------------------------------------------
1 | /* global describe, it */
2 |
3 | import assert from 'assert'
4 | import clownface from '../../index.js'
5 | import rdf from '../support/factory.js'
6 |
7 | describe('.term', () => {
8 | it('should be undefined if there is no context with a term', () => {
9 | const cf = clownface({ dataset: rdf.dataset() })
10 |
11 | assert.strictEqual(typeof cf.term, 'undefined')
12 | })
13 |
14 | it('should be the term of the context if there is only one term', () => {
15 | const term = rdf.literal('1')
16 | const cf = clownface({ dataset: rdf.dataset(), term })
17 |
18 | assert(term.equals(cf.term))
19 | })
20 |
21 | it('should be undefined if there are multiple terms in the context', () => {
22 | const termA = rdf.literal('1')
23 | const termB = rdf.namedNode('http://example.org/')
24 |
25 | const cf = clownface({ dataset: rdf.dataset(), term: [termA, termB] })
26 |
27 | assert.strictEqual(typeof cf.term, 'undefined')
28 | })
29 | })
30 |
--------------------------------------------------------------------------------
/test/Clownface/values.test.js:
--------------------------------------------------------------------------------
1 | /* global describe, it */
2 |
3 | import assert from 'assert'
4 | import clownface from '../../index.js'
5 | import rdf from '../support/factory.js'
6 |
7 | describe('.values', () => {
8 | it('should be an array property', () => {
9 | const cf = clownface({ dataset: rdf.dataset() })
10 |
11 | assert(Array.isArray(cf.values))
12 | })
13 |
14 | it('should be empty if there is no context with a term', () => {
15 | const cf = clownface({ dataset: rdf.dataset() })
16 |
17 | assert.deepStrictEqual(cf.values, [])
18 | })
19 |
20 | it('should contain the values of the terms', () => {
21 | const termA = rdf.literal('1')
22 | const termB = rdf.namedNode('http://example.org/')
23 | const cf = clownface({ dataset: rdf.dataset(), value: [termA, termB] })
24 |
25 | const result = cf.values
26 |
27 | assert.strictEqual(result.length, 2)
28 | assert.strictEqual(result[0], termA.value)
29 | assert.strictEqual(result[1], termB.value)
30 | })
31 | })
32 |
--------------------------------------------------------------------------------
/test/Clownface/value.test.js:
--------------------------------------------------------------------------------
1 | /* global describe, it */
2 |
3 | import assert from 'assert'
4 | import clownface from '../../index.js'
5 | import rdf from '../support/factory.js'
6 |
7 | describe('.value', () => {
8 | it('should be undefined if there is no context with a term', () => {
9 | const cf = clownface({ dataset: rdf.dataset() })
10 |
11 | assert.strictEqual(typeof cf.value, 'undefined')
12 | })
13 |
14 | it('should be the value of the context if there is only one term', () => {
15 | const term = rdf.literal('1')
16 | const cf = clownface({ dataset: rdf.dataset(), value: term.value })
17 |
18 | assert.strictEqual(cf.value, term.value)
19 | })
20 |
21 | it('should be undefined if there are multiple terms in the context', () => {
22 | const termA = rdf.literal('1')
23 | const termB = rdf.namedNode('http://example.org/')
24 | const cf = clownface({ dataset: rdf.dataset(), term: [termA, termB] })
25 |
26 | assert.strictEqual(typeof cf.value, 'undefined')
27 | })
28 | })
29 |
--------------------------------------------------------------------------------
/test/Clownface/terms.test.js:
--------------------------------------------------------------------------------
1 | /* global describe, it */
2 |
3 | import assert from 'assert'
4 | import clownface from '../../index.js'
5 | import rdf from '../support/factory.js'
6 |
7 | describe('.terms', () => {
8 | it('should be an array property', () => {
9 | const cf = clownface({ dataset: rdf.dataset() })
10 |
11 | assert(Array.isArray(cf.terms))
12 | })
13 |
14 | it('should be empty if there is no context with a term', () => {
15 | const cf = clownface({ dataset: rdf.dataset() })
16 |
17 | const result = cf.terms
18 |
19 | assert.deepStrictEqual(result, [])
20 | })
21 |
22 | it('should contain all terms of the context', () => {
23 | const termA = rdf.literal('1')
24 | const termB = rdf.namedNode('http://example.org/')
25 | const cf = clownface({ dataset: rdf.dataset(), term: [termA, termB] })
26 |
27 | const result = cf.terms
28 |
29 | assert.strictEqual(result.length, 2)
30 | assert(termA.equals(result[0]))
31 | assert(termB.equals(result[1]))
32 | })
33 | })
34 |
--------------------------------------------------------------------------------
/lib/fromPrimitive.js:
--------------------------------------------------------------------------------
1 | import rdf from './environment.js'
2 | import namespace from './namespace.js'
3 |
4 | const { xsd } = namespace(rdf)
5 |
6 | export function booleanToLiteral(value, factory = rdf) {
7 | if (typeof value !== 'boolean') {
8 | return null
9 | }
10 |
11 | return factory.literal(value.toString(), xsd('boolean'))
12 | }
13 |
14 | export function numberToLiteral(value, factory = rdf) {
15 | if (typeof value !== 'number') {
16 | return null
17 | }
18 |
19 | if (Number.isInteger(value)) {
20 | return factory.literal(value.toString(10), xsd('integer'))
21 | }
22 |
23 | return factory.literal(value.toString(10), xsd('double'))
24 | }
25 |
26 | export function stringToLiteral(value, factory = rdf) {
27 | if (typeof value !== 'string') {
28 | return null
29 | }
30 |
31 | return factory.literal(value)
32 | }
33 |
34 | export function toLiteral(value, factory = rdf) {
35 | return booleanToLiteral(value, factory) ||
36 | numberToLiteral(value, factory) ||
37 | stringToLiteral(value, factory)
38 | }
39 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Publish
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | release:
10 | name: Release
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Checkout Repo
14 | uses: actions/checkout@v4
15 | with:
16 | # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
17 | fetch-depth: 0
18 |
19 | - name: Setup Node.js
20 | uses: actions/setup-node@v3
21 | with:
22 | node-version: 20
23 |
24 | - name: Install Dependencies
25 | run: npm ci
26 |
27 | - name: Create Release Pull Request or Publish to npm
28 | id: changesets
29 | uses: changesets/action@v1
30 | with:
31 | # This expects you to have a script called release which does a build for your packages and calls changeset publish
32 | publish: npm run release
33 | env:
34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
35 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
36 |
--------------------------------------------------------------------------------
/test/Factory.test.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 | import Environment from '@rdfjs/environment'
3 | import DatasetFactory from '@rdfjs/dataset/Factory.js'
4 | import $rdf from 'rdf-ext'
5 | import NamespaceFactory from '@rdfjs/namespace/Factory.js'
6 | import DataFactory from '@rdfjs/data-model/Factory.js'
7 | import ClownfaceFactory from '../Factory.js'
8 |
9 | describe('Factory', () => {
10 | const env = new Environment([
11 | ClownfaceFactory,
12 | NamespaceFactory,
13 | DatasetFactory,
14 | DataFactory,
15 | ])
16 |
17 | it('creates dataset using the available factory', () => {
18 | // when
19 | const ptr = env.clownface()
20 |
21 | // then
22 | expect(ptr.dataset).to.be.ok
23 | })
24 |
25 | it('uses provided dataset', () => {
26 | // when
27 | const dataset = $rdf.dataset()
28 | const ptr = env.clownface({ dataset })
29 |
30 | // then
31 | expect(ptr.dataset).to.eq(dataset)
32 | })
33 |
34 | it('forwards arguments', () => {
35 | // when
36 | const term = $rdf.namedNode('http://example.com')
37 | const ptr = env.clownface({ term })
38 |
39 | // then
40 | expect(ptr.term).to.deep.eq(term)
41 | })
42 | })
43 |
--------------------------------------------------------------------------------
/test/Clownface/dataset.test.js:
--------------------------------------------------------------------------------
1 | /* global describe, it */
2 |
3 | import assert from 'assert'
4 | import clownface from '../../index.js'
5 | import rdf from '../support/factory.js'
6 |
7 | describe('.dataset', () => {
8 | it('should be undefined if there is no context with a dataset', () => {
9 | const cf = clownface({ dataset: rdf.dataset() })
10 |
11 | cf._context = []
12 |
13 | assert.strictEqual(typeof cf.dataset, 'undefined')
14 | })
15 |
16 | it('should be the dataset of the context if there is only one dataset', () => {
17 | const dataset = rdf.dataset()
18 | const cf = clownface({ dataset })
19 |
20 | assert.strictEqual(cf.dataset, dataset)
21 | })
22 |
23 | it('should be undefined if there are multiple datasets in the context', () => {
24 | const termA = rdf.literal('1')
25 | const termB = rdf.namedNode('http://example.org/')
26 | const datasetA = rdf.dataset()
27 | const datasetB = rdf.dataset()
28 | const cf = clownface({ dataset: rdf.dataset(), value: [termA, termB] })
29 |
30 | // TODO: should be possible with constructor or static method
31 | cf._context[0].dataset = datasetA
32 | cf._context[1].dataset = datasetB
33 |
34 | assert.strictEqual(typeof cf.dataset, 'undefined')
35 | })
36 | })
37 |
--------------------------------------------------------------------------------
/test/Clownface/datasets.test.js:
--------------------------------------------------------------------------------
1 | /* global describe, it */
2 |
3 | import assert from 'assert'
4 | import equals from 'rdf-dataset-ext/equals.js'
5 | import clownface from '../../index.js'
6 | import rdf from '../support/factory.js'
7 |
8 | describe('.datasets', () => {
9 | it('should be an array property', () => {
10 | const cf = clownface({ dataset: rdf.dataset() })
11 |
12 | assert(Array.isArray(cf.datasets))
13 | })
14 |
15 | it('should be empty if there is no context with a dataset', () => {
16 | const cf = clownface({ dataset: rdf.dataset() })
17 |
18 | cf._context = []
19 |
20 | assert.deepStrictEqual(cf.datasets, [])
21 | })
22 |
23 | it('should contain all datasets of the context', () => {
24 | const termA = rdf.literal('1')
25 | const termB = rdf.namedNode('http://example.org/')
26 | const datasetA = rdf.dataset()
27 | const datasetB = rdf.dataset()
28 | const cf = clownface({ dataset: rdf.dataset(), value: [termA, termB] })
29 |
30 | // TODO: should be possible with constructor or static method
31 | cf._context[0].dataset = datasetA
32 | cf._context[1].dataset = datasetB
33 |
34 | const result = cf.datasets
35 |
36 | assert.strictEqual(result.length, 2)
37 | assert(equals(datasetA, result[0]))
38 | assert(equals(datasetB, result[1]))
39 | })
40 | })
41 |
--------------------------------------------------------------------------------
/test/Clownface/toArray.test.js:
--------------------------------------------------------------------------------
1 | /* global describe, it */
2 |
3 | import assert from 'assert'
4 | import clownface from '../../index.js'
5 | import loadExample from '../support/example.js'
6 | import rdf from '../support/factory.js'
7 | import Clownface from '../../lib/Clownface.js'
8 |
9 | describe('.toArray', () => {
10 | it('should be a function', () => {
11 | const cf = clownface({ dataset: rdf.dataset() })
12 |
13 | assert.strictEqual(typeof cf.toArray, 'function')
14 | })
15 |
16 | it('should return an array', () => {
17 | const cf = clownface({ dataset: rdf.dataset() })
18 |
19 | assert(Array.isArray(cf.toArray()))
20 | })
21 |
22 | it('should return a Dataset instance for every context object', async () => {
23 | const cf = clownface({
24 | dataset: await loadExample(),
25 | term: rdf.namedNode('http://localhost:8080/data/person/bernadette-rostenkowski'),
26 | })
27 |
28 | const result = cf.in(rdf.namedNode('http://schema.org/knows')).toArray()
29 |
30 | assert.strictEqual(result.length, 7)
31 | assert(result[0] instanceof Clownface)
32 | })
33 |
34 | it('should not return an instance for undefined context', () => {
35 | const cf = clownface({ dataset: rdf.dataset() })
36 |
37 | const array = cf.toArray()
38 |
39 | assert.strictEqual(array.length, 0)
40 | })
41 | })
42 |
--------------------------------------------------------------------------------
/test/Clownface/isList.test.js:
--------------------------------------------------------------------------------
1 | import { strictEqual } from 'assert'
2 | import { describe, it } from 'mocha'
3 | import clownface from '../../index.js'
4 | import rdf from '../support/factory.js'
5 | import * as ns from '../support/namespace.js'
6 |
7 | describe('.isList', () => {
8 | it('should be a function', () => {
9 | const ptr = clownface({})
10 |
11 | strictEqual(typeof ptr.isList, 'function')
12 | })
13 |
14 | it('should return false if there is no term', () => {
15 | const ptr = clownface({})
16 |
17 | strictEqual(ptr.isList(), false)
18 | })
19 |
20 | it('should return false if the term is not a list', () => {
21 | const ptr = clownface({ term: rdf.blankNode(), dataset: rdf.dataset() })
22 |
23 | strictEqual(ptr.isList(), false)
24 | })
25 |
26 | it('should return true if the term points to a list', () => {
27 | const item = [rdf.blankNode(), rdf.blankNode()]
28 | const dataset = rdf.dataset([
29 | rdf.quad(item[0], ns.first, rdf.literal('1')),
30 | rdf.quad(item[0], ns.rest, item[1]),
31 | rdf.quad(item[1], ns.first, rdf.literal('2')),
32 | rdf.quad(item[1], ns.rest, ns.nil),
33 | ])
34 | const ptr = clownface({ term: item[0], dataset })
35 |
36 | strictEqual(ptr.isList(), true)
37 | })
38 |
39 | it('should return true if the term points to an empty list', () => {
40 | const ptr = clownface({ term: ns.nil })
41 |
42 | strictEqual(ptr.isList(), true)
43 | })
44 | })
45 |
--------------------------------------------------------------------------------
/test/Clownface/toString.test.js:
--------------------------------------------------------------------------------
1 | /* global describe, it */
2 |
3 | import assert from 'assert'
4 | import clownface from '../../index.js'
5 | import loadExample from '../support/example.js'
6 | import rdf from '../support/factory.js'
7 |
8 | describe('.toString', () => {
9 | it('should be a function', () => {
10 | const cf = clownface({ dataset: rdf.dataset() })
11 |
12 | assert.strictEqual(typeof cf.toString, 'function')
13 | })
14 |
15 | it('should return a string', () => {
16 | const cf = clownface({ dataset: rdf.dataset() })
17 |
18 | assert.strictEqual(typeof cf.toString(), 'string')
19 | })
20 |
21 | it('should return the value of a single term', async () => {
22 | const cf = clownface({
23 | dataset: await loadExample(),
24 | term: rdf.namedNode('http://localhost:8080/data/person/bernadette-rostenkowski'),
25 | })
26 |
27 | const result = cf.out(rdf.namedNode('http://schema.org/givenName')).toString()
28 |
29 | assert.strictEqual(result, 'Bernadette')
30 | })
31 |
32 | it('should return comma separated values if multiple terms', async () => {
33 | const cf = clownface({
34 | dataset: await loadExample(),
35 | term: [
36 | rdf.namedNode('http://localhost:8080/data/person/bernadette-rostenkowski'),
37 | rdf.namedNode('http://localhost:8080/data/person/howard-wolowitz'),
38 | ],
39 | })
40 |
41 | const givenName = cf.out(rdf.namedNode('http://schema.org/givenName')).toString()
42 |
43 | assert.strictEqual(givenName, 'Bernadette,Howard')
44 | })
45 | })
46 |
--------------------------------------------------------------------------------
/test/Clownface/any.test.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert'
2 | import { describe, it } from 'mocha'
3 | import clownface from '../../index.js'
4 | import rdf from '../support/factory.js'
5 |
6 | describe('.any', () => {
7 | it('should be a function', () => {
8 | const cf = clownface({ dataset: rdf.dataset() })
9 |
10 | assert.strictEqual(typeof cf.any, 'function')
11 | })
12 |
13 | it('should return any pointer object', () => {
14 | const pointer = clownface({ dataset: rdf.dataset() })
15 |
16 | const any = pointer.any()
17 |
18 | assert.strictEqual(typeof any.term, 'undefined')
19 | assert.deepStrictEqual(any.terms, [])
20 | })
21 |
22 | it('should remove current pointer', () => {
23 | const pointer = clownface({ dataset: rdf.dataset(), term: [rdf.blankNode(), rdf.blankNode()] })
24 |
25 | const any = pointer.any()
26 |
27 | assert.strictEqual(typeof any.term, 'undefined')
28 | assert.deepStrictEqual(any.terms, [])
29 | })
30 |
31 | it('should remove current pointers', () => {
32 | const pointer = clownface({ dataset: rdf.dataset(), term: [rdf.blankNode()] })
33 |
34 | const any = pointer.any()
35 |
36 | assert.strictEqual(typeof any.term, 'undefined')
37 | assert.deepStrictEqual(any.terms, [])
38 | })
39 |
40 | it('should keep same graph pointer in the result', () => {
41 | const cf = clownface({ dataset: rdf.dataset(), graph: rdf.namedNode('foo'), term: rdf.blankNode() })
42 |
43 | const anyPointer = cf.any()
44 |
45 | assert.strictEqual(anyPointer._context[0].graph.value, 'foo')
46 | })
47 | })
48 |
--------------------------------------------------------------------------------
/examples/namespace.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-extraneous-dependencies */
2 | import namespace from '@rdfjs/namespace'
3 | import clownface from '../index.js'
4 | import initExample from '../test/support/example.js'
5 |
6 | initExample().then(dataset => {
7 | const people = namespace('http://localhost:8080/data/person/')
8 | const schema = namespace('http://schema.org/')
9 |
10 | const tbbt = clownface({ dataset })
11 |
12 | const stuartBloom = tbbt.node(people('stuart-bloom'))
13 |
14 | console.log(`name of ${stuartBloom.values[0]}:`)
15 | console.log(
16 | stuartBloom
17 | .out([schema.givenName, schema.familyName])
18 | .values.join(' '),
19 | )
20 |
21 | console.log(`people ${stuartBloom.values[0]} knows:`)
22 | console.log(
23 | stuartBloom
24 | .out(schema.knows)
25 | .map(person => person
26 | .out([schema.givenName, schema.familyName])
27 | .values.join(' '),
28 | )
29 | .join(', '),
30 | )
31 |
32 | const apartment4a = tbbt.node('2311 North Los Robles Avenue, Aparment 4A')
33 |
34 | console.log(`people who live in ${apartment4a.values[0]}`)
35 | console.log(
36 | apartment4a
37 | .in(schema.streetAddress)
38 | .in(schema.address)
39 | .map(person => person.out(schema.givenName).value)
40 | .join(', '),
41 | )
42 |
43 | const neurobiologist = tbbt.has(schema.jobTitle, 'neurobiologist')
44 |
45 | console.log('people with the job title neurobiologist')
46 | console.log(
47 | neurobiologist
48 | .out(schema.givenName)
49 | .values.join(', '),
50 | )
51 | })
52 |
--------------------------------------------------------------------------------
/test/Clownface/factory.test.js:
--------------------------------------------------------------------------------
1 | /* global describe, it */
2 |
3 | import assert from 'assert'
4 | import clownface from '../../index.js'
5 | import rdf from '../support/factory.js'
6 | import Clownface from '../../lib/Clownface.js'
7 |
8 | describe('factory', () => {
9 | it('should create a Clownface object', () => {
10 | const dataset = rdf.dataset()
11 | const cf = clownface({ dataset })
12 |
13 | assert(cf instanceof Clownface)
14 | })
15 |
16 | it('should forward the dataset argument', () => {
17 | const dataset = rdf.dataset()
18 | const cf = clownface({ dataset })
19 |
20 | assert.strictEqual(cf._context.length, 1)
21 | assert.strictEqual(cf._context[0].dataset, dataset)
22 | })
23 |
24 | it('should forward the term argument', () => {
25 | const dataset = rdf.dataset()
26 | const term = rdf.namedNode('http://example.org/subject')
27 | const cf = clownface({ dataset, term })
28 |
29 | assert.strictEqual(cf._context.length, 1)
30 | assert.strictEqual(cf._context[0].term, term)
31 | })
32 |
33 | it('should forward the value argument', () => {
34 | const dataset = rdf.dataset()
35 | const value = 'abc'
36 | const cf = clownface({ dataset, value })
37 |
38 | assert.strictEqual(cf._context.length, 1)
39 | assert.strictEqual(cf._context[0].term.termType, 'Literal')
40 | assert.strictEqual(cf._context[0].term.value, value)
41 | })
42 |
43 | it('should forward the factory argument', () => {
44 | const dataset = rdf.dataset()
45 | const factory = rdf
46 | const cf = clownface({ dataset, factory })
47 |
48 | assert.strictEqual(cf.factory, factory)
49 | assert.strictEqual(cf._context[0].factory, factory)
50 | })
51 | })
52 |
--------------------------------------------------------------------------------
/test/Clownface/forEach.test.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert'
2 | import { describe, it } from 'mocha'
3 | import clownface from '../../index.js'
4 | import loadExample from '../support/example.js'
5 | import rdf from '../support/factory.js'
6 | import Clownface from '../../lib/Clownface.js'
7 |
8 | describe('.forEach', () => {
9 | it('should be a function', () => {
10 | const cf = clownface({ dataset: rdf.dataset() })
11 |
12 | assert.strictEqual(typeof cf.forEach, 'function')
13 | })
14 |
15 | it('should call the function with Dataset parameter', async () => {
16 | const cf = clownface({
17 | dataset: await loadExample(),
18 | term: rdf.namedNode('http://localhost:8080/data/person/bernadette-rostenkowski'),
19 | })
20 |
21 | cf.in(rdf.namedNode('http://schema.org/knows')).forEach(item => {
22 | assert(item instanceof Clownface)
23 |
24 | return true
25 | })
26 | })
27 |
28 | it('should call the function for each context', async () => {
29 | const cf = clownface({
30 | dataset: await loadExample(),
31 | term: rdf.namedNode('http://localhost:8080/data/person/bernadette-rostenkowski'),
32 | })
33 |
34 | let count = 0
35 |
36 | cf.in(rdf.namedNode('http://schema.org/knows')).forEach(() => {
37 | count++
38 |
39 | return true
40 | })
41 |
42 | assert.strictEqual(count, 7)
43 | })
44 |
45 | it('should return self', () => {
46 | const cf = clownface({
47 | dataset: rdf.dataset(),
48 | term: rdf.namedNode('http://localhost:8080/data/person/bernadette-rostenkowski'),
49 | })
50 |
51 | const forEachReturned = cf.forEach(() => {})
52 |
53 | assert.strictEqual(forEachReturned, cf)
54 | })
55 | })
56 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "clownface",
3 | "version": "2.0.3",
4 | "description": "Simple but powerful graph traversing library",
5 | "main": "index.js",
6 | "type": "module",
7 | "scripts": {
8 | "docs:build": "jsdoc2md --no-gfm -f index.js lib/* > docs/api.md; git add docs/api.md",
9 | "docs:serve": "docsify serve docs",
10 | "lint": "eslint . --quiet --ignore-path .gitignore",
11 | "test": "c8 --all --reporter=lcovonly mocha --recursive test",
12 | "release": "changeset publish"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "https://github.com/zazuko/clownface.git"
17 | },
18 | "keywords": [
19 | "rdf",
20 | "graph",
21 | "traversing"
22 | ],
23 | "author": "Thomas Bergwinkl (https://www.bergnet.org/people/bergi/card#me)",
24 | "license": "MIT",
25 | "bugs": {
26 | "url": "https://github.com/zazuko/clownface/issues"
27 | },
28 | "homepage": "https://zazuko.github.io/clownface/#/",
29 | "dependencies": {
30 | "@rdfjs/data-model": "^2.0.1",
31 | "@rdfjs/environment": "0 - 1",
32 | "@rdfjs/namespace": "^2.0.0"
33 | },
34 | "devDependencies": {
35 | "@changesets/cli": "^2.26.1",
36 | "@rdfjs/dataset": "^2.0.1",
37 | "@rdfjs/parser-n3": "^2.0.0",
38 | "@tpluscode/eslint-config": "^0.4.4",
39 | "c8": "^7.14.0",
40 | "chai": "^4.3.7",
41 | "docsify-cli": "^4.4.0",
42 | "husky": "^4.2.5",
43 | "jsdoc-to-markdown": "^5.0.3",
44 | "mocha": "^10.2.0",
45 | "rdf-dataset-ext": "^1.0.0",
46 | "rdf-ext": "^2.2.0",
47 | "rimraf": "^3.0.2",
48 | "sinon": "^9.0.2",
49 | "string-to-stream": "^3.0.1",
50 | "tbbt-ld": "^1.1.0"
51 | },
52 | "husky": {
53 | "hooks": {
54 | "pre-commit": "npm run docs:build"
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/docs/deep-dive.md:
--------------------------------------------------------------------------------
1 | # Deep dive
2 |
3 | If we would like to get the given name and family name of all persons known by e.g. Stuart Bloom
4 | (from [tbbt-ld][tbbt]) we can write:
5 |
6 |
7 |
8 | ```js
9 | const rdf = require('@zazuko/env-bundle')
10 |
11 | // load the tbbt-ld graph
12 | const dataset = await rdf.fetch('http://zazuko.github.io/tbbt-ld/dist/tbbt.nt')
13 | .then(response => response.dataset())
14 |
15 | // turn the RDF/JS dataset into a Clownface context
16 | const tbbt = rdf.clownface({ dataset })
17 |
18 | // get a starting node inside the dataset
19 | const stuartBloom = tbbt.namedNode('http://localhost:8080/data/person/stuart-bloom')
20 |
21 | // query for all people Stuart knows and print their full name
22 | stuartBloom
23 | // get all nodes connected through schema:knows
24 | .out(rdf.ns.schema.knows)
25 | // for every result
26 | .map((person) => {
27 | // get their schema:givenName and schema:familyName
28 | const personalInformation = person.out([
29 | rdf.ns.schema.givenName,
30 | rdf.ns.schema.familyName
31 | ])
32 | // join the givenName and familyName with a space
33 | return personalInformation.values.join(' ')
34 | })
35 | // join the list of names with a comma
36 | .join(', ')
37 | ```
38 |
39 |
40 |
41 | [tbbt]: https://github.com/zazuko/tbbt-ld
42 |
43 | ## Notes
44 |
45 | See that unlike the [Getting started](/#getting-started) example, which uses absolute URIs to refer to terms, the code above imports the [@tpluscode/rdf-ns-builders](https://npm.im/@tpluscode/rdf-ns-builders) library which exports a number of common RDF vocabularies wrapped as [namespace builder objects](https://npm.im/@rdfjs/namespace).
46 |
47 | Thus, typing `schema.givenName` is equivalent to `namedNode('http://schema.org/givenName')`
48 |
--------------------------------------------------------------------------------
/lib/languageTag.js:
--------------------------------------------------------------------------------
1 | import RDF from './environment.js'
2 | import namespace from './namespace.js'
3 |
4 | const ns = namespace(RDF)
5 |
6 | function mapLiteralsByLanguage(map, current) {
7 | const notLiteral = current.termType !== 'Literal'
8 | const notStringLiteral = ns.langString.equals(current.datatype) || ns.xsd.string.equals(current.datatype)
9 |
10 | if (notLiteral || !notStringLiteral) return map
11 |
12 | const language = current.language.toLowerCase()
13 |
14 | if (map.has(language)) {
15 | map.get(language).push(current)
16 | } else {
17 | map.set(language, [current])
18 | }
19 |
20 | return map
21 | }
22 |
23 | function createLanguageMapper(objects) {
24 | const literalsByLanguage = objects.reduce(mapLiteralsByLanguage, new Map())
25 | const langMapEntries = [...literalsByLanguage.entries()]
26 |
27 | return language => {
28 | const languageLowerCase = language.toLowerCase()
29 |
30 | if (languageLowerCase === '*') {
31 | return langMapEntries[0] && langMapEntries[0][1]
32 | }
33 |
34 | const exactMatch = literalsByLanguage.get(languageLowerCase)
35 | if (exactMatch) {
36 | return exactMatch
37 | }
38 |
39 | const secondaryMatches = langMapEntries.find(([entryLanguage]) => entryLanguage.startsWith(languageLowerCase))
40 |
41 | return secondaryMatches && secondaryMatches[1]
42 | }
43 | }
44 |
45 | /**
46 | *
47 | * @param {Term[]} terms
48 | * @param {object} [options]
49 | * @param {string | string[]} [options.language]
50 | * @returns {Term[]}
51 | */
52 | export function filterTaggedLiterals(terms, { language }) {
53 | const languages = (typeof language === 'string' ? [language] : language)
54 | const getLiteralsForLanguage = createLanguageMapper(terms)
55 |
56 | return languages.map(getLiteralsForLanguage).find(Boolean) || []
57 | }
58 |
--------------------------------------------------------------------------------
/test/Clownface/map.test.js:
--------------------------------------------------------------------------------
1 | /* global describe, it */
2 |
3 | import assert from 'assert'
4 | import clownface from '../../index.js'
5 | import loadExample from '../support/example.js'
6 | import rdf from '../support/factory.js'
7 | import Clownface from '../../lib/Clownface.js'
8 |
9 | describe('.map', () => {
10 | it('should be a function', () => {
11 | const cf = clownface({ dataset: rdf.dataset() })
12 |
13 | assert.strictEqual(typeof cf.map, 'function')
14 | })
15 |
16 | it('should return an Array', () => {
17 | const cf = clownface({ dataset: rdf.dataset() })
18 |
19 | assert(Array.isArray(cf.map(item => item)))
20 | })
21 |
22 | it('should call the function with Dataset parameter', async () => {
23 | const cf = clownface({
24 | dataset: await loadExample(),
25 | term: rdf.namedNode('http://localhost:8080/data/person/bernadette-rostenkowski'),
26 | })
27 |
28 | cf.in(rdf.namedNode('http://schema.org/knows')).map(item => {
29 | assert(item instanceof Clownface)
30 |
31 | return true
32 | })
33 | })
34 |
35 | it('should call the function for each context', async () => {
36 | const cf = clownface({
37 | dataset: await loadExample(),
38 | term: rdf.namedNode('http://localhost:8080/data/person/bernadette-rostenkowski'),
39 | })
40 |
41 | let count = 0
42 |
43 | cf.in(rdf.namedNode('http://schema.org/knows')).map(() => {
44 | count++
45 |
46 | return true
47 | })
48 |
49 | assert.strictEqual(count, 7)
50 | })
51 |
52 | it('should return an array of all return values', async () => {
53 | const cf = clownface({
54 | dataset: await loadExample(),
55 | term: rdf.namedNode('http://localhost:8080/data/person/bernadette-rostenkowski'),
56 | })
57 |
58 | const result = cf.in(rdf.namedNode('http://schema.org/knows')).map((item, index) => index)
59 |
60 | assert.deepStrictEqual(result, [0, 1, 2, 3, 4, 5, 6])
61 | })
62 | })
63 |
--------------------------------------------------------------------------------
/docs/named-graphs.md:
--------------------------------------------------------------------------------
1 | # Working with named graphs
2 |
3 | ## Default behavior
4 |
5 | The examples on all other pages do not specify any graph identifier. In this mode the traversal methods are free to navigate the entire dataset (aka. [union graph](https://patterns.dataincubator.org/book/union-graph.html)) but any triples added and removed only apply to the default graph.
6 |
7 |
8 |
9 | ```js
10 | const rdf = require('@zazuko/env-bundle')
11 | const { nquads } = require('@tpluscode/rdf-string@0.2.26')
12 |
13 | const tbbt = rdf.namespace('https://bigbangtheory.tv/')
14 |
15 | const quads = [
16 | rdf.quad(tbbt.Leonard, rdf.ns.schema.knows, tbbt.Amy, tbbt.Amy),
17 | rdf.quad(tbbt.Leonard, rdf.ns.schema.knows, tbbt.Sheldon, tbbt.Sheldon),
18 | ]
19 |
20 | const leonard = rdf.clownface({ dataset: rdf.dataset(quads) })
21 | .namedNode(tbbt.Leonard)
22 | .addOut(rdf.ns.schema.name, 'Leonard')
23 |
24 | console.log('Leonard knows ' + leonard.out(rdf.ns.schema.knows).values.join(', '))
25 |
26 | nquads`${leonard.dataset}`.toString()
27 | ```
28 |
29 |
30 |
31 | ## Single graph
32 |
33 | A graph identifier can be passed to the factory call, which narrows down the context to only a specific graph.
34 |
35 |
36 |
37 | ```js
38 | const rdf = require('@zazuko/env-bundle')
39 | const { nquads } = require('@tpluscode/rdf-string@0.2.26')
40 |
41 | const tbbt = rdf.namespace('https://bigbangtheory.tv/')
42 |
43 | const quads = [
44 | rdf.quad(tbbt.Leonard, rdf.ns.schema.name, 'Leonard', tbbt.Leonard),
45 | rdf.quad(tbbt.Sheldon, rdf.ns.schema.name, 'Sheldon', tbbt.Sheldon),
46 | ]
47 |
48 | // pass a NamedNode
49 | // or use RDF.defaultGraph() for the default graph
50 | const leonard = rdf.clownface({ dataset: rdf.dataset(quads), graph: tbbt.Leonard })
51 | .node(tbbt.Leonard)
52 | .addOut(rdf.ns.schema.knows, tbbt.Sheldon)
53 |
54 | nquads`${leonard.dataset}`.toString()
55 | ```
56 |
57 |
58 |
--------------------------------------------------------------------------------
/test/term.test.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert'
2 | import { describe, it } from 'mocha'
3 | import term from '../lib/term.js'
4 | import rdf from './support/factory.js'
5 |
6 | describe('term', () => {
7 | it('should be a function', () => {
8 | assert.strictEqual(typeof term, 'function')
9 | })
10 |
11 | it('should return undefined if no argument is given', () => {
12 | const result = term()
13 |
14 | assert.strictEqual(typeof result, 'undefined')
15 | })
16 |
17 | it('should return undefined if null is given', () => {
18 | const result = term(null)
19 |
20 | assert.strictEqual(typeof result, 'undefined')
21 | })
22 |
23 | it('should create a NamedNode if only a URL object is given', () => {
24 | const url = new URL('http://localhost:8080/test')
25 | const result = term(url, undefined, undefined, rdf)
26 |
27 | assert.strictEqual(result.termType, 'NamedNode')
28 | assert.strictEqual(result.value, url.toString())
29 | })
30 |
31 | it('should create a Literal if only a string is given', () => {
32 | const result = term('test', undefined, undefined, rdf)
33 |
34 | assert.strictEqual(result.termType, 'Literal')
35 | assert.strictEqual(result.value, 'test')
36 | })
37 |
38 | it('should create a BlankNode if the type is BlankNode', () => {
39 | const result = term(null, 'BlankNode', undefined, rdf)
40 |
41 | assert.strictEqual(result.termType, 'BlankNode')
42 | })
43 |
44 | it('should use the value as blank node identifier', () => {
45 | const result = term('test', 'BlankNode', undefined, rdf)
46 |
47 | assert.strictEqual(result.termType, 'BlankNode')
48 | assert.strictEqual(result.value, 'test')
49 | })
50 |
51 | it('should create a NamedNode if the type is NamedNode', () => {
52 | const result = term('http://example.org/', 'NamedNode', undefined, rdf)
53 |
54 | assert.strictEqual(result.termType, 'NamedNode')
55 | assert.strictEqual(result.value, 'http://example.org/')
56 | })
57 | })
58 |
--------------------------------------------------------------------------------
/test/Clownface/deleteList.test.js:
--------------------------------------------------------------------------------
1 | /* global describe, it */
2 |
3 | import assert from 'assert'
4 | import addAll from 'rdf-dataset-ext/addAll.js'
5 | import clownface from '../../index.js'
6 | import rdf from '../support/factory.js'
7 | import * as ns from '../support/namespace.js'
8 |
9 | describe('.deleteList', () => {
10 | it('should be a function', () => {
11 | const cf = clownface({ dataset: rdf.dataset() })
12 |
13 | assert.strictEqual(typeof cf.deleteList, 'function')
14 | })
15 |
16 | it('should throw an error if predicate parameter is missing', () => {
17 | const dataset = rdf.dataset()
18 | const subject = rdf.namedNode('http://localhost:8080/data/person/mary-cooper')
19 | const cf = clownface({
20 | dataset,
21 | term: subject,
22 | })
23 |
24 | let touched = false
25 |
26 | try {
27 | cf.deleteList(null)
28 | } catch (err) {
29 | touched = true
30 | }
31 |
32 | assert(touched)
33 | })
34 |
35 | it('should remove list quads using the context term as subject and the given predicate', () => {
36 | const dataset = rdf.dataset()
37 | const subject = rdf.namedNode('http://localhost:8080/data/person/mary-cooper')
38 | const predicate = rdf.namedNode('http://schema.org/counts')
39 | const predicateOther = rdf.namedNode('http://schema.org/other')
40 | const item0 = rdf.literal('0')
41 | const item1 = rdf.literal('1')
42 | const first0 = rdf.blankNode()
43 | const first1 = rdf.blankNode()
44 | const other = rdf.quad(subject, predicateOther, item0)
45 | const cf = clownface({
46 | dataset,
47 | term: subject,
48 | })
49 |
50 | addAll(dataset, [
51 | other,
52 | rdf.quad(subject, predicate, first0),
53 | rdf.quad(first0, ns.first, item0),
54 | rdf.quad(first0, ns.rest, first1),
55 | rdf.quad(first1, ns.first, item1),
56 | rdf.quad(first1, ns.rest, ns.nil),
57 | ])
58 |
59 | cf.deleteList(predicate)
60 |
61 | assert.strictEqual(dataset.size, 1)
62 | assert([...dataset][0].equals(other))
63 | })
64 | })
65 |
--------------------------------------------------------------------------------
/lib/term.js:
--------------------------------------------------------------------------------
1 | import { toLiteral } from './fromPrimitive.js'
2 |
3 | function blankNode(value, factory) {
4 | if (value && typeof value !== 'string') {
5 | throw new Error('Blank node identifier must be a string')
6 | }
7 |
8 | return factory.blankNode(value)
9 | }
10 |
11 | function literal(value, languageOrDatatype, factory) {
12 | if (typeof value === 'string') {
13 | // check if it's given, if given try RDF/JS Term value otherwise convert it to a string
14 | languageOrDatatype = languageOrDatatype && (languageOrDatatype.value || languageOrDatatype.toString())
15 |
16 | if (languageOrDatatype && languageOrDatatype.indexOf(':') !== -1) {
17 | languageOrDatatype = factory.namedNode(languageOrDatatype)
18 | }
19 |
20 | return factory.literal(value.toString(), languageOrDatatype)
21 | }
22 |
23 | const term = toLiteral(value, factory)
24 |
25 | if (!term) {
26 | throw new Error('The value cannot be converted to a literal node')
27 | }
28 |
29 | return term
30 | }
31 |
32 | function namedNode(value, factory) {
33 | if (typeof value !== 'string') {
34 | throw new Error('Named node must be an IRI string')
35 | }
36 |
37 | return factory.namedNode(value)
38 | }
39 |
40 | export default function term(value, type = 'Literal', languageOrDatatype, factory) {
41 | // it's already a RDF/JS Term
42 | if (value && typeof value === 'object' && value.termType) {
43 | return value
44 | }
45 |
46 | // check if it's a URL object
47 | if (value && value.constructor.name === 'URL') {
48 | return namedNode(value.toString(), factory)
49 | }
50 |
51 | // check if it's a blank node...
52 | if (type === 'BlankNode') {
53 | return blankNode(value, factory)
54 | }
55 |
56 | // ...cause that's the only type that doesn't require a value
57 | if (value === null || typeof value === 'undefined') {
58 | return undefined
59 | }
60 |
61 | if (type === 'Literal') {
62 | return literal(value, languageOrDatatype, factory)
63 | }
64 |
65 | if (type === 'NamedNode') {
66 | return namedNode(value, factory)
67 | }
68 |
69 | throw new Error('unknown type')
70 | }
71 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # clownface
2 |
3 | [](https://travis-ci.org/github/zazuko/clownface)
4 | [](https://npm.im/clownface)
5 |
6 | Clownface is a graph traversal library inspired by [Gremlin](https://tinkerpop.apache.org/gremlin.html) which allows to query any [RDF dataset](https://rdf.js.org/dataset-spec/) in a concise and readable way.
7 |
8 | Clownface greatly simplifies interacting with RDF data in JavaScript.
9 |
10 | # Quick start
11 |
12 | The recommended way is to use clownface with and RDF/JS environment.
13 | It also requires [`DataFactory`](https://rdf.js.org/data-model-spec/#datafactory-interface) and [`DatasetFactory`](https://rdf.js.org/dataset-spec/#datasetfactory-interface), for example those provided by [`@rdfjs/data-model`](https://npm.im/@rdfjs/data-model) and [`@rdfjs/dataset`](https://npm.im/@rdfjs/dataset) packages respectively, as well as [`@rdfjs/namespace`](https://npm.im/@rdfjs/namespace).
14 |
15 | ```shell
16 | npm install clownface @rdfjs/environment @rdfjs/data-model @rdfjs/dataset @rdfjs/namespace
17 | ````
18 |
19 | ```js
20 | import Environment from '@rdfjs/environment/Environment.js'
21 | import NamespaceFactory from '@rdfjs/namespace/Factory.js'
22 | import DatasetFactory from '@rdfjs/dataset/Factory.js'
23 | import DataFactory from '@rdfjs/data-model/Factory.js'
24 | import ClownfaceFactory from 'clownface/Factory.js'
25 |
26 | const $rdf = new Environment([
27 | NamespaceFactory,
28 | DatasetFactory,
29 | DataFactory,
30 | ClownfaceFactory
31 | ])
32 |
33 | const graph = $rdf.clownface()
34 | ```
35 |
36 | Alternatively, if you already use [@zazuko/env](https://npm.im/@zazuko/env), it comes bundled with clownface and its dependencies.
37 |
38 | ```js
39 | import $rdf from '@zazuko/env'
40 |
41 | const graph = $rdf.clownface()
42 | ```
43 |
44 | # Learn more
45 |
46 | If you are new to RDF and JavaScript, consider our [Getting Started](https://zazuko.com/get-started/developers/#traverse-an-rdf-graph) guide that also covers Clownface basics.
47 |
48 | For API documentation and examples, see http://zazuko.github.io/clownface/.
49 |
--------------------------------------------------------------------------------
/docs/rdf-lists.md:
--------------------------------------------------------------------------------
1 | # RDF Lists
2 |
3 | RDF Lists are unavoidable when working with RDF data and their bare triples representation make it necessary to provide specialized programmatic APIs to handle.
4 |
5 | Clownface comes with handy methods to create, iterate and remove lists from RDF datasets.
6 |
7 | - `addList` method is similar to `addOut` but requires an array of object which it links as an `rdf:List`
8 | - `list` method returns an `Iterator` of the values of an `rdf:List`
9 | - finally `deleteList` removes the list nodes
10 |
11 |
12 |
13 | ```js
14 | const rdf = require('@zazuko/env-bundle')
15 | const { turtle } = require('@tpluscode/rdf-string@0.2.26')
16 |
17 | const ex = rdf.namespace('http://example.com/')
18 |
19 | const game = rdf.clownface()
20 | .namedNode(ex.game)
21 |
22 | // Create a list with 3 elements
23 | game.addList(ex.score, [ex.score1, ex.score2, ex.score3])
24 |
25 | // Add statements about the list elements
26 | let scoreIndex = 1
27 | for (const score of game.out(ex.score).list()) {
28 | score.addOut(rdf.ns.dtype.orderIndex, scoreIndex++)
29 | }
30 |
31 | // Remove the list but not statements about the individual scores
32 | game.deleteList(ex.score)
33 |
34 | turtle`${game.dataset}`.toString()
35 | ```
36 |
37 |
38 |
39 | ## Handling non-lists
40 |
41 | The `list()` method will return null when the object is not a list (such as a literal or without a `rdf:first`). A little bit of defensive programming can be employed to conditionally iterate a list or get the non-list objects of a property.
42 |
43 |
44 |
45 | ```js
46 | const rdf = require('@zazuko/env-bundle')
47 | const { turtle } = require('@tpluscode/rdf-string@0.2.26')
48 |
49 | const ex = rdf.namespace('http://example.com/')
50 |
51 | const game = rdf.clownface()
52 | .namedNode(ex.game)
53 |
54 | // add object directly to ex:score
55 | game.addOut(ex.score, [ex.score1, ex.score2, ex.score3])
56 |
57 | // pretend we don't know if the value is a list of scores
58 | const scores = game.out(ex.score)
59 | const list = scores.list()
60 |
61 | // iterate the raw objects as alternative
62 | ;(list && [...list]) || scores.toArray()
63 | ```
64 |
65 |
66 |
--------------------------------------------------------------------------------
/test/filter.test.js:
--------------------------------------------------------------------------------
1 | /* global describe, it */
2 |
3 | import assert from 'assert'
4 | import clownface from '../index.js'
5 | import { taggedLiteral } from '../filter.js'
6 | import rdf from './support/factory.js'
7 |
8 | describe('clownface/filter', () => {
9 | describe('.taggedLiteral', () => {
10 | it('should return literals for language', async () => {
11 | const schemaName = rdf.namedNode('http://schema.org/givenName')
12 | const cf = clownface({ dataset: rdf.dataset() })
13 | .blankNode()
14 | .addOut(schemaName, [
15 | rdf.literal('Zurich', 'en'),
16 | rdf.literal('Zürich', 'de'),
17 | rdf.literal('Zurych', 'pl')])
18 | .out(schemaName)
19 |
20 | const result = cf.filter(taggedLiteral(['de']))
21 |
22 | assert(result.term.equals(rdf.literal('Zürich', 'de')))
23 | })
24 |
25 | it('should work with multi-pointer', async () => {
26 | const containsPlace = rdf.namedNode('http://schema.org/containsPlace')
27 | const name = rdf.namedNode('http://schema.org/name')
28 | const cf = clownface({ dataset: rdf.dataset() })
29 | cf.namedNode('Europe').addOut(containsPlace, [
30 | cf.namedNode('CH')
31 | .addOut(name, cf.literal('Switzerland', 'en'))
32 | .addOut(name, cf.literal('Die Schweiz', 'de')),
33 | cf.namedNode('PL')
34 | .addOut(name, cf.literal('Poland', 'en'))
35 | .addOut(name, cf.literal('Polen', 'de')),
36 | ])
37 |
38 | const multi = cf.node([rdf.namedNode('CH'), rdf.namedNode('PL')]).out(name)
39 | const result = multi.filter(taggedLiteral(['de']))
40 |
41 | const terms = rdf.termSet(result.terms)
42 | assert.strictEqual(result.terms.length, 2)
43 | assert(terms.has(rdf.literal('Die Schweiz', 'de')))
44 | assert(terms.has(rdf.literal('Polen', 'de')))
45 | })
46 |
47 | it('should ignore non-literals', async () => {
48 | const schemaKnows = rdf.namedNode('http://schema.org/knows')
49 | const cf = clownface({ dataset: rdf.dataset() })
50 | .blankNode()
51 | .addOut(schemaKnows, [rdf.blankNode(), rdf.blankNode()])
52 |
53 | const result = cf.out(schemaKnows).filter(taggedLiteral(['de']))
54 |
55 | assert.strictEqual(result.terms.length, 0)
56 | })
57 | })
58 | })
59 |
--------------------------------------------------------------------------------
/test/Clownface/filter.test.js:
--------------------------------------------------------------------------------
1 | /* global describe, it */
2 |
3 | import assert from 'assert'
4 | import clownface from '../../index.js'
5 | import loadExample from '../support/example.js'
6 | import rdf from '../support/factory.js'
7 | import Clownface from '../../lib/Clownface.js'
8 | import Context from '../../lib/Context.js'
9 |
10 | describe('.filter', () => {
11 | it('should be a function', () => {
12 | const cf = clownface({ dataset: rdf.dataset() })
13 |
14 | assert.strictEqual(typeof cf.filter, 'function')
15 | })
16 |
17 | it('should return a Dataset instance', () => {
18 | const cf = clownface({ dataset: rdf.dataset() })
19 |
20 | assert(cf.filter(() => true) instanceof Clownface)
21 | })
22 |
23 | it('should return instance with _context of correct type', () => {
24 | const cf = clownface({ dataset: rdf.dataset() }).namedNode()
25 |
26 | const [context] = cf.filter(() => true)._context
27 |
28 | assert(context instanceof Context)
29 | })
30 |
31 | it('should call the function with Dataset parameter', async () => {
32 | const cf = clownface({
33 | dataset: await loadExample(),
34 | term: rdf.namedNode('http://localhost:8080/data/person/bernadette-rostenkowski'),
35 | })
36 |
37 | cf.in(rdf.namedNode('http://schema.org/knows')).filter(item => {
38 | assert(item instanceof Clownface)
39 |
40 | return true
41 | })
42 | })
43 |
44 | it('should call the function for each context', async () => {
45 | const cf = clownface({
46 | dataset: await loadExample(),
47 | term: rdf.namedNode('http://localhost:8080/data/person/bernadette-rostenkowski'),
48 | })
49 |
50 | let count = 0
51 |
52 | cf.in(rdf.namedNode('http://schema.org/knows')).filter(() => {
53 | count++
54 |
55 | return true
56 | })
57 |
58 | assert.strictEqual(count, 7)
59 | })
60 |
61 | it('should filter the context based on the return value of the function', async () => {
62 | const cf = clownface({
63 | dataset: await loadExample(),
64 | term: rdf.namedNode('http://localhost:8080/data/person/bernadette-rostenkowski'),
65 | })
66 |
67 | const result = cf.in(rdf.namedNode('http://schema.org/knows')).filter(item => {
68 | return !item.terms.every(term => {
69 | return term.equals(rdf.namedNode('http://localhost:8080/data/person/howard-wolowitz'))
70 | })
71 | })
72 |
73 | assert.strictEqual(result._context.length, 6)
74 | })
75 | })
76 |
--------------------------------------------------------------------------------
/docs/traversal.md:
--------------------------------------------------------------------------------
1 | # Graph traversal
2 |
3 | A core functionality of clownface is moving between nodes along properties, or edges in graph lingo. That is done by chainable `out`/`in` methods which accept zero more properties as arguments. These methods move the graph pointer(s) from the current context by following properties where it is the `subject` or `object` respectively.
4 |
5 | When called with no properties (or empty array), all properties are traversed and returned as a context of multiple graph pointers (if multiple nodes were found that is).
6 |
7 |
8 |
9 | ```js
10 | const rdf = require('@zazuko/env-bundle')
11 |
12 | const dataset = await rdf.fetch('http://zazuko.github.io/tbbt-ld/dist/tbbt.nt')
13 | .then(response => response.dataset())
14 |
15 | rdf.clownface({ dataset })
16 | .namedNode('http://localhost:8080/data/person/howard-wolowitz')
17 | .in(rdf.ns.schema.spouse)
18 | .out(rdf.ns.schema.knows)
19 | .out(rdf.ns.schema.address)
20 | .out(rdf.ns.schema.addressLocality)
21 | .values
22 | ```
23 |
24 |
25 |
26 | It is important that every call to `in` or `out` may return multiple pointers and they may be duplicate instances of the same node but are indistinguishable. If multiple predicates are used it is currently impossible to tell which one was traversed to reach any given node.
27 |
28 | The above traversal would be equivalent to a SPARQL property path `^schema:spouse/schema:knows/schema:address/schema:addressLocality`.
29 |
30 | ## Node lookup
31 |
32 | Another way for getting to the desired nodes is to find them using the `has` method. It has two parameters: properties and objects. The first one is required and narrows down the context to only those subjects where the property is used. The results can be narrowed down further by passing a second argument which filter to specific object.
33 |
34 | Both those parameters and be a single value or an array.
35 |
36 | When the context does not represent any pointer, all subjects from the dataset are considered.
37 |
38 |
39 |
40 | ```js
41 | const rdf = require('@zazuko/env-bundle')
42 |
43 | const dataset = await rdf.fetch('http://zazuko.github.io/tbbt-ld/dist/tbbt.nt')
44 | .then(response => response.dataset())
45 |
46 | const howard = rdf.namedNode('http://localhost:8080/data/person/howard-wolowitz')
47 |
48 | // all people whose spouse is Howard
49 | const hasSpouse = rdf.clownface({ dataset })
50 | .has(rdf.ns.schema.spouse, howard)
51 | .values
52 | ```
53 |
54 |
55 |
--------------------------------------------------------------------------------
/test/Clownface/in.test.js:
--------------------------------------------------------------------------------
1 | /* global describe, it */
2 |
3 | import assert from 'assert'
4 | import clownface from '../../index.js'
5 | import loadExample from '../support/example.js'
6 | import rdf from '../support/factory.js'
7 | import * as ns from '../support/namespace.js'
8 | import Clownface from '../../lib/Clownface.js'
9 |
10 | describe('.in', () => {
11 | it('should be a function', () => {
12 | const cf = clownface({ dataset: rdf.dataset() })
13 |
14 | assert.strictEqual(typeof cf.in, 'function')
15 | })
16 |
17 | it('should return a new Clownface instance', async () => {
18 | const cf = clownface({
19 | dataset: await loadExample(),
20 | value: '2311 North Los Robles Avenue, Aparment 4A',
21 | })
22 |
23 | const result = cf.in(rdf.namedNode('http://schema.org/streetAddress'))
24 |
25 | assert(result instanceof Clownface)
26 | assert.notStrictEqual(result, cf)
27 | })
28 |
29 | it('should search object -> subject without predicate', async () => {
30 | const cf = clownface({
31 | dataset: await loadExample(),
32 | term: ns.tbbtp('bernadette-rostenkowski'),
33 | })
34 |
35 | const result = cf.in()
36 |
37 | assert.strictEqual(result._context.length, 8)
38 | })
39 |
40 | it('should search object -> subject with predicate', async () => {
41 | const cf = clownface({
42 | dataset: await loadExample(),
43 | value: '2311 North Los Robles Avenue, Aparment 4A',
44 | })
45 |
46 | const result = cf.in(rdf.namedNode('http://schema.org/streetAddress'))
47 |
48 | assert.strictEqual(result._context.length, 2)
49 | assert.strictEqual(result._context[0].term.termType, 'BlankNode')
50 | })
51 |
52 | it('should support multiple predicate values in an array', async () => {
53 | const cf = clownface({
54 | dataset: await loadExample(),
55 | term: rdf.namedNode('http://localhost:8080/data/person/bernadette-rostenkowski'),
56 | })
57 |
58 | const result = cf.in([
59 | rdf.namedNode('http://schema.org/spouse'),
60 | rdf.namedNode('http://schema.org/knows'),
61 | ])
62 |
63 | assert.strictEqual(result._context.length, 8)
64 | })
65 |
66 | it('should support clownface objects as predicates', async () => {
67 | const cf = clownface({
68 | dataset: await loadExample(),
69 | term: rdf.namedNode('http://localhost:8080/data/person/bernadette-rostenkowski'),
70 | })
71 |
72 | const result = cf.in(cf.node([
73 | rdf.namedNode('http://schema.org/spouse'),
74 | rdf.namedNode('http://schema.org/knows'),
75 | ]))
76 |
77 | assert.strictEqual(result._context.length, 8)
78 | })
79 | })
80 |
--------------------------------------------------------------------------------
/test/fromPrimitive.test.js:
--------------------------------------------------------------------------------
1 | /* global describe, it */
2 |
3 | import assert from 'assert'
4 | import namespace from '@rdfjs/namespace'
5 | import { booleanToLiteral, numberToLiteral, stringToLiteral, toLiteral } from '../lib/fromPrimitive.js'
6 | import rdf from './support/factory.js'
7 |
8 | const xsd = namespace('http://www.w3.org/2001/XMLSchema#')
9 |
10 | describe('fromPrimitive', () => {
11 | describe('booleanToLiteral', () => {
12 | it('is function', () => {
13 | assert.strictEqual(typeof booleanToLiteral, 'function')
14 | })
15 |
16 | it('returns null if non boolean value is given', () => {
17 | assert.strictEqual(booleanToLiteral(3), null)
18 | })
19 |
20 | it('returns Literal with xsd:boolean datatype for true value', () => {
21 | assert(rdf.literal('true', xsd.boolean), booleanToLiteral(true))
22 | })
23 |
24 | it('returns Literal with xsd:boolean datatype for false value', () => {
25 | assert(rdf.literal('false', xsd.boolean), booleanToLiteral(false))
26 | })
27 | })
28 |
29 | describe('numberToLiteral', () => {
30 | it('is function', () => {
31 | assert.strictEqual(typeof numberToLiteral, 'function')
32 | })
33 |
34 | it('returns null if non number value is given', () => {
35 | assert.strictEqual(numberToLiteral(true), null)
36 | })
37 |
38 | it('returns Literal with xsd:double datatype for number value', () => {
39 | assert(rdf.literal('3.21', xsd.double).equals(numberToLiteral(3.21)))
40 | })
41 |
42 | it('returns Literal with xsd:integer datatype for a number value that is an integer', () => {
43 | assert(rdf.literal('321', xsd.integer).equals(numberToLiteral(321)))
44 | })
45 | })
46 |
47 | describe('stringToLiteral', () => {
48 | it('is function', () => {
49 | assert.strictEqual(typeof stringToLiteral, 'function')
50 | })
51 |
52 | it('returns null if non string value is given', () => {
53 | assert.strictEqual(stringToLiteral(true), null)
54 | })
55 |
56 | it('returns Literal', () => {
57 | assert(rdf.literal('M42').equals(stringToLiteral('M42')))
58 | })
59 | })
60 |
61 | describe('toLiteral', () => {
62 | it('is function', () => {
63 | assert.strictEqual(typeof toLiteral, 'function')
64 | })
65 |
66 | it('returns null if a value with an unknown type is given', () => {
67 | assert.strictEqual(toLiteral({}), null)
68 | })
69 |
70 | it('returns Literal with xsd:boolean datatype for a boolean value', () => {
71 | assert(rdf.literal('true', xsd.boolean), toLiteral(true))
72 | })
73 |
74 | it('returns Literal with xsd:double datatype for a number value', () => {
75 | assert(rdf.literal('3.21', xsd.double).equals(toLiteral(3.21)))
76 | })
77 |
78 | it('returns Literal', () => {
79 | assert(rdf.literal('M42').equals(toLiteral('M42')))
80 | })
81 | })
82 | })
83 |
--------------------------------------------------------------------------------
/test/Clownface/blankNode.test.js:
--------------------------------------------------------------------------------
1 | /* global describe, it */
2 |
3 | import assert from 'assert'
4 | import clownface from '../../index.js'
5 | import rdf from '../support/factory.js'
6 | import Clownface from '../../lib/Clownface.js'
7 |
8 | describe('.blankNode', () => {
9 | it('should be a function', () => {
10 | const cf = clownface({ dataset: rdf.dataset() })
11 |
12 | assert.strictEqual(typeof cf.blankNode, 'function')
13 | })
14 |
15 | it('should return a new Clownface instance', () => {
16 | const cf = clownface({ dataset: rdf.dataset() })
17 |
18 | const result = cf.blankNode()
19 |
20 | assert(result instanceof Clownface)
21 | assert.notStrictEqual(result, cf)
22 | })
23 |
24 | it('should use the dataset from the context', () => {
25 | const dataset = rdf.dataset()
26 | const cf = clownface({ dataset })
27 |
28 | const result = cf.blankNode()
29 |
30 | assert.strictEqual(result._context[0].dataset, dataset)
31 | })
32 |
33 | it('should create a context with a Blank Node term', () => {
34 | const cf = clownface({ dataset: rdf.dataset() })
35 |
36 | const result = cf.blankNode()
37 |
38 | assert.strictEqual(result._context.length, 1)
39 | assert.strictEqual(result._context[0].term.termType, 'BlankNode')
40 | })
41 |
42 | it('should use the given label for the Blank Node', () => {
43 | const label = 'b321'
44 | const cf = clownface({ dataset: rdf.dataset() })
45 |
46 | const result = cf.blankNode(label)
47 |
48 | assert.strictEqual(result._context.length, 1)
49 | assert.strictEqual(result._context[0].term.termType, 'BlankNode')
50 | assert.strictEqual(result._context[0].term.value, label)
51 | })
52 |
53 | it('should support multiple values in an array', () => {
54 | const labelA = 'b321a'
55 | const labelB = 'b321b'
56 | const cf = clownface({ dataset: rdf.dataset() })
57 |
58 | const result = cf.blankNode([labelA, labelB])
59 |
60 | assert.strictEqual(result._context.length, 2)
61 | assert.strictEqual(result._context[0].term.termType, 'BlankNode')
62 | assert.strictEqual(result._context[0].term.value, labelA)
63 | assert.strictEqual(result._context[1].term.termType, 'BlankNode')
64 | assert.strictEqual(result._context[1].term.value, labelB)
65 | })
66 |
67 | it('should support multiple values in an iterable', () => {
68 | const labelA = 'b321a'
69 | const labelB = 'b321b'
70 | const cf = clownface({ dataset: rdf.dataset() })
71 |
72 | const result = cf.blankNode(new Set([labelA, labelB]))
73 |
74 | assert.strictEqual(result._context.length, 2)
75 | assert.strictEqual(result._context[0].term.termType, 'BlankNode')
76 | assert.strictEqual(result._context[0].term.value, labelA)
77 | assert.strictEqual(result._context[1].term.termType, 'BlankNode')
78 | assert.strictEqual(result._context[1].term.value, labelB)
79 | })
80 | })
81 |
--------------------------------------------------------------------------------
/test/Clownface/deleteIn.test.js:
--------------------------------------------------------------------------------
1 | /* global describe, it */
2 |
3 | import assert from 'assert'
4 | import { addAll } from 'rdf-dataset-ext'
5 | import clownface from '../../index.js'
6 | import loadExample from '../support/example.js'
7 | import * as ns from '../support/namespace.js'
8 | import rdf from '../support/factory.js'
9 |
10 | describe('.deleteIn', () => {
11 | it('should be a function', () => {
12 | const cf = clownface({ dataset: rdf.dataset() })
13 |
14 | assert.strictEqual(typeof cf.deleteIn, 'function')
15 | })
16 |
17 | it('should return the called object', async () => {
18 | const dataset = addAll(rdf.dataset(), await loadExample())
19 | const cf = clownface({ dataset })
20 |
21 | assert.strictEqual(cf.deleteIn(), cf)
22 | })
23 |
24 | it('should remove quads based on the object value', async () => {
25 | const dataset = addAll(rdf.dataset(), await loadExample())
26 | const cf = clownface({
27 | dataset,
28 | term: rdf.namedNode('http://localhost:8080/data/person/bernadette-rostenkowski'),
29 | })
30 |
31 | cf.deleteIn()
32 |
33 | assert.strictEqual(dataset.size, 118)
34 | })
35 |
36 | it('should remove quads based on the object value and predicate', async () => {
37 | const dataset = addAll(rdf.dataset(), await loadExample())
38 | const cf = clownface({
39 | dataset,
40 | term: rdf.namedNode('http://localhost:8080/data/person/bernadette-rostenkowski'),
41 | })
42 |
43 | cf.deleteIn(rdf.namedNode('http://schema.org/knows'))
44 |
45 | assert.strictEqual(dataset.size, 119)
46 | })
47 |
48 | it('should remove quads based on the object value and multiple predicates', async () => {
49 | const dataset = addAll(rdf.dataset(), await loadExample())
50 | const cf = clownface({
51 | dataset,
52 | term: rdf.namedNode('http://localhost:8080/data/person/bernadette-rostenkowski'),
53 | })
54 |
55 | cf.deleteIn([
56 | rdf.namedNode('http://schema.org/knows'),
57 | rdf.namedNode('http://schema.org/spouse'),
58 | ])
59 |
60 | assert.strictEqual(dataset.size, 118)
61 | })
62 |
63 | it('should remove quads based on the object value, predicate and subject', async () => {
64 | const dataset = addAll(rdf.dataset(), await loadExample())
65 | const cf = clownface({
66 | dataset,
67 | term: ns.tbbtp('bernadette-rostenkowski'),
68 | })
69 |
70 | cf.deleteIn(ns.schema.knows, ns.tbbtp('amy-farrah-fowler'))
71 |
72 | assert.strictEqual(dataset.size, 125)
73 | })
74 |
75 | it('should remove quads based on the object value, multiple predicates and multiple subjects', async () => {
76 | const dataset = addAll(rdf.dataset(), await loadExample())
77 | const cf = clownface({
78 | dataset,
79 | term: ns.tbbtp('bernadette-rostenkowski'),
80 | })
81 |
82 | cf.deleteIn([ns.schema.knows, ns.schema.spouse], [ns.tbbtp('amy-farrah-fowler'), ns.tbbtp('howard-wolowitz')])
83 |
84 | assert.strictEqual(dataset.size, 123)
85 | })
86 | })
87 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # clownface
2 |
3 | [](https://travis-ci.org/rdf-ext/clownface)
4 | [](https://npm.im/clownface)
5 |
6 | Clownface is a graph traversal library inspired by [Gremlin](http://tinkerpop.apache.org/)
7 | which allows to query any [RDF dataset](https://rdf.js.org/dataset-spec/) in a concise and readable way.
8 |
9 | ## Introduction
10 |
11 | Querying an RDF graph can be challenging. Apart from trivial queries, e.g. simply listing all the
12 | objects connected to a subject, it is usually necessary to write multiple loop and validation
13 | conditions to retrieve an answer. Clownface is there to help.
14 |
15 | ## Getting started
16 |
17 | The package is installed from npm:
18 |
19 | ```
20 | npm i -S clownface
21 | ```
22 |
23 | To start using it, an instance of [RDF/JS `DatasetCore`](https://rdf.js.org/dataset-spec/#datasetcore-interface) must be
24 | provided to the exported factory method
25 |
26 |
27 |
28 | ```js
29 | const rdf = require('@zazuko/env-bundle')
30 |
31 | const firstName = rdf.namedNode('http://xmlns.com/foaf/0.1/firstName')
32 | const lastName = rdf.namedNode('http://xmlns.com/foaf/0.1/lastName')
33 |
34 | // initialize
35 | const ptr = rdf.clownface()
36 |
37 | // add some resources
38 | ptr
39 | .namedNode('http://example.com/person/stuart-bloom')
40 | .addOut(firstName, 'Stuart')
41 | .addOut(lastName, 'Bloom')
42 | .namedNode('http://example.com/person/penny')
43 | .addOut(firstName, 'Penny')
44 |
45 | // and now retrieve the first names of those who have a last name
46 | ptr
47 | .has(lastName)
48 | .out(firstName)
49 | .values
50 | ```
51 |
52 |
53 |
54 | ## Details
55 |
56 | At the core of clownface sits an object called a "graph pointer". A graph pointer can be backed by zero or more RDF/JS terms. It is being created by calling the function exported by the packages's default module.
57 |
58 | The current context node or nodes can be accessed by `term`/`value` and `terms`/`values` pairs of properties. The first pair return only a single RDF/JS and its string value. They will return `undefined` if the context points at 0 or >1 nodes. The second pair of properties always return an array of the context terms and their string values.
59 |
60 | Graph pointers provides a set of chainable methods. The most important ones are `.in(predicate)` and `.out(predicate)` which allow the traversal through the graph. It is possible to chain as many of these methods to extract a sub-graph from the available dataset.
61 |
62 | ## More examples
63 |
64 | Check out the [deep dive](deep-dive.md) and other pages for a running examples which show how to use the graph pointer to traverse and manipulate RDF graphs.
65 |
66 | We use the well known `` `` `