├── .nvmrc ├── .npmrc ├── docs ├── sdk ├── demo └── index.html ├── packages ├── sdk │ ├── .gitignore │ ├── src │ │ ├── index.ts │ │ ├── test │ │ │ ├── __fixtures__ │ │ │ │ ├── index.ts │ │ │ │ ├── from_root_examples.ts │ │ │ │ └── small_quadstore.ts │ │ │ ├── utils.ts │ │ │ ├── functions.test.ts │ │ │ ├── types.test.ts │ │ │ └── util.test.ts │ │ ├── functions.ts │ │ ├── debuggable.ts │ │ └── util.ts │ ├── .prettierrc.cjs │ ├── tsconfig.json │ ├── .eslintrc.cjs │ ├── package.json │ ├── CONTRIBUTING.md │ └── README.md ├── http-client │ ├── README.md │ ├── src │ │ ├── index.ts │ │ ├── types.ts │ │ ├── client.ts │ │ └── contract.ts │ ├── .prettierrc.cjs │ ├── tsconfig.json │ ├── .eslintrc.cjs │ └── package.json ├── etl │ ├── src │ │ ├── plugins │ │ │ └── crossref │ │ │ │ ├── index.ts │ │ │ │ ├── functions.ts │ │ │ │ └── api.ts │ │ ├── main.ts │ │ ├── index.ts │ │ ├── types.ts │ │ └── utils │ │ │ └── index.ts │ ├── test │ │ └── unit │ │ │ ├── __fixtures__ │ │ │ ├── index.ts │ │ │ └── abstract.ts │ │ │ ├── utils.ts │ │ │ ├── command.test.ts │ │ │ ├── crossref │ │ │ └── functions.test.ts │ │ │ └── processor.test.ts │ ├── .prettierrc.cjs │ ├── tsconfig.json │ ├── .eslintrc.cjs │ ├── package.json │ ├── README.md │ └── CONTRIBUTING.md ├── http-server │ ├── src │ │ ├── api_version.ts │ │ ├── adapter │ │ │ ├── index.ts │ │ │ ├── oxigraph_inmem.ts │ │ │ └── sparql_fetch.ts │ │ ├── httpserver │ │ │ ├── main.ts │ │ │ ├── index.ts │ │ │ ├── command.ts │ │ │ └── server.ts │ │ ├── sparql.ts │ │ ├── index.ts │ │ ├── utils.ts │ │ ├── rdf.ts │ │ ├── types.ts │ │ └── api.ts │ ├── .prettierrc.cjs │ ├── test │ │ ├── integration │ │ │ ├── assets │ │ │ │ └── docker-compose.yml │ │ │ └── httpserver.test.ts │ │ ├── utils.ts │ │ └── unit │ │ │ ├── utils.test.ts │ │ │ ├── api.test.ts │ │ │ └── sparql.test.ts │ ├── tsconfig.json │ ├── .eslintrc.cjs │ ├── docker-compose.local.yml │ ├── package.json │ └── Dockerfile ├── widget │ ├── src │ │ ├── vite-env.d.ts │ │ ├── index.ts │ │ ├── assets │ │ │ ├── index.ts │ │ │ ├── close-details-button.ts │ │ │ ├── logo-large.ts │ │ │ ├── logo-small.ts │ │ │ └── copy-to-clipboard.ts │ │ ├── font.ts │ │ └── detail-navigation-header.ts │ ├── images-for-readme │ │ ├── graph-view.jpeg │ │ └── detail-view.jpeg │ ├── .prettierrc.cjs │ ├── vite.config.ts │ ├── .gitignore │ ├── .eslintrc.cjs │ ├── tsconfig.json │ ├── test │ │ ├── fixtures │ │ │ ├── docmapWithNoMetadata.ts │ │ │ ├── docmapWithOneStep.ts │ │ │ └── anotherDocmapWithOneStep.ts │ │ └── integration │ │ │ └── util.ts │ ├── public │ │ └── vite.svg │ ├── package.json │ └── playwright.config.ts ├── spa │ ├── .gitignore │ ├── public │ │ ├── favicon.png │ │ ├── index.html │ │ └── global.css │ ├── screenshots │ │ ├── preprint_posted.pdf │ │ ├── preprint_posted.png │ │ ├── preprint_published.pdf │ │ ├── preprint_published.png │ │ ├── preprint_refereed.pdf │ │ ├── preprint_refereed.png │ │ ├── preprint_published_reviewed.png │ │ └── preprint_reviewed_published_reviewed.pdf │ ├── src │ │ ├── main.js │ │ ├── utils.js │ │ ├── CrossrefDemo.svelte │ │ ├── JsonBox.svelte │ │ └── Widget.svelte │ ├── package.json │ ├── rollup.config.js │ └── README.md ├── example │ ├── tsconfig.json │ ├── package.json │ ├── README.md │ ├── src │ │ └── index.ts │ └── docmap.jsonld └── build-configs │ ├── tsconfig.json │ ├── README.md │ └── package.json ├── .releaserc ├── .dockerignore ├── .gitattributes ├── renovate.json ├── scripts ├── link-docs.sh ├── package-tests.js ├── validate-shacl.js ├── jsonld-to-rdf.js └── upload_to_local_deployment.ts ├── pnpm-workspace.yaml ├── CODE_OF_CONDUCT.md ├── .github ├── ISSUE_TEMPLATE │ ├── documentation.md │ ├── bug_report.md │ ├── feature_request.md │ └── protocol.md ├── pull_request_template.md └── workflows │ ├── specification-tests.yaml │ ├── spa-tests.yaml │ ├── etl-tests.yaml │ ├── example-tests.yaml │ ├── sdk-tests.yaml │ ├── widget-tests.yaml │ ├── release.yaml │ ├── gh-pages.yaml │ └── http-server-tests.yaml ├── examples ├── STALE.md └── docmaps-example-epmc-01.jsonld ├── LICENSE ├── package.json └── .gitignore /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.10 -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true -------------------------------------------------------------------------------- /docs/sdk: -------------------------------------------------------------------------------- 1 | ../packages/sdk/docs -------------------------------------------------------------------------------- /docs/demo: -------------------------------------------------------------------------------- 1 | ../packages/spa/public/ -------------------------------------------------------------------------------- /packages/sdk/.gitignore: -------------------------------------------------------------------------------- 1 | docs/ 2 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | branches: 2 | - main 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/tmp 3 | -------------------------------------------------------------------------------- /packages/http-client/README.md: -------------------------------------------------------------------------------- 1 | # Docmaps API http client -------------------------------------------------------------------------------- /packages/etl/src/plugins/crossref/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api' 2 | -------------------------------------------------------------------------------- /packages/http-server/src/api_version.ts: -------------------------------------------------------------------------------- 1 | export const API_VERSION = '0.1.0' 2 | -------------------------------------------------------------------------------- /packages/widget/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/spa/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /public/build/ 3 | 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /packages/sdk/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types' 2 | export * from './typed_graph' 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /**/docs/**/* linguist-documentation 2 | /**/docs/**/*.html linguist-generated 3 | -------------------------------------------------------------------------------- /packages/etl/test/unit/__fixtures__/index.ts: -------------------------------------------------------------------------------- 1 | export * from './crossref' 2 | export * from './abstract' 3 | -------------------------------------------------------------------------------- /packages/http-server/src/adapter/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sparql_adapter' 2 | export * from './sparql_fetch' 3 | -------------------------------------------------------------------------------- /packages/etl/src/main.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env npx tsx 2 | 3 | import cli from './command' 4 | 5 | await cli.parseAsync() 6 | -------------------------------------------------------------------------------- /packages/http-client/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './client' 2 | export * from './contract' 3 | export * from './types' 4 | -------------------------------------------------------------------------------- /packages/http-server/src/httpserver/main.ts: -------------------------------------------------------------------------------- 1 | import { MakeCli } from './command' 2 | 3 | await MakeCli().parseAsync() 4 | -------------------------------------------------------------------------------- /packages/sdk/src/test/__fixtures__/index.ts: -------------------------------------------------------------------------------- 1 | export * from './small_quadstore' 2 | export * from './from_root_examples' 3 | -------------------------------------------------------------------------------- /packages/spa/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Docmaps-Project/docmaps/HEAD/packages/spa/public/favicon.png -------------------------------------------------------------------------------- /packages/spa/screenshots/preprint_posted.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Docmaps-Project/docmaps/HEAD/packages/spa/screenshots/preprint_posted.pdf -------------------------------------------------------------------------------- /packages/spa/screenshots/preprint_posted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Docmaps-Project/docmaps/HEAD/packages/spa/screenshots/preprint_posted.png -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /packages/spa/screenshots/preprint_published.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Docmaps-Project/docmaps/HEAD/packages/spa/screenshots/preprint_published.pdf -------------------------------------------------------------------------------- /packages/spa/screenshots/preprint_published.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Docmaps-Project/docmaps/HEAD/packages/spa/screenshots/preprint_published.png -------------------------------------------------------------------------------- /packages/spa/screenshots/preprint_refereed.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Docmaps-Project/docmaps/HEAD/packages/spa/screenshots/preprint_refereed.pdf -------------------------------------------------------------------------------- /packages/spa/screenshots/preprint_refereed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Docmaps-Project/docmaps/HEAD/packages/spa/screenshots/preprint_refereed.png -------------------------------------------------------------------------------- /packages/widget/images-for-readme/graph-view.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Docmaps-Project/docmaps/HEAD/packages/widget/images-for-readme/graph-view.jpeg -------------------------------------------------------------------------------- /packages/widget/images-for-readme/detail-view.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Docmaps-Project/docmaps/HEAD/packages/widget/images-for-readme/detail-view.jpeg -------------------------------------------------------------------------------- /packages/spa/screenshots/preprint_published_reviewed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Docmaps-Project/docmaps/HEAD/packages/spa/screenshots/preprint_published_reviewed.png -------------------------------------------------------------------------------- /packages/etl/src/index.ts: -------------------------------------------------------------------------------- 1 | import cli from './command' 2 | export default cli 3 | 4 | export * from './command' 5 | export * from './plugins/crossref' 6 | export * from './types' 7 | -------------------------------------------------------------------------------- /packages/http-server/src/sparql.ts: -------------------------------------------------------------------------------- 1 | import { SELECT } from '@tpluscode/sparql-builder' 2 | 3 | export const COUNT_TOTAL_TRIPLES_QUERY = SELECT`(count(*) as ?n)`.WHERE`?s ?p ?o .` 4 | -------------------------------------------------------------------------------- /packages/http-server/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api' 2 | export * from './types' 3 | export * from './adapter' 4 | export * from './httpserver' 5 | export * from './api_version' 6 | -------------------------------------------------------------------------------- /packages/spa/src/main.js: -------------------------------------------------------------------------------- 1 | import App from './App.svelte'; 2 | 3 | const app = new App({ 4 | target: document.body, 5 | props: { 6 | } 7 | }); 8 | 9 | export default app; 10 | -------------------------------------------------------------------------------- /packages/spa/screenshots/preprint_reviewed_published_reviewed.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Docmaps-Project/docmaps/HEAD/packages/spa/screenshots/preprint_reviewed_published_reviewed.pdf -------------------------------------------------------------------------------- /packages/http-server/src/utils.ts: -------------------------------------------------------------------------------- 1 | // utility function 2 | export async function* arrayToAsyncIterable(arr: T[]): AsyncIterable { 3 | for (const t of arr) { 4 | yield t 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/etl/.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | trailingComma: 'all', 4 | singleQuote: true, 5 | quoteProps: 'as-needed', 6 | printWidth: 100, 7 | tabWidth: 2, 8 | } 9 | -------------------------------------------------------------------------------- /packages/sdk/.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | trailingComma: 'all', 4 | singleQuote: true, 5 | quoteProps: 'as-needed', 6 | printWidth: 100, 7 | tabWidth: 2, 8 | } 9 | -------------------------------------------------------------------------------- /packages/widget/.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: 'all', 4 | singleQuote: true, 5 | quoteProps: 'as-needed', 6 | printWidth: 100, 7 | tabWidth: 2, 8 | } 9 | -------------------------------------------------------------------------------- /packages/widget/src/index.ts: -------------------------------------------------------------------------------- 1 | // All files which will be accessible when the widget is installed via npm are declared here 2 | export * from './docmaps-widget'; 3 | export * from './docmap-controller'; 4 | -------------------------------------------------------------------------------- /packages/http-client/.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | trailingComma: 'all', 4 | singleQuote: true, 5 | quoteProps: 'as-needed', 6 | printWidth: 100, 7 | tabWidth: 2, 8 | } 9 | -------------------------------------------------------------------------------- /packages/http-server/.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | trailingComma: 'all', 4 | singleQuote: true, 5 | quoteProps: 'as-needed', 6 | printWidth: 100, 7 | tabWidth: 2, 8 | } 9 | -------------------------------------------------------------------------------- /packages/http-server/test/integration/assets/docker-compose.yml: -------------------------------------------------------------------------------- 1 | name: docmaps-integration-tests 2 | services: 3 | oxigraph: 4 | image: ghcr.io/oxigraph/oxigraph 5 | ports: 6 | - "33078:7878" 7 | -------------------------------------------------------------------------------- /scripts/link-docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eu 4 | 5 | find . -type l -exec bash -c ' 6 | export SRC=$(readlink -f "$0"); 7 | 8 | rm "$0"; 9 | mkdir "$0"; 10 | 11 | cp -avfPR "$SRC/." "$0"; 12 | ' {} \; 13 | 14 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/build-configs' 3 | - 'packages/etl' 4 | - 'packages/example' 5 | - 'packages/http-client' 6 | - 'packages/http-server' 7 | - 'packages/sdk' 8 | - 'packages/spa' 9 | - 'packages/widget' 10 | -------------------------------------------------------------------------------- /packages/widget/src/assets/index.ts: -------------------------------------------------------------------------------- 1 | // All files which will be accessible when the widget is installed via npm are declared here 2 | export * from './copy-to-clipboard'; 3 | export * from './close-details-button'; 4 | export * from './logo-small'; 5 | export * from './logo-large'; 6 | -------------------------------------------------------------------------------- /packages/http-client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@docmaps/build-configs/tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "include": [ 7 | "src/**/*" 8 | ], 9 | "exclude": [ 10 | "dist", 11 | "node_modules" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/http-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@docmaps/build-configs/tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "include": [ 7 | "src/**/*" 8 | ], 9 | "exclude": [ 10 | "dist", 11 | "node_modules" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/etl/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@docmaps/build-configs/tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "include": [ 7 | "src/**/*" 8 | ], 9 | "exclude": [ 10 | "dist", 11 | "**/test/*", 12 | "node_modules" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /packages/example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@docmaps/build-configs/tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "include": [ 7 | "src/**/*" 8 | ], 9 | "exclude": [ 10 | "dist", 11 | "**/test/*", 12 | "node_modules" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /packages/sdk/src/test/utils.ts: -------------------------------------------------------------------------------- 1 | import { isRight, Either } from 'fp-ts/lib/Either' 2 | 3 | export function rightAnd(e: Either, validation: (res: T) => void) { 4 | if (isRight(e)) { 5 | validation(e.right) 6 | return true 7 | } else { 8 | return false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/http-server/test/utils.ts: -------------------------------------------------------------------------------- 1 | import pino, { Logger } from 'pino' 2 | 3 | export function testLoggerWithPino(log: (s: string) => void): Logger { 4 | return pino( 5 | // options 6 | { 7 | level: 'trace', 8 | }, 9 | // a pino.Destination 10 | { write: log }, 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /packages/spa/src/utils.js: -------------------------------------------------------------------------------- 1 | import { ItemCmd, CreateCrossrefClient } from '@docmaps/etl' 2 | import util from 'util' 3 | import {isLeft} from 'fp-ts/lib/Either' 4 | 5 | 6 | export function structureError(error) { 7 | error['type'] = 'error' 8 | return JSON.stringify(error, ['message', 'cause', 'type']) 9 | } 10 | -------------------------------------------------------------------------------- /packages/sdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@docmaps/build-configs/tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "exclude": [ 7 | "docs/**", 8 | "src/test/**/*", 9 | "dist", 10 | "node_modules", 11 | "src/debuggable.ts", 12 | "src/test/__fixtures__/*" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg)](https://github.com/knowledgefutures/general/blob/master/CODE_OF_CONDUCT.md) 2 | 3 | This project is governed by the [Knowledge Futures, Inc Organizational Code of Conduct](https://github.com/knowledgefutures/general/blob/master/CODE_OF_CONDUCT.md). -------------------------------------------------------------------------------- /packages/http-server/test/unit/utils.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import * as util from '../../src/utils' 3 | 4 | test('arrayToAsyncIterable', async (t) => { 5 | const array = [1, 20, 300, 4] 6 | const iter = util.arrayToAsyncIterable(array) 7 | 8 | let i = 0 9 | 10 | for await (const v of iter) { 11 | t.is(array[i], v) 12 | i++ 13 | } 14 | }) 15 | -------------------------------------------------------------------------------- /packages/http-client/src/types.ts: -------------------------------------------------------------------------------- 1 | // TODO: use io-ts? this is only decodable by consumers... 2 | export type ApiInfo = { 3 | api_url: string 4 | api_version: string 5 | ephemeral_document_expiry: { 6 | max_seconds: number 7 | max_retrievals: number 8 | } 9 | peers: { 10 | api_url: string 11 | }[] 12 | } 13 | 14 | export type ErrorBody = { message?: string; data?: object } 15 | -------------------------------------------------------------------------------- /packages/build-configs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@tsconfig/strictest/tsconfig.json", "@tsconfig/node-lts/tsconfig.json"], 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "target": "ES2020", 6 | "esModuleInterop": true, 7 | "moduleResolution": "bundler", 8 | "sourceMap": true, 9 | "removeComments": true, 10 | "noUnusedLocals": false, 11 | "declaration": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/http-client/src/client.ts: -------------------------------------------------------------------------------- 1 | import { contract } from './contract' 2 | import { initClient, InitClientArgs } from '@ts-rest/core' 3 | 4 | export function MakeHttpClient(opts: InitClientArgs) { 5 | return initClient(contract, opts) 6 | } 7 | 8 | const _localClient = MakeHttpClient({ 9 | baseUrl: 'http://localhost:3000', 10 | baseHeaders: {}, 11 | }) 12 | 13 | export type DocmapsApiClientT = typeof _localClient 14 | -------------------------------------------------------------------------------- /packages/sdk/src/functions.ts: -------------------------------------------------------------------------------- 1 | import type { StepT, ThingT } from './types' 2 | 3 | type hasInputs = { inputs: ThingT[] } 4 | 5 | export function Migrate__Step0_14_to_15(s: StepT): StepT { 6 | const inputs: ThingT[] = (s as hasInputs).inputs || [] 7 | 8 | const result: StepT = { 9 | ...s, 10 | actions: s.actions.map((a) => ({ 11 | ...a, 12 | inputs: inputs, 13 | })), 14 | } 15 | return result 16 | } 17 | -------------------------------------------------------------------------------- /packages/spa/src/CrossrefDemo.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | {#key key} 13 |
14 |

Docmap derived from CrossRef

15 | 16 |
17 | {/key} -------------------------------------------------------------------------------- /packages/build-configs/README.md: -------------------------------------------------------------------------------- 1 | # docmaps project build configs 2 | 3 | This is an unpublished NPM module consumed by `pnpm` workspace references. 4 | All the modules in this monorepo depend on this module for their 5 | shared typescript configurations, including tsconfig, lint configs, etc. 6 | 7 | ## Usage 8 | 9 | This module can be installed into a new monorepo package by: 10 | 11 | ``` 12 | pnpm install @docmaps/build-configs@workspace:0.0.0 13 | ``` 14 | -------------------------------------------------------------------------------- /packages/http-server/src/rdf.ts: -------------------------------------------------------------------------------- 1 | import namespace from '@rdfjs/namespace' 2 | 3 | export const dcterms = namespace('http://purl.org/dc/terms/') 4 | export const pwo = namespace('http://purl.org/spar/pwo/') 5 | export const fabio = namespace('http://purl.org/spar/fabio/') 6 | export const pso = namespace('http://purl.org/spar/pso/') 7 | export const pro = namespace('http://purl.org/spar/pro/') 8 | export const tx = namespace('http://www.ontologydesignpatterns.org/cp/owl/taskexecution.owl#') 9 | -------------------------------------------------------------------------------- /packages/widget/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, UserConfig } from 'vite' 2 | import dts from 'vite-plugin-dts' 3 | 4 | export const widgetConfig: UserConfig = { 5 | build: { 6 | lib: { 7 | entry: 'src/index.ts', 8 | name: 'DocmapsWidget', 9 | // the proper extensions will be added 10 | formats: ['es'], 11 | fileName: 'index', 12 | }, 13 | rollupOptions: {}, 14 | }, 15 | plugins: [dts()], 16 | } 17 | 18 | export default defineConfig(widgetConfig) 19 | -------------------------------------------------------------------------------- /packages/spa/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Docmaps Explorer 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /packages/sdk/src/debuggable.ts: -------------------------------------------------------------------------------- 1 | import * as pkg from '.' 2 | import * as fixtures from './test/__fixtures__' 3 | 4 | /** Debug entrypoint script 5 | * 6 | * This file can be used with the npm debugger to dive in to 7 | * any issues we may face due to the algorithm complexity of 8 | * jsonld. 9 | */ 10 | 11 | /* eslint-disable-next-line no-debugger */ 12 | debugger 13 | 14 | const t = new pkg.TypedGraph() 15 | 16 | const parsed = await t.pickStream(fixtures.FromRootExamples.elife_01_nt, pkg.DocmapNormalizedFrame) 17 | console.log(parsed) 18 | -------------------------------------------------------------------------------- /packages/widget/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | /test-results/ 26 | /playwright-report/ 27 | /blob-report/ 28 | /playwright/.cache/ 29 | /test-results/ 30 | /playwright-report/ 31 | /blob-report/ 32 | /playwright/.cache/ 33 | -------------------------------------------------------------------------------- /packages/http-server/src/httpserver/index.ts: -------------------------------------------------------------------------------- 1 | export * from './server' 2 | 3 | /** HTTPSServer module 4 | * 5 | * The purpose of this module is to handle all network-related 6 | * and operation-related elements of the API Server. 7 | * 8 | * This includes things like an executable program that runs 9 | * the abstract code, mappings to HTTP error codes. logging 10 | * and telemetry. You can avoid using this package if You 11 | * want to manage the lifecycle of the API server in your 12 | * own way, and just use the `ApiInstance` class to handle 13 | * state and logic 14 | */ 15 | -------------------------------------------------------------------------------- /packages/sdk/src/test/functions.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import * as D from '../types' 3 | import { PartialExamples as ex } from './__fixtures__' 4 | import * as E from 'fp-ts/lib/Either' 5 | import { Migrate__Step0_14_to_15 } from '../functions' 6 | 7 | test('Migrating a step to 0.15.0', (t) => { 8 | const v = D.Step.decode(ex.elife.Step[1]) 9 | 10 | if (E.isLeft(v)) { 11 | t.fail('expected to decode a Step') 12 | return 13 | } 14 | 15 | t.is(v.right.inputs?.length, 1) 16 | 17 | const r = Migrate__Step0_14_to_15(v.right) 18 | 19 | t.deepEqual(r.actions[0]?.inputs, v.right.inputs) 20 | }) 21 | -------------------------------------------------------------------------------- /packages/etl/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'eslint:recommended', 4 | 'plugin:@typescript-eslint/recommended', 5 | 'plugin:@typescript-eslint/eslint-recommended', 6 | 'plugin:prettier/recommended', 7 | ], 8 | parser: '@typescript-eslint/parser', 9 | plugins: ['@typescript-eslint', 'prettier'], 10 | root: true, 11 | ignorePatterns: ['dist/'], 12 | rules: { 13 | '@typescript-eslint/no-unused-vars': [ 14 | 'error', 15 | { 16 | varsIgnorePattern: '^_', 17 | argsIgnorePattern: '^_', 18 | caughtErrorsIgnorePattern: '^_', 19 | }, 20 | ], 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /packages/build-configs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@docmaps/build-configs", 3 | "version": "0.0.0", 4 | "repository": "git@github.com:docmaps-project/docmaps.git", 5 | "homepage": "https://github.com/Docmaps-Project/docmaps/tree/main/packages/build-tools", 6 | "description": "typescript build configurations for shared standards across monorepo", 7 | "type": "module", 8 | "scripts": {}, 9 | "keywords": [], 10 | "author": "github.com/ships", 11 | "license": "ISC", 12 | "files": [ 13 | "/" 14 | ], 15 | "devDependencies": { 16 | "@tsconfig/node-lts": "^18.12.5", 17 | "@tsconfig/strictest": "^2.0.2" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/sdk/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'eslint:recommended', 4 | 'plugin:@typescript-eslint/recommended', 5 | 'plugin:@typescript-eslint/eslint-recommended', 6 | 'plugin:prettier/recommended', 7 | ], 8 | parser: '@typescript-eslint/parser', 9 | plugins: ['@typescript-eslint', 'prettier'], 10 | root: true, 11 | ignorePatterns: ['dist/', 'docs/', 'src/test/__fixtures__/'], 12 | rules: { 13 | '@typescript-eslint/no-unused-vars': [ 14 | 'error', 15 | { 16 | varsIgnorePattern: '^_', 17 | argsIgnorePattern: '^_', 18 | caughtErrorsIgnorePattern: '^_', 19 | }, 20 | ], 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /packages/widget/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'eslint:recommended', 4 | 'plugin:@typescript-eslint/recommended', 5 | 'plugin:@typescript-eslint/eslint-recommended', 6 | 'plugin:prettier/recommended', 7 | ], 8 | parser: '@typescript-eslint/parser', 9 | plugins: ['@typescript-eslint', 'prettier'], 10 | root: true, 11 | ignorePatterns: ['dist/', 'docs/', 'src/test/__fixtures__/'], 12 | rules: { 13 | '@typescript-eslint/no-unused-vars': [ 14 | 'error', 15 | { 16 | varsIgnorePattern: '^_', 17 | argsIgnorePattern: '^_', 18 | caughtErrorsIgnorePattern: '^_', 19 | }, 20 | ], 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /packages/spa/src/JsonBox.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 |
23 | 24 | 31 | -------------------------------------------------------------------------------- /packages/http-client/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'eslint:recommended', 4 | 'plugin:@typescript-eslint/recommended', 5 | 'plugin:@typescript-eslint/eslint-recommended', 6 | 'plugin:prettier/recommended', 7 | ], 8 | parser: '@typescript-eslint/parser', 9 | plugins: ['@typescript-eslint', 'prettier'], 10 | root: true, 11 | ignorePatterns: ['dist/'], 12 | rules: { 13 | '@typescript-eslint/no-inferrable-types': 0, 14 | '@typescript-eslint/no-unused-vars': [ 15 | 'error', 16 | { 17 | varsIgnorePattern: '^_', 18 | argsIgnorePattern: '^_', 19 | caughtErrorsIgnorePattern: '^_', 20 | }, 21 | ], 22 | }, 23 | } 24 | -------------------------------------------------------------------------------- /packages/http-server/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'eslint:recommended', 4 | 'plugin:@typescript-eslint/recommended', 5 | 'plugin:@typescript-eslint/eslint-recommended', 6 | 'plugin:prettier/recommended', 7 | ], 8 | parser: '@typescript-eslint/parser', 9 | plugins: ['@typescript-eslint', 'prettier'], 10 | root: true, 11 | ignorePatterns: ['dist/'], 12 | rules: { 13 | '@typescript-eslint/no-inferrable-types': 0, 14 | '@typescript-eslint/no-unused-vars': [ 15 | 'error', 16 | { 17 | varsIgnorePattern: '^_', 18 | argsIgnorePattern: '^_', 19 | caughtErrorsIgnorePattern: '^_', 20 | }, 21 | ], 22 | }, 23 | } 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: DocumentationRequest 3 | about: Use this template for requesting new documentation 4 | title: "[DOCS]: [DESCRIPTION]" 5 | --- 6 | 7 | # Documentation Request 8 | 9 | Packages related to documentation request: 10 | 11 | - [ ] sdk 12 | - [ ] etl 13 | - [ ] widget 14 | - [ ] http-client 15 | - [ ] http-server 16 | - [ ] spa (explorer) 17 | - [ ] OWL/SHACL definitions 18 | 19 | ### Description 20 | 21 | Describe the documentation you would like to see added or improved. 22 | 23 | ### Additional information 24 | 25 | Provide any additional information that might be helpful in understanding the documentation issue. 26 | Library Feature Request 27 | csharp 28 | Copy code 29 | ### Description 30 | -------------------------------------------------------------------------------- /packages/http-server/src/types.ts: -------------------------------------------------------------------------------- 1 | import * as D from '@docmaps/sdk' 2 | import * as TE from 'fp-ts/TaskEither' 3 | 4 | // TODO: use io-ts? this is only decodable by consumers... 5 | export type ApiInfo = { 6 | api_url: string 7 | api_version: string 8 | ephemeral_document_expiry: { 9 | max_seconds: number 10 | max_retrievals: number 11 | } 12 | peers: { 13 | api_url: string 14 | }[] 15 | } 16 | 17 | export type ThingSpec = { 18 | identifier: string 19 | kind: 'iri' | 'doi' 20 | } 21 | 22 | // TODO is this the same as the Client? 23 | export interface BackendAdapter { 24 | docmapWithIri(iri: string): TE.TaskEither 25 | docmapForThing(thing: ThingSpec): TE.TaskEither 26 | } 27 | -------------------------------------------------------------------------------- /packages/widget/src/assets/close-details-button.ts: -------------------------------------------------------------------------------- 1 | import { svg } from 'lit'; 2 | 3 | export const closeDetailsButton = (color: string) => svg` 4 | 5 | 6 | 7 | 8 | `; 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Use this template for reporting a bug. 4 | title: "[PKG]: [BUG DESCRIPTION]" 5 | --- 6 | 7 | # Bug Report 8 | 9 | Packages affected: 10 | 11 | - [ ] sdk 12 | - [ ] etl 13 | - [ ] widget 14 | - [ ] http-client 15 | - [ ] http-server 16 | - [ ] spa (explorer) 17 | - [ ] OWL/SHACL definitions 18 | 19 | ### Expected behavior 20 | 21 | Describe what you expected to happen. 22 | 23 | ### Actual behavior 24 | 25 | Describe what actually happened. 26 | 27 | ### Steps to reproduce 28 | 29 | List the steps to reproduce the issue. Be as specific as possible. 30 | 31 | ### Additional information 32 | 33 | Provide any additional information that might be helpful in understanding the issue, such as error messages, stack traces, or screenshots. 34 | -------------------------------------------------------------------------------- /packages/widget/src/font.ts: -------------------------------------------------------------------------------- 1 | export function loadFont() { 2 | // Load IBM Plex Mono font 3 | // It would be nice to do this in styles.ts, but `@import` is not supported there. 4 | addLinkToDocumentHeader('preconnect', 'https://fonts.googleapis.com'); 5 | addLinkToDocumentHeader('preconnect', 'https://fonts.gstatic.com', 'anonymous'); 6 | addLinkToDocumentHeader( 7 | 'stylesheet', 8 | 'https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,300;0,400;0,500;0,600;1,300&display=swap', 9 | ); 10 | } 11 | 12 | function addLinkToDocumentHeader(rel: string, href: string, crossorigin?: string) { 13 | const link = document.createElement('link'); 14 | link.rel = rel; 15 | link.href = href; 16 | if (crossorigin) { 17 | link.crossOrigin = crossorigin; 18 | } 19 | document.head.appendChild(link); 20 | } 21 | -------------------------------------------------------------------------------- /packages/example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@docmaps/example", 3 | "version": "1.0.0", 4 | "description": "example of using fp-ts to parse and handle a docmaps structure", 5 | "main": "src/index.ts", 6 | "type": "module", 7 | "scripts": { 8 | "test": "npx tsx src/index.ts", 9 | "build": "tsc", 10 | "build:deps": "pnpm run --filter=@docmaps/example^... build", 11 | "lint": "echo WARN: no linting specified for this package" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "@docmaps/sdk": "workspace:^0.0.0", 18 | "fp-ts": "^2.16.0", 19 | "io-ts": "^2.2.20", 20 | "tsx": "^4.0.0" 21 | }, 22 | "devDependencies": { 23 | "@docmaps/build-configs": "workspace:^", 24 | "@types/node": "^20.0.0", 25 | "typescript": "^5.2.2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Briefly describe the changes you've made in this pull request. 4 | 5 | ### Related Issues 6 | 7 | List any issues that are related to this pull request, such as bug reports or feature requests. 8 | 9 | ### Checklist 10 | 11 | - [ ] I have tested these changes locally and they work as expected. 12 | - [ ] I have added or updated tests to cover any new functionality or bug fixes. 13 | - [ ] I have updated the documentation to reflect any changes or additions to the project. 14 | - [ ] I have followed the [project's code of conduct](/CODE_OF_CONDUCT.md) and conventions for commit messages. 15 | 16 | ### Additional Information 17 | 18 | Provide any additional information that might be helpful in understanding this pull request, such as screenshots, links to relevant research, or other context. 19 | -------------------------------------------------------------------------------- /packages/widget/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "experimentalDecorators": true, 5 | "useDefineForClassFields": false, 6 | "module": "ESNext", 7 | "lib": [ 8 | "ES2020", 9 | "DOM", 10 | "DOM.Iterable" 11 | ], 12 | "skipLibCheck": true, 13 | "outDir": "./dist", 14 | "sourceMap": true, 15 | 16 | /* Bundler mode */ 17 | "moduleResolution": "node", 18 | "allowImportingTsExtensions": true, 19 | "resolveJsonModule": true, 20 | "isolatedModules": true, 21 | "noEmit": true, 22 | 23 | /* Linting */ 24 | "strict": true, 25 | "noUnusedLocals": false, 26 | "noUnusedParameters": false, 27 | "noFallthroughCasesInSwitch": true 28 | }, 29 | "include": [ 30 | "src" 31 | ], 32 | "exclude": [ 33 | "node_modules", 34 | "dist" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Use this template for requesting new features to be implemented 4 | title: "[PKG]: [FEATURE NAME]" 5 | --- 6 | 7 | # Feature Request 8 | 9 | Packages to improve: 10 | 11 | - [ ] sdk 12 | - [ ] etl 13 | - [ ] widget 14 | - [ ] http-client 15 | - [ ] http-server 16 | - [ ] spa (explorer) 17 | - [ ] OWL/SHACL definitions 18 | 19 | ### Description 20 | 21 | Describe the new feature you would like to see added to the library. 22 | 23 | ### Use case 24 | 25 | Describe how you plan to use this feature and why it is important to you. 26 | 27 | ### Proposed solution 28 | 29 | If you have an idea for how this feature could be implemented, please describe it here. 30 | 31 | ### Additional information 32 | 33 | Provide any additional information that might be helpful in understanding the feature request. 34 | 35 | -------------------------------------------------------------------------------- /examples/STALE.md: -------------------------------------------------------------------------------- 1 | # Notes regarding stale examples in the wild 2 | 3 | This directory contains examples of docmaps that we expect to work with the published 4 | tools and libraries and protocol semantics. Sometimes, when we deprecate certain 5 | semantics, the old examples may become stale. When that happens, I will mark them 6 | with a `STALE_` prefix and note them here. 7 | 8 | 9 | `STALE_docmaps-example-embo-01`: 10 | this became stale with the disallowing of the arbitrary key `author-response` as a 11 | `type` of an `output`. Note that an equivalent docmap with the correct `reply` key 12 | is included alongside the deprecating changes. 13 | 14 | additionally, this docmap generated by old EMBO code includes `next-step` key but not 15 | `previous-step`. This causes challenges in serializing triples to JSON-LD according 16 | to `jsonld-serializer-ext`, for reasons we are still investigating. The appropriate key 17 | has been added to the alternate embo docmap. 18 | -------------------------------------------------------------------------------- /packages/etl/src/types.ts: -------------------------------------------------------------------------------- 1 | import type * as E from 'fp-ts/Either' 2 | import type D from '@docmaps/sdk' 3 | import type * as TE from 'fp-ts/lib/TaskEither' 4 | 5 | export type ErrorOrDocmap = E.Either 6 | 7 | // This type is needed because the recursion may produce steps in 8 | // order with review step last, but the preprint step must be 9 | // known to the recursing caller. This is awkward but workable 10 | // for now without going all the way to a graphy representation 11 | // in this procedure. 12 | export type RecursiveStepDataChain = { 13 | head: D.StepT 14 | all: D.StepT[] 15 | visitedIds: Set 16 | } 17 | 18 | export type InductiveStepResult = { 19 | step: D.StepT 20 | preprints: ID[] 21 | manuscripts: ID[] 22 | reviews: ID[] 23 | } 24 | 25 | export type Plugin = { 26 | stepForId: (id: ID) => TE.TaskEither> 27 | actionForReviewId: (id: ID) => TE.TaskEither 28 | } 29 | -------------------------------------------------------------------------------- /packages/example/README.md: -------------------------------------------------------------------------------- 1 | # Example: Using `@docmaps/sdk` and `fp-ts` to Parse and Validate Docmaps 2 | 3 | Welcome to `example`. This project demonstrates how to parse and validate docmaps using `@docmaps/sdk` and `fp-ts`. 4 | 5 | The demonstration aims to provide a starting point and guide for developers who want to use these libraries to manage docmaps effectively in their applications. 6 | Mainly it exists so you can inspect the code and replicate the pattern. See comments inline 7 | for details. 8 | 9 | ## Prerequisites 10 | 11 | 1. npm - If not already installed, follow the instructions from the official [Node.js website](https://nodejs.org/en/) 12 | 13 | 2. Familiarity with [TypeScript](https://www.typescriptlang.org/) 14 | 15 | 3. [Recommended] Familiarity with functional programming 16 | 17 | ## Usage 18 | 19 | Please refer to the `@docmaps/sdk` and `fp-ts` documentation for more detailed information on usage and functionality. 20 | 21 | ## License 22 | 23 | [MIT](https://choosealicense.com/licenses/mit/) 24 | -------------------------------------------------------------------------------- /packages/etl/test/unit/utils.ts: -------------------------------------------------------------------------------- 1 | import * as TE from 'fp-ts/lib/TaskEither' 2 | import { when, deepEqual } from 'ts-mockito' 3 | 4 | export function testCrossrefDate() { 5 | const now = new Date(0) 6 | return { 7 | 'date-parts': [[now.getUTCFullYear(), now.getUTCMonth() + 1, now.getUTCDate()]], 8 | 'date-time': now.toISOString(), 9 | timestamp: now.valueOf(), 10 | } 11 | } 12 | 13 | export function whenThenResolve(functor: (input: T) => Promise, filter: T, response: V) { 14 | when(functor(deepEqual(filter))).thenResolve(response) 15 | } 16 | 17 | export function whenThenRight( 18 | functor: (input1: T) => TE.TaskEither, 19 | filter: T, 20 | response: V, 21 | ) { 22 | when(functor(deepEqual(filter))).thenReturn(TE.of(response)) 23 | } 24 | 25 | export function whenThenRight2( 26 | functor: (input1: T, input2: U) => TE.TaskEither, 27 | filter: [T, U], 28 | response: V, 29 | ) { 30 | when(functor(deepEqual(filter[0]), deepEqual(filter[1]))).thenReturn(TE.of(response)) 31 | } 32 | -------------------------------------------------------------------------------- /packages/http-client/src/contract.ts: -------------------------------------------------------------------------------- 1 | import { initContract } from '@ts-rest/core' 2 | import { DocmapT } from '@docmaps/sdk' 3 | import { ApiInfo, ErrorBody } from './types' 4 | 5 | const c = initContract() 6 | 7 | export const contract = c.router({ 8 | getInfo: { 9 | method: 'GET', 10 | path: '/info', 11 | responses: { 12 | 200: c.type(), 13 | }, 14 | summary: 'Get information about this Docmaps API server', 15 | }, 16 | 17 | getDocmapById: { 18 | method: 'GET', 19 | path: '/docmap/:id', 20 | // TODO: id as arg? 21 | responses: { 22 | 200: c.type(), 23 | 404: c.type(), 24 | }, 25 | summary: 'Get a docmap matching an IRI exactly', 26 | }, 27 | 28 | getDocmapForDoi: { 29 | method: 'GET', 30 | path: '/docmap_for/doi', 31 | query: c.type<{ subject: string }>(), 32 | // TODO: id as arg? 33 | responses: { 34 | 200: c.type(), 35 | 404: c.type(), 36 | }, 37 | summary: 'Get a docmap that describes a research artifact with this DOI', 38 | }, 39 | }) 40 | -------------------------------------------------------------------------------- /packages/spa/src/Widget.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | {#key key} 16 |
17 |

Docmaps widget

18 | 19 | {#if doi} 20 |

Docmap fetched from the Docmaps staging server.

21 | 22 | {:else if docmap} 23 |

From plaintext docmap

24 | 25 | {/if} 26 |
27 | {/key} 28 | 29 | 37 | -------------------------------------------------------------------------------- /packages/http-server/src/adapter/oxigraph_inmem.ts: -------------------------------------------------------------------------------- 1 | import type { SparqlProcessor } from '.' 2 | import oxigraph from 'oxigraph' 3 | import type { Quad } from '@rdfjs/types' 4 | import type { Construct, Describe } from '@tpluscode/sparql-builder' 5 | import { arrayToAsyncIterable } from '../utils' 6 | import * as TE from 'fp-ts/lib/TaskEither' 7 | 8 | export class OxigraphInmemBackend implements SparqlProcessor { 9 | // inmem usage allows writes directly to memory, separate from 10 | // the query interface, if you have a handle to it. 11 | public store: oxigraph.Store 12 | base: string 13 | 14 | constructor(baseIRI: string) { 15 | this.store = new oxigraph.Store() 16 | this.base = baseIRI 17 | } 18 | 19 | triples(query: Construct | Describe): TE.TaskEither> { 20 | const qstr = query.build({ base: this.base }) // oxigraph is not type-safe in its return here, but it guarantees Array of Quad in these two cases 21 | // console.log(qstr) 22 | const filteredStore = this.store.query(qstr) as Quad[] 23 | return TE.of(arrayToAsyncIterable(filteredStore)) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/protocol.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Protocol semantics 3 | about: Use this template for requesting changes to the Docmaps protocol itself. 4 | title: "[PROTOCOL]: [FEATURE NAME]" 5 | --- 6 | 7 | # Protocol semantics improvement 8 | 9 | ### Description 10 | 11 | Describe the semantics issue you have identified and how you think it should be improved. This could include changes to the underlying protocol, data models, or other technical aspects of the project that are beyond the scope of individual implementations/libraries. 12 | 13 | ### Use case 14 | 15 | Describe how this improvement would benefit the project or users of the project. What problems would it solve or opportunities would it create? 16 | 17 | ### Proposed solution 18 | 19 | If you have an idea for how this issue could be addressed, please describe it here. Be as specific as possible about what changes you are proposing and how they would work. 20 | 21 | ### Additional information 22 | 23 | Provide any additional information that might be helpful in understanding the semantics improvement issue, such as relevant research or examples from other projects. 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Knowledge Futures, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docmaps-specification", 3 | "type": "module", 4 | "version": "0.1.0", 5 | "description": "Top-level Docmaps specification module -- not intended for direct npm consumption.", 6 | "scripts": { 7 | "clean": "manypkg exec $npm_execpath run clean && rm -rf node_modules", 8 | "test": "node scripts/package-tests.js", 9 | "test:packages": "manypkg exec $npm_execpath test", 10 | "test:all": "$npm_execpath run test && $npm_execpath run test:packages" 11 | }, 12 | "author": "early evening @ships", 13 | "license": "MTI", 14 | "dependencies": { 15 | "@manypkg/cli": "^0.21.0", 16 | "@rdfjs/formats-common": "^3.1.0", 17 | "@rdfjs/parser-jsonld": "^2.1.0", 18 | "@rdfjs/parser-n3": "^2.0.1", 19 | "@rdfjs/serializer-ntriples": "^2.0.0", 20 | "@rdfjs/serializer-turtle": "^1.1.1", 21 | "multi-semantic-release": "^3.0.2", 22 | "rdf-ext": "^2.2.0" 23 | }, 24 | "devDependencies": { 25 | "axios": "^1.6.0", 26 | "jsonld-streaming-parser": "^3.2.1", 27 | "multi-semantic-release": "^3.0.2", 28 | "rdf-validate-shacl": "^0.5.0", 29 | "tsx": "^4.0.0", 30 | "typescript": "^5.0.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/spa/public/global.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | position: relative; 3 | width: 100%; 4 | height: 100%; 5 | } 6 | 7 | body { 8 | color: #333; 9 | margin: 0; 10 | padding: 8px; 11 | box-sizing: border-box; 12 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; 13 | } 14 | 15 | a { 16 | color: rgb(0,100,200); 17 | text-decoration: none; 18 | } 19 | 20 | a:hover { 21 | text-decoration: underline; 22 | } 23 | 24 | a:visited { 25 | color: rgb(0,80,160); 26 | } 27 | 28 | label { 29 | display: block; 30 | } 31 | 32 | input, button, select, textarea { 33 | font-family: inherit; 34 | font-size: inherit; 35 | -webkit-padding: 0.4em 0; 36 | padding: 0.4em; 37 | margin: 0 0 0.5em 0; 38 | box-sizing: border-box; 39 | border: 1px solid #ccc; 40 | border-radius: 2px; 41 | } 42 | 43 | input:disabled { 44 | color: #ccc; 45 | } 46 | 47 | button { 48 | color: #333; 49 | background-color: #f4f4f4; 50 | outline: none; 51 | } 52 | 53 | button:disabled { 54 | color: #999; 55 | } 56 | 57 | button:not(:disabled):active { 58 | background-color: #ddd; 59 | } 60 | 61 | button:focus { 62 | border-color: #666; 63 | } 64 | -------------------------------------------------------------------------------- /packages/widget/test/fixtures/docmapWithNoMetadata.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | "id": "https://sciety.org/docmaps/v1/articles/10.21203/rs.4.rs-1043992/v1/biophysics-colab.docmap.json", 3 | "type": "docmap", 4 | "created": "2022-04-19T11:40:09.289Z", 5 | "publisher": { 6 | "id": "https://biophysics.sciencecolab.org", 7 | "homepage": "https://biophysics.sciencecolab.org/", 8 | "logo": "https://sciety.org/static/groups/biophysics-colab--4bbf0c12-629b-4bb8-91d6-974f4df8efb2.png", 9 | "name": "Biophysics Colab", 10 | "account": { 11 | "id": "https://sciety.org/groups/biophysics-colab", 12 | "service": "https://sciety.org/" 13 | } 14 | }, 15 | "first-step": "_:b1", 16 | "steps": { 17 | "_:b1": { 18 | "inputs": [ 19 | { 20 | "doi": "10.21203/rs.4.rs-1043992/v1", 21 | "url": "https://doi.org/10.21203/rs.4.rs-1043992/v1" 22 | } 23 | ], 24 | "actions": [ 25 | { 26 | "participants": [], 27 | "outputs": [ 28 | { 29 | "type": "review-article" 30 | } 31 | ] 32 | } 33 | ], 34 | "assertions": [] 35 | } 36 | }, 37 | "@context": "https://w3id.org/docmaps/context.jsonld" 38 | } 39 | -------------------------------------------------------------------------------- /packages/http-server/docker-compose.local.yml: -------------------------------------------------------------------------------- 1 | name: docmaps-api-server-local 2 | services: 3 | oxigraph: 4 | image: ghcr.io/oxigraph/oxigraph 5 | ports: 6 | - "33378:7878" 7 | volumes: 8 | - "./tmp/oxigraph_data:/data" 9 | profiles: 10 | - cluster 11 | server: 12 | build: 13 | dockerfile: ./packages/http-server/Dockerfile 14 | context: ./../../ 15 | command: start 16 | environment: 17 | NODE_ENV: production 18 | DM_SERVER_API_URL: http://localhost:8080 19 | DM_SERVER_PORT: 8080 20 | DM_BACKEND_TYPE: sparql-endpoint 21 | DM_BACKEND_SPARQL_ENDPOINT_URL: http://oxigraph:7878/query 22 | depends_on: 23 | - oxigraph 24 | ports: 25 | - 8080:8080 26 | profiles: 27 | - cluster 28 | server_cluster: 29 | build: 30 | dockerfile: ./packages/http-server/Dockerfile 31 | context: ./../../ 32 | command: start 33 | environment: 34 | NODE_ENV: production 35 | DM_SERVER_API_URL: http://localhost:8080 36 | DM_SERVER_PORT: 8080 37 | DM_BACKEND_TYPE: sparql-endpoint 38 | DM_BACKEND_SPARQL_ENDPOINT_URL: http://host.docker.internal:7878/query 39 | ports: 40 | - 8080:8080 41 | profiles: 42 | - db78 43 | -------------------------------------------------------------------------------- /scripts/package-tests.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import {fileURLToPath} from 'url' 3 | import { validateShacl } from './validate-shacl.js' 4 | 5 | // TODO rewrite with Jest -- currently, because it runs in streams 6 | // the pass/fail does not connect with which test example failed in stdoot connect with which test example failed in stdout 7 | 8 | const __filename = path.basename(import.meta.url); 9 | const __dirname = path.dirname(import.meta.url); 10 | const ROOT_DIR = path.resolve(fileURLToPath(__dirname), "..") 11 | 12 | 13 | const TEST_EXAMPLES = [ 14 | // TODO : generate these .NT files from JSON-LD as part of testing, or stream the content directly in 15 | `${ROOT_DIR}/examples/docmaps-example-embo-01.jsonld.nt`, 16 | `${ROOT_DIR}/examples/docmaps-example-elife-01.jsonld.nt`, 17 | `${ROOT_DIR}/examples/docmaps-example-elife-02.jsonld.nt`, 18 | `${ROOT_DIR}/examples/docmaps-example-biorxiv-01.jsonld.nt`, 19 | ] 20 | 21 | const SHACL_FILE = `${ROOT_DIR}/docmaps-shapes.ttl` 22 | 23 | async function main() { 24 | var ex = '' 25 | console.log(`Using SHACL shape file: ${SHACL_FILE}`) 26 | console.log(`to validate data files...`) 27 | for (ex of TEST_EXAMPLES) { 28 | await validateShacl(SHACL_FILE, ex); 29 | } 30 | } 31 | 32 | main(); 33 | -------------------------------------------------------------------------------- /packages/spa/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@docmaps/spa", 3 | "version": "1.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "build:deps": "pnpm run --filter=@docmaps/spa^... build", 8 | "build": "rollup -c", 9 | "dev": "rollup -c -w", 10 | "start": "sirv public --no-clear", 11 | "lint": "echo !!!!WARN: No linter setup!", 12 | "test": "echo !!!!WARN: No test setup!" 13 | }, 14 | "devDependencies": { 15 | "@docmaps/build-configs": "workspace:^", 16 | "@rollup/plugin-commonjs": "^25.0.0", 17 | "@rollup/plugin-node-resolve": "^15.1.0", 18 | "@rollup/plugin-terser": "^0.4.3", 19 | "rollup": "^4.0.0", 20 | "rollup-plugin-css-only": "^4.3.0", 21 | "rollup-plugin-livereload": "^2.0.5", 22 | "rollup-plugin-polyfill-node": "^0.13.0", 23 | "rollup-plugin-svelte": "^7.1.5", 24 | "svelte": "^4.0.0", 25 | "typescript": "^5.2.2" 26 | }, 27 | "dependencies": { 28 | "@docmaps/etl": "workspace:^0.1.2", 29 | "@docmaps/widget": "workspace:^0.0.0", 30 | "@spider-ui/global-event-registry": "^0.2.7", 31 | "@docmaps/sdk": "workspace:^0.0.0", 32 | "fp-ts": "^2.16.0", 33 | "prismjs": "^1.29.0", 34 | "sirv-cli": "^2.0.2", 35 | "upgraded-element": "^0.6.5" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/http-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@docmaps/http-client", 3 | "version": "0.1.0", 4 | "description": "HTTP client for Docmaps API specification", 5 | "type": "module", 6 | "main": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "scripts": { 9 | "clean": "rm -rf dist/", 10 | "lint": "npx eslint .", 11 | "lint:fix": "npx eslint --fix .", 12 | "build:deps": "pnpm run --filter=@docmaps/http-client^... build", 13 | "build": "tsc --declaration" 14 | }, 15 | "keywords": [], 16 | "author": "eve github.com/ships", 17 | "license": "ISC", 18 | "files": [ 19 | "dist/", 20 | "README.md", 21 | "package.json", 22 | "tsconfig.json" 23 | ], 24 | "dependencies": { 25 | "@ts-rest/core": "^3.30.2", 26 | "@docmaps/sdk": "workspace:^0.0.0" 27 | }, 28 | "devDependencies": { 29 | "@docmaps/build-configs": "workspace:^", 30 | "@types/node": "^20.0.0", 31 | "@typescript-eslint/eslint-plugin": "^6.0.0", 32 | "@typescript-eslint/parser": "^6.0.0", 33 | "eslint": "^8.39.0", 34 | "eslint-config-prettier": "^9.0.0", 35 | "eslint-plugin-prettier": "^5.0.0", 36 | "prettier": "^3.0.0", 37 | "ts-mockito": "^2.6.1", 38 | "typescript": "^5.2.2", 39 | "zod": "^3.22.2" 40 | }, 41 | "engines": { 42 | "node": ">=18.14.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /scripts/validate-shacl.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import factory from 'rdf-ext' 3 | import ParserN3 from '@rdfjs/parser-n3' 4 | import SHACLValidator from 'rdf-validate-shacl' 5 | 6 | async function loadDataset (filePath) { 7 | const stream = fs.createReadStream(filePath) 8 | const parser = new ParserN3({ factory }) 9 | return factory.dataset().import(parser.import(stream)) 10 | } 11 | 12 | export async function validateShacl(shapeFile, dataFile) { 13 | process.stdout.write(`... ${dataFile} `) 14 | 15 | const shapes = await loadDataset(shapeFile) 16 | const data = await loadDataset(dataFile) 17 | 18 | const validator = new SHACLValidator(shapes, { factory }) 19 | const report = await validator.validate(data) 20 | 21 | 22 | // Check conformance: `true` or `false` 23 | console.log(report.conforms) 24 | 25 | if (!report.conforms) { 26 | for (const result of report.results) { 27 | // See https://www.w3.org/TR/shacl/#results-validation-result for details 28 | // about each property 29 | console.log(result.message) 30 | console.log(result.path) 31 | console.log(result.focusNode) 32 | console.log(result.severity) 33 | console.log(result.sourceConstraintComponent) 34 | console.log(result.sourceShape) 35 | } 36 | 37 | // Validation report as RDF dataset 38 | console.log(report.dataset) 39 | 40 | process.exit(1) 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /.github/workflows/specification-tests.yaml: -------------------------------------------------------------------------------- 1 | name: Test docmaps specifications 2 | 3 | on: 4 | push: 5 | # Allows you to run this workflow manually from the Actions tab 6 | workflow_dispatch: 7 | workflow_call: 8 | 9 | jobs: 10 | nodejs_test: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [18.14.0] 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | 25 | - uses: pnpm/action-setup@v2 26 | name: Install pnpm 27 | id: pnpm-install 28 | with: 29 | version: 8 30 | run_install: false 31 | 32 | - name: Get pnpm store directory 33 | id: pnpm-cache 34 | shell: bash 35 | run: | 36 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 37 | 38 | - uses: actions/cache@v3 39 | name: Setup pnpm cache 40 | with: 41 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 42 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }} 43 | restore-keys: | 44 | ${{ runner.os }}-pnpm-store- 45 | 46 | - name: Install dependencies 47 | run: | 48 | pnpm install; 49 | 50 | - name: Test 51 | run: | 52 | pnpm test; 53 | -------------------------------------------------------------------------------- /packages/http-server/src/adapter/sparql_fetch.ts: -------------------------------------------------------------------------------- 1 | import { SparqlEndpointFetcher } from 'fetch-sparql-endpoint' 2 | import fetch from 'isomorphic-fetch' 3 | import type * as RDF from '@rdfjs/types' 4 | import type { SparqlProcessor } from '.' 5 | import type { Construct, Describe } from '@tpluscode/sparql-builder' 6 | import * as TE from 'fp-ts/lib/TaskEither' 7 | 8 | // This one uses SparqlEndpointFetcher to create a stream of Triples. 9 | // Should be as thin as possible. 10 | // 11 | // 12 | // to test this, we would need to inject fetch like this https://github.com/rubensworks/fetch-sparql-endpoint.js/blob/a882427835dcd356eb265ce93a70388cf955c631/test/SparqlEndpointFetcher-test.ts 13 | export class SparqlFetchBackend implements SparqlProcessor { 14 | endpoint: string 15 | fetcher: SparqlEndpointFetcher 16 | 17 | constructor(endpoint: string) { 18 | this.endpoint = endpoint 19 | this.fetcher = new SparqlEndpointFetcher({ 20 | fetch: fetch, 21 | }) 22 | } 23 | 24 | triples(query: Construct | Describe): TE.TaskEither> { 25 | return TE.tryCatch( 26 | () => this.fetcher.fetchTriples(this.endpoint, query.build()), 27 | (reason) => new Error(`failed to fetch triples from sparql endpoint: ${reason}`), 28 | ) 29 | } 30 | 31 | // bindings(query: string): Promise> { 32 | // return this.fetcher.fetchBindings(this.endpoint, query).then((rs) => rs.) 33 | // } 34 | } 35 | -------------------------------------------------------------------------------- /packages/widget/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/widget/src/assets/logo-large.ts: -------------------------------------------------------------------------------- 1 | import { svg } from 'lit' 2 | 3 | export const logoLarge = svg` 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | `; 12 | -------------------------------------------------------------------------------- /packages/widget/src/assets/logo-small.ts: -------------------------------------------------------------------------------- 1 | import { svg } from 'lit'; 2 | 3 | export const logoSmall = svg` 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | `; 12 | -------------------------------------------------------------------------------- /packages/widget/src/assets/copy-to-clipboard.ts: -------------------------------------------------------------------------------- 1 | import { svg } from 'lit'; 2 | 3 | export const copyToClipboardButton = ( 4 | textToCopy: string, 5 | onCopy: (newText: string, x: number, y: number) => void, 6 | ) => svg` 7 | 9 | 10 | 11 | `; 12 | 13 | // Be aware: the copy-to-clipboard functionality is not tested. Sadly, it is basically impossible to test clipboard 14 | // functionality in Playwright. 15 | const copyToClipboard = ( 16 | event: PointerEvent, 17 | textToCopy: string, 18 | onCopy: (newText: string, x: number, y: number) => void, 19 | ) => { 20 | navigator.clipboard 21 | .writeText(textToCopy) 22 | .then(() => { 23 | const x: number = event.pageX; 24 | const y: number = event.pageY; 25 | onCopy('Copied!', x, y); 26 | }) 27 | .catch((err) => { 28 | console.log('Failed to copy to clipboard\n\n', err); 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /.github/workflows/spa-tests.yaml: -------------------------------------------------------------------------------- 1 | name: Test SPA 2 | 3 | on: 4 | push: 5 | # Allows you to run this workflow manually from the Actions tab 6 | workflow_dispatch: 7 | workflow_call: 8 | 9 | env: 10 | PKG_DIR: "packages/spa" 11 | 12 | jobs: 13 | nodejs_test: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [18.14.0] 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | 28 | - uses: pnpm/action-setup@v2 29 | name: Install pnpm 30 | id: pnpm-install 31 | with: 32 | version: 8 33 | run_install: false 34 | 35 | - name: Get pnpm store directory 36 | id: pnpm-cache 37 | shell: bash 38 | run: | 39 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 40 | 41 | - uses: actions/cache@v3 42 | name: Setup pnpm cache 43 | with: 44 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 45 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('${{env.PKG_DIR}}/pnpm-lock.yaml') }} 46 | restore-keys: | 47 | ${{ runner.os }}-pnpm-store- 48 | 49 | - name: Install dependencies 50 | run: | 51 | cd ${{env.PKG_DIR}} ; 52 | pnpm install; 53 | 54 | - name: Verify builds 55 | run: | 56 | cd ${{env.PKG_DIR}} ; 57 | pnpm run build:deps ; 58 | 59 | - name: Test 60 | run: | 61 | cd ${{env.PKG_DIR}} ; 62 | pnpm test; 63 | 64 | - name: Lint Check 65 | run: | 66 | cd ${{env.PKG_DIR}} ; 67 | pnpm lint; 68 | -------------------------------------------------------------------------------- /.github/workflows/etl-tests.yaml: -------------------------------------------------------------------------------- 1 | name: Test @docmaps/etl 2 | 3 | on: 4 | push: 5 | # Allows you to run this workflow manually from the Actions tab 6 | workflow_dispatch: 7 | workflow_call: 8 | 9 | env: 10 | PKG_DIR: "packages/etl" 11 | 12 | jobs: 13 | nodejs_test: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [18.14.0] 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | 28 | - uses: pnpm/action-setup@v2 29 | name: Install pnpm 30 | id: pnpm-install 31 | with: 32 | version: 8 33 | run_install: false 34 | 35 | - name: Get pnpm store directory 36 | id: pnpm-cache 37 | shell: bash 38 | run: | 39 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 40 | 41 | - uses: actions/cache@v3 42 | name: Setup pnpm cache 43 | with: 44 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 45 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('${{env.PKG_DIR}}/pnpm-lock.yaml') }} 46 | restore-keys: | 47 | ${{ runner.os }}-pnpm-store- 48 | 49 | - name: Install dependencies 50 | run: | 51 | cd ${{env.PKG_DIR}} ; 52 | pnpm install; 53 | 54 | - name: Verify builds 55 | run: | 56 | cd ${{env.PKG_DIR}} ; 57 | pnpm run build:deps ; 58 | 59 | - name: Test 60 | run: | 61 | cd ${{env.PKG_DIR}} ; 62 | pnpm test; 63 | 64 | - name: Lint Check 65 | run: | 66 | cd ${{env.PKG_DIR}} ; 67 | pnpm lint; 68 | -------------------------------------------------------------------------------- /.github/workflows/example-tests.yaml: -------------------------------------------------------------------------------- 1 | name: Test example 2 | 3 | on: 4 | push: 5 | # Allows you to run this workflow manually from the Actions tab 6 | workflow_dispatch: 7 | workflow_call: 8 | 9 | env: 10 | PKG_DIR: "packages/example" 11 | 12 | jobs: 13 | nodejs_test: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [18.14.0] 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | 28 | - uses: pnpm/action-setup@v2 29 | name: Install pnpm 30 | id: pnpm-install 31 | with: 32 | version: 8 33 | run_install: false 34 | 35 | - name: Get pnpm store directory 36 | id: pnpm-cache 37 | shell: bash 38 | run: | 39 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 40 | 41 | - uses: actions/cache@v3 42 | name: Setup pnpm cache 43 | with: 44 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 45 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('${{env.PKG_DIR}}/pnpm-lock.yaml') }} 46 | restore-keys: | 47 | ${{ runner.os }}-pnpm-store- 48 | 49 | - name: Install dependencies 50 | run: | 51 | cd ${{env.PKG_DIR}} ; 52 | pnpm install; 53 | 54 | - name: Verify builds 55 | run: | 56 | cd ${{env.PKG_DIR}} ; 57 | pnpm run build:deps ; 58 | 59 | - name: Test 60 | run: | 61 | cd ${{env.PKG_DIR}} ; 62 | pnpm test; 63 | 64 | - name: Lint Check 65 | run: | 66 | cd ${{env.PKG_DIR}} ; 67 | pnpm lint; 68 | -------------------------------------------------------------------------------- /.github/workflows/sdk-tests.yaml: -------------------------------------------------------------------------------- 1 | name: Test @docmaps/sdk 2 | 3 | on: 4 | push: 5 | # Allows you to run this workflow manually from the Actions tab 6 | workflow_dispatch: 7 | workflow_call: 8 | 9 | env: 10 | PKG_DIR: "packages/sdk" 11 | 12 | jobs: 13 | nodejs_test: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [18.14.0] 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | 28 | - uses: pnpm/action-setup@v2 29 | name: Install pnpm 30 | id: pnpm-install 31 | with: 32 | version: 8 33 | run_install: false 34 | 35 | - name: Get pnpm store directory 36 | id: pnpm-cache 37 | shell: bash 38 | run: | 39 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 40 | 41 | - uses: actions/cache@v3 42 | name: Setup pnpm cache 43 | with: 44 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 45 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('${{env.PKG_DIR}}/pnpm-lock.yaml') }} 46 | restore-keys: | 47 | ${{ runner.os }}-pnpm-store- 48 | 49 | - name: Install dependencies 50 | run: | 51 | cd ${{env.PKG_DIR}} ; 52 | pnpm install; 53 | 54 | - name: Verify builds 55 | run: | 56 | cd ${{env.PKG_DIR}} ; 57 | pnpm run build:deps ; 58 | 59 | - name: Test 60 | run: | 61 | cd ${{env.PKG_DIR}} ; 62 | pnpm test; 63 | 64 | - name: Lint Check 65 | run: | 66 | cd ${{env.PKG_DIR}} ; 67 | pnpm lint; 68 | -------------------------------------------------------------------------------- /packages/etl/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@docmaps/etl", 3 | "version": "0.1.2", 4 | "description": "ETL tool for Docmaps", 5 | "type": "module", 6 | "main": "dist/index.js", 7 | "scripts": { 8 | "test": "ava", 9 | "clean": "rm -rf dist/", 10 | "test:integration": "ava test/integration/", 11 | "test:unit": "ava test/unit/", 12 | "start": "tsx dist/main.js", 13 | "lint": "npx eslint .", 14 | "lint:fix": "npx eslint --fix .", 15 | "build:deps": "pnpm run --filter=@docmaps/etl^... build", 16 | "build": "tsc --declaration" 17 | }, 18 | "bin": { 19 | "docmaps-etl": "dist/main.js" 20 | }, 21 | "keywords": [], 22 | "author": "eve github.com/ships", 23 | "license": "ISC", 24 | "files": [ 25 | "dist/", 26 | "README.md", 27 | "package.json", 28 | "tsconfig.json" 29 | ], 30 | "dependencies": { 31 | "@commander-js/extra-typings": "^11.0.0", 32 | "@docmaps/sdk": "workspace:^0.0.0", 33 | "commander": "^11.0.0", 34 | "crossref-openapi-client-ts": "^1.5.0", 35 | "fp-ts": "^2.14.0", 36 | "tsx": "^4.0.0", 37 | "typescript-collections": "^1.3.3" 38 | }, 39 | "devDependencies": { 40 | "@docmaps/build-configs": "workspace:^", 41 | "@types/node": "^20.0.0", 42 | "@typescript-eslint/eslint-plugin": "^6.0.0", 43 | "@typescript-eslint/parser": "^6.0.0", 44 | "ava": "^5.2.0", 45 | "eslint": "^8.39.0", 46 | "eslint-config-prettier": "^9.0.0", 47 | "eslint-plugin-prettier": "^5.0.0", 48 | "prettier": "^3.0.0", 49 | "ts-mockito": "^2.6.1", 50 | "typescript": "^5.2.2" 51 | }, 52 | "ava": { 53 | "extensions": { 54 | "ts": "module" 55 | }, 56 | "nodeArguments": [ 57 | "--loader=tsx/esm" 58 | ], 59 | "files": [ 60 | "**/*.test.ts" 61 | ] 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /.github/workflows/widget-tests.yaml: -------------------------------------------------------------------------------- 1 | name: Test widget 2 | 3 | on: 4 | push: 5 | # Allows you to run this workflow manually from the Actions tab 6 | workflow_dispatch: 7 | workflow_call: 8 | 9 | env: 10 | PKG_DIR: "packages/widget" 11 | 12 | jobs: 13 | nodejs_test: 14 | runs-on: ubuntu-latest 15 | container: mcr.microsoft.com/playwright:v1.40.1 16 | 17 | timeout-minutes: 20 18 | 19 | strategy: 20 | matrix: 21 | node-version: [18.14.0] 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | 26 | - name: Use Node.js ${{ matrix.node-version }} 27 | uses: actions/setup-node@v4 28 | with: 29 | node-version: ${{ matrix.node-version }} 30 | 31 | - uses: pnpm/action-setup@v2 32 | name: Install pnpm 33 | id: pnpm-install 34 | with: 35 | version: 8 36 | run_install: false 37 | 38 | - name: Get pnpm store directory 39 | id: pnpm-cache 40 | shell: bash 41 | run: | 42 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 43 | 44 | - uses: actions/cache@v3 45 | name: Setup pnpm cache 46 | with: 47 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 48 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('${{env.PKG_DIR}}/pnpm-lock.yaml') }} 49 | restore-keys: | 50 | ${{ runner.os }}-pnpm-store- 51 | 52 | - name: Install dependencies 53 | run: | 54 | cd ${{env.PKG_DIR}} 55 | pnpm install; 56 | 57 | - name: Verify builds 58 | run: | 59 | cd ${{env.PKG_DIR}} 60 | pnpm run build:deps ; 61 | 62 | - name: Run Playwright tests 63 | run: | 64 | cd ${{env.PKG_DIR}} 65 | HOME=/root pnpm run test 66 | 67 | - uses: actions/upload-artifact@v3 68 | if: always() 69 | with: 70 | name: playwright-report 71 | path: ${{env.PKG_DIR}}/playwright-report/ 72 | retention-days: 30 73 | -------------------------------------------------------------------------------- /packages/widget/test/fixtures/docmapWithOneStep.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | id: 'https://sciety.org/docmaps/v1/articles/10.21203/rs.3.rs-1043992/v1/biophysics-colab.docmap.json', 3 | type: 'docmap', 4 | created: '2022-04-19T11:40:09.289Z', 5 | publisher: { 6 | id: 'https://biophysics.sciencecolab.org', 7 | homepage: 'https://biophysics.sciencecolab.org/', 8 | logo: 'https://sciety.org/static/groups/biophysics-colab--4bbf0c12-629b-4bb8-91d6-974f4df8efb2.png', 9 | name: 'Biophysics Colab', 10 | account: { 11 | id: 'https://sciety.org/groups/biophysics-colab', 12 | service: 'https://sciety.org/', 13 | }, 14 | }, 15 | 'first-step': '_:b1', 16 | steps: { 17 | '_:b1': { 18 | inputs: [ 19 | { 20 | doi: '10.21203/rs.3.rs-1043992/v1', 21 | url: 'https://doi.org/10.21203/rs.3.rs-1043992/v1', 22 | }, 23 | ], 24 | actions: [ 25 | { 26 | participants: [ 27 | { 28 | actor: { 29 | type: 'person', 30 | name: 'anonymous', 31 | }, 32 | role: 'peer-reviewer', 33 | }, 34 | ], 35 | outputs: [ 36 | { 37 | type: 'review-article', 38 | published: '2022-04-19T11:35:26.469Z', 39 | content: [ 40 | { 41 | type: 'web-page', 42 | url: 'https://sciety.org/articles/activity/10.21203/rs.3.rs-1043992/v1#hypothesis:ztQE-L_UEey5hB8TupDhxw', 43 | }, 44 | { 45 | type: 'web-page', 46 | url: 'https://hypothes.is/a/ztQE-L_UEey5hB8TupDhxw', 47 | }, 48 | { 49 | type: 'web-content', 50 | url: 'https://sciety.org/evaluations/hypothesis:ztQE-L_UEey5hB8TupDhxw/content', 51 | }, 52 | ], 53 | }, 54 | ], 55 | }, 56 | ], 57 | assertions: [], 58 | }, 59 | }, 60 | '@context': 'https://w3id.org/docmaps/context.jsonld', 61 | } 62 | -------------------------------------------------------------------------------- /packages/widget/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@docmaps/widget", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "tsc && vite build", 8 | "build:deps": "pnpm run --filter=@docmaps/widget^... build", 9 | "preview": "vite preview", 10 | "install:browsers": "playwright install --with-deps", 11 | "test": "pnpm run test:unit && pnpm run test:integration", 12 | "test:unit": "ava", 13 | "test:integration": "playwright test", 14 | "test:integration:all-browsers": "ALL_BROWSERS=true playwright test", 15 | "test:integration:ui": "playwright test --ui", 16 | "test:integration:ui:all-browsers": "ALL_BROWSERS=true playwright test --ui" 17 | }, 18 | "exports": { 19 | ".": "./dist/index.js" 20 | }, 21 | "types": "./dist/index.d.ts", 22 | "dependencies": { 23 | "@docmaps/http-client": "workspace:^0.1.0", 24 | "@lit/task": "^1.0.0", 25 | "d3": "^7.8.5", 26 | "d3-force": "^3.0.0", 27 | "dagre": "^0.8.5", 28 | "@docmaps/sdk": "workspace:^0.0.0", 29 | "fp-ts": "^2.16.1", 30 | "lit": "^2.7.6" 31 | }, 32 | "devDependencies": { 33 | "@docmaps/build-configs": "workspace:^", 34 | "@playwright/test": "^1.40.0", 35 | "@types/d3": "^7.4.2", 36 | "@types/d3-force": "^3.0.7", 37 | "@types/d3-selection": "^3.0.8", 38 | "@types/dagre": "^0.7.51", 39 | "@types/node": "^20.0.0", 40 | "@typescript-eslint/eslint-plugin": "^6.0.0", 41 | "@typescript-eslint/parser": "^6.0.0", 42 | "ava": "^5.2.0", 43 | "eslint": "^8.39.0", 44 | "eslint-config-prettier": "^9.0.0", 45 | "eslint-plugin-prettier": "^5.0.0", 46 | "prettier": "^3.0.0", 47 | "typescript": "^5.2.2", 48 | "vite": "^4.4.5", 49 | "vite-plugin-dts": "^3.6.3" 50 | }, 51 | "ava": { 52 | "extensions": { 53 | "ts": "module" 54 | }, 55 | "nodeArguments": [ 56 | "--loader=tsx/esm" 57 | ], 58 | "files": [ 59 | "test/unit/**/*.test.ts" 60 | ] 61 | }, 62 | "files": [ 63 | "dist", 64 | "types", 65 | "README.md", 66 | "package.json", 67 | "tsconfig.json" 68 | ] 69 | } 70 | -------------------------------------------------------------------------------- /packages/http-server/src/api.ts: -------------------------------------------------------------------------------- 1 | import type { ApiInfo, BackendAdapter, ThingSpec } from './types' 2 | import * as TE from 'fp-ts/TaskEither' 3 | import * as D from '@docmaps/sdk' 4 | 5 | /** ApiInstance - a concrete class that handles sourcing data and composing answers 6 | * 7 | * This class exists to decouple the notion of an "API" and its data source 8 | * from the networking layer that is most often going to sit in front of it 9 | * and make it available. 10 | * 11 | * You can use this class to connect to any concrete backend and get a spec- 12 | * compliant API instance. 13 | */ 14 | export class ApiInstance { 15 | adapter: BackendAdapter 16 | api_url: URL 17 | peers: URL[] 18 | expiry_max_seconds: number 19 | expiry_max_retrievals: number 20 | 21 | /** 22 | * @param api_url is required for info method even if you are not serving over http. 23 | */ 24 | constructor( 25 | adapter: BackendAdapter, 26 | api_url: URL, 27 | peers: URL[] = [], 28 | expiry_max_seconds: number = 60, 29 | expiry_max_retrievals: number = 1, 30 | ) { 31 | this.adapter = adapter 32 | this.api_url = api_url 33 | this.peers = peers 34 | this.expiry_max_seconds = expiry_max_seconds 35 | this.expiry_max_retrievals = expiry_max_retrievals 36 | } 37 | 38 | get_info(): ApiInfo { 39 | return { 40 | api_version: '0.1.0', 41 | api_url: this.api_url.toString(), 42 | ephemeral_document_expiry: { 43 | max_seconds: this.expiry_max_seconds, 44 | max_retrievals: this.expiry_max_retrievals, 45 | }, 46 | peers: this.peers.map((p) => ({ api_url: p.toString() })), 47 | } 48 | } 49 | 50 | // FIXME: it is likely that this needs to be called docmap_by_iri instead. 51 | // Current rationale for this name is that the IRI-based docmap "may not" 52 | // be implicit in any user of this contract, but that is pretty weak. 53 | get_docmap_by_id(id: string): TE.TaskEither { 54 | return this.adapter.docmapWithIri(id) 55 | } 56 | 57 | get_docmap_for_thing(s: ThingSpec): TE.TaskEither { 58 | return this.adapter.docmapForThing(s) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /scripts/jsonld-to-rdf.js: -------------------------------------------------------------------------------- 1 | import ParserJsonld from "@rdfjs/parser-jsonld"; 2 | import Serializer from "@rdfjs/serializer-turtle"; 3 | import ParserN3 from "@rdfjs/parser-n3"; 4 | import formats from "@rdfjs/formats-common"; 5 | import SerializerNtriples from "@rdfjs/serializer-ntriples"; 6 | import { Readable, Transform } from "stream"; 7 | import fs from "fs"; 8 | import readline from "readline"; 9 | 10 | const BASE_IRI = "https://w3id.org/docmaps/examples/"; 11 | 12 | async function processFile(fileName, callback) { 13 | try { 14 | const readStream = readline.createInterface({ 15 | input: fs.createReadStream(fileName, "utf-8"), 16 | }); 17 | 18 | let i = 0; 19 | 20 | for await (const line of readStream) { 21 | console.error( 22 | `${i} ... Processing graph: ${line.substring(0, 120)} with length ${ 23 | line.length 24 | }`, 25 | ); 26 | await callback(line); 27 | i++; 28 | } 29 | } catch (err) { 30 | console.error(err); 31 | } 32 | 33 | console.error("done"); 34 | } 35 | 36 | async function processGraphString(g) { 37 | const input = new Readable({ 38 | read: () => { 39 | input.push(g); 40 | input.push(null); 41 | }, 42 | }); 43 | 44 | return new Promise((res, rej) => { 45 | const parserJsonld = new ParserJsonld({ 46 | baseIRI: BASE_IRI, 47 | steamingProfile: false, 48 | }); 49 | const serializerNtriples = new SerializerNtriples(); 50 | 51 | // TODO: include declaration that a docmap is a :Docmap 52 | const quads = parserJsonld.import(input); 53 | 54 | const nt = serializerNtriples.import(quads); 55 | nt.on("data", (d) => { 56 | process.stdout.write(d); 57 | }) 58 | .on("end", () => { 59 | console.error("received `end` event"); 60 | res(); 61 | }) 62 | .on("close", (err) => { 63 | console.error("received `close` event"); 64 | if (err) { 65 | rej(err); 66 | } else { 67 | res(); 68 | } 69 | }); 70 | }); 71 | } 72 | 73 | const fileName = process.argv[2]; 74 | const _ = await processFile(fileName, processGraphString); 75 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Docmaps OSS 4 | 54 | 55 | 56 |
57 |

Docmaps Project Open Source Documentation

58 | 65 |

Docmaps are a powerful, community-driven framework that will meet key requirements for representations of editorial processes in a healthy publishing ecosystem.

66 |

This page is your gateway to technical documentation for the packages and tools maintained by the Docmaps Project core maintainers. Explore the navigation links above to get started.

67 |
Copyright © 2023 Docmaps Project & Knowledge Futures, Inc
68 | 69 | 70 | -------------------------------------------------------------------------------- /packages/etl/README.md: -------------------------------------------------------------------------------- 1 | # Extract-Transform-Load CLI for Docmaps 2 | 3 | This typescript library is designed to provide core, highly-general docmaps 4 | functionality for ease-of-use in Typescript. It provides out-of-the-box 5 | validation of JSON-LD documents interpreted as docmaps directly. It is intended 6 | to additionally support validation of Docmap sub-elements, such as individual 7 | Actions or Actors that might be published separately from a whole Docmap. It 8 | will also be integrated into concrete tools such as a docmap-from-meca ETL pipeline 9 | and general visualization tools. 10 | 11 | # Usage 12 | 13 | Via a global install: 14 | 15 | ```bash 16 | npm i -g @docmaps/etl 17 | npx docmaps-etl item --source crossref-api 10.5194/angeo-40-247-2022 # sub with your DOI of interest 18 | ``` 19 | 20 | In this repository: 21 | 22 | ```bash 23 | pnpm install # or npm install 24 | pnpm docmaps-etl item --source crossref-api 10.5194/angeo-40-247-2022 # or npm docmaps-etl 25 | ``` 26 | 27 | ## Implementation 28 | 29 | This tool and library are written using the [`@docmaps/sdk` package](/packages/sdk) 30 | in this repository, as well as the [`crossref-openapi-client-ts`](https://github.com/Docmaps-Project/crossref-openapi-client-ts) 31 | also maintained by Knowledge Futures, Inc. As seen in `src/crossref.ts`[src/crossref.ts], 32 | Codecs from the SDK are processed using functional paradigms provided conveniently by 33 | `fp-ts`. 34 | 35 | ## Documentation 36 | 37 | Documentation is comments-only for now. See [relevant issue](https://github.com/Docmaps-Project/docmaps/issues/20). 38 | 39 | ## Contributing 40 | 41 | For Code of Conduct, see the repository-wide [CODE_OF_CONDUCT.md](/CODE_OF_CONDUCT.md). 42 | 43 | For info about local development of this repository, see [CONTRIBUTING.md](CONTRIBUTING.md). 44 | 45 | ## Current next steps 46 | 47 | Review the issues on this repository for up-to-date info of desired improvements. 48 | There are also expressive TODOs in the codebase. 49 | Here are some examples: 50 | 51 | - [ ] Enable direct configuration of the publisher information for generated Docmaps 52 | - [ ] Handle paginated requests for efficient parallel processing. 53 | - [ ] Make the ETL interface generic enough to handle at least one other data source than Crossref. 54 | -------------------------------------------------------------------------------- /packages/etl/test/unit/command.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { isLeft } from 'fp-ts/lib/Either' 3 | import { ItemCmd } from '../../src/command' 4 | import { whenThenResolve } from './utils' 5 | import * as cm from './__fixtures__/crossref' 6 | 7 | // This test is redundant with a test in the crossref case. Possibly there 8 | // is no need for a test suite at this level as integration can handle it; 9 | // however this asserts that the Cmd still passes through. 10 | test('ItemCmd: crossref: happy-path scenario: a manuscript with one preprint and no reviews', async (t) => { 11 | const mocks = cm.CrossrefClientMocks() 12 | whenThenResolve( 13 | mocks.worksT.getWorks, 14 | { doi: cm.MANUSCRIPT_DOI }, 15 | cm.mockCrossrefManuscriptWithPreprintResponse, 16 | ) 17 | whenThenResolve(mocks.worksT.getWorks, { doi: cm.PREPRINT_DOI }, cm.mockCrossrefPreprintResponse) 18 | 19 | const publisher = { 20 | id: 'my_pub_id', 21 | name: 'my_name', 22 | } 23 | 24 | const res = await ItemCmd([cm.MANUSCRIPT_DOI], { 25 | source: { 26 | preset: 'crossref-api', 27 | client: mocks.crs, 28 | }, 29 | publisher: publisher, 30 | }) 31 | 32 | if (isLeft(res)) { 33 | t.fail(`Got error instead of docmaps: ${res.left}`) 34 | return 35 | } 36 | 37 | t.is(res.right.length, 1) 38 | const dm = res.right[0] 39 | 40 | // necessary because Typescript doesn't narrow down type of dm just because 41 | // test failure guarantees we can't get here 42 | if (!dm) { 43 | t.fail('impossibly, we couldnt find the first docmap in a list of one') 44 | return //necessary 45 | } 46 | 47 | t.deepEqual(dm.type, 'docmap') 48 | t.deepEqual(dm.publisher, { 49 | id: 'my_pub_id', 50 | name: 'my_name', 51 | }) 52 | t.is(dm.steps ? Object.keys(dm.steps).length : 0, 2) 53 | t.is(dm.steps?.['_:b0']?.inputs?.length, 0) 54 | t.is(dm.steps?.['_:b0']?.actions[0]?.inputs.length, 0) 55 | t.deepEqual(dm.steps?.['_:b0']?.actions[0]?.outputs[0]?.doi, cm.PREPRINT_DOI) 56 | t.deepEqual(dm.steps?.['_:b1']?.actions[0]?.inputs[0]?.doi, cm.PREPRINT_DOI) 57 | t.deepEqual(dm.steps?.['_:b1']?.actions[0]?.outputs[0]?.doi, cm.MANUSCRIPT_DOI) 58 | //TODO: can write stronger assertions as we learn what this should look like 59 | }) 60 | -------------------------------------------------------------------------------- /packages/spa/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { spawn } from 'child_process'; 2 | import svelte from 'rollup-plugin-svelte'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | import terser from '@rollup/plugin-terser'; 5 | import resolve from '@rollup/plugin-node-resolve'; 6 | import livereload from 'rollup-plugin-livereload'; 7 | import nodePolyfills from 'rollup-plugin-polyfill-node'; 8 | import css from 'rollup-plugin-css-only'; 9 | 10 | const production = !process.env.ROLLUP_WATCH; 11 | 12 | function serve() { 13 | let server; 14 | 15 | function toExit() { 16 | if (server) server.kill(0); 17 | } 18 | 19 | return { 20 | writeBundle() { 21 | if (server) return; 22 | server = spawn('npm', ['run', 'start', '--', '--dev'], { 23 | stdio: ['ignore', 'inherit', 'inherit'], 24 | shell: true 25 | }); 26 | 27 | process.on('SIGTERM', toExit); 28 | process.on('exit', toExit); 29 | } 30 | }; 31 | } 32 | 33 | export default { 34 | input: 'src/main.js', 35 | output: { 36 | sourcemap: true, 37 | format: 'iife', 38 | name: 'app', 39 | file: 'public/build/bundle.js' 40 | }, 41 | plugins: [ 42 | svelte({ 43 | compilerOptions: { 44 | // enable run-time checks when not in production 45 | dev: !production 46 | } 47 | }), 48 | // we'll extract any component CSS out into 49 | // a separate file - better for performance 50 | css({ output: 'bundle.css' }), 51 | 52 | nodePolyfills(), 53 | 54 | // If you have external dependencies installed from 55 | // npm, you'll most likely need these plugins. In 56 | // some cases you'll need additional configuration - 57 | // consult the documentation for details: 58 | // https://github.com/rollup/plugins/tree/master/packages/commonjs 59 | resolve({ 60 | browser: true, 61 | dedupe: ['svelte'], 62 | exportConditions: ['svelte'] 63 | }), 64 | commonjs(), 65 | 66 | // In dev mode, call `npm run start` once 67 | // the bundle has been generated 68 | !production && serve(), 69 | 70 | // Watch the `public` directory and refresh the 71 | // browser on changes when not in production 72 | !production && livereload('public'), 73 | 74 | // If we're building for production (npm run build 75 | // instead of npm run dev), minify 76 | production && terser() 77 | ], 78 | watch: { 79 | clearScreen: false 80 | } 81 | }; 82 | -------------------------------------------------------------------------------- /packages/example/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as D from '@docmaps/sdk' 2 | import * as E from 'fp-ts/lib/Either' 3 | import * as A from 'fp-ts/lib/Array' 4 | import {pipe} from 'fp-ts/lib/function' 5 | import * as t from 'io-ts' 6 | import util from "util"; 7 | import { readFileSync } from 'fs'; 8 | 9 | // program starts 10 | console.log("\n\n---Docmaps exploration in TS---\n\n") 11 | 12 | // get a file as string (this could be an API response on a browser) 13 | const file = readFileSync('./docmap.jsonld', 'utf-8'); 14 | 15 | //-- equivalent to: 16 | // const program = D.Docmap.decode(JSON.parse(file)) followed by more computation 17 | 18 | const program = pipe( 19 | // any value as input 20 | file, 21 | // function that takes that value as only argument 22 | JSON.parse, 23 | // function that takes the return of previous as only argument 24 | // this one returns an E.Either (actually, with a subtype of Error) 25 | D.Docmap.decode, 26 | 27 | // here we mean "map" not across a collection but across the monadic Either type 28 | // it maps the function provided against the success case (when we did get a docmap) 29 | E.map((d) => (d.steps ? Object.values(d.steps): [])), 30 | 31 | // likewise, we map again. But now the success case contains an array, so we use 32 | // the standard array flatMap to produce a new array based on subarrays. 33 | E.map(A.flatMap((s) => s.actions)), 34 | 35 | // likewise, but more complex. 36 | E.map(A.map((a) => pipe( 37 | a.participants, 38 | // note the use of `any` here, which is because the `actor` field 39 | // currently doesn't have any obligatory fields. This leaks the 40 | // type safety slightly by assuming there is a `name`. Improvements 41 | // to the `@docmaps/sdk` based on narrower specification or even more 42 | // involved parsing in the consumer library (like this script) can 43 | // provide further safety. 44 | A.map((p) => (p.actor as any).name) 45 | ))), 46 | ) 47 | 48 | 49 | // -- In Functional programming, we "return errors" rather than throwing. 50 | // For this demo, I will unwrap that pattern here to show the outcome. 51 | if(E.isLeft(program)) { 52 | throw new Error("input was invalid docmap!", {cause: program.left}) 53 | } else { 54 | console.log(util.inspect(program.right, {depth: 2, colors: true})) 55 | } 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /packages/sdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@docmaps/sdk", 3 | "version": "0.0.0", 4 | "repository": "git@github.com:docmaps-project/docmaps.git", 5 | "homepage": "https://github.com/Docmaps-Project/docmaps/tree/main/packages/sdk", 6 | "description": "", 7 | "type": "module", 8 | "main": "dist/index.js", 9 | "types": "dist/index.d.ts", 10 | "scripts": { 11 | "test": "ava", 12 | "debug": "node --loader=ts-node/esm --experimental-specifier-resolution=node --nolazy -r ts-node/register/transpile-only --inspect-brk src/debuggable.ts", 13 | "lint": "eslint .", 14 | "lint:fix": "eslint --fix .", 15 | "docs:generate": "typedoc src/index.ts", 16 | "clean": "rm -rf dist/", 17 | "build:deps": "pnpm run --filter=@docmaps/sdk^... build", 18 | "build": "tsc --declaration" 19 | }, 20 | "engines": { 21 | "node": ">=18.14.0" 22 | }, 23 | "keywords": [], 24 | "author": "", 25 | "license": "ISC", 26 | "files": [ 27 | "dist/", 28 | "README.md", 29 | "package.json", 30 | "tsconfig.json" 31 | ], 32 | "dependencies": { 33 | "@rdfjs/data-model": "^2.0.1", 34 | "@rdfjs/parser-n3": "^2.0.1", 35 | "@rdfjs/serializer-jsonld-ext": "~4.0.0", 36 | "fp-ts": "^2.16.1", 37 | "io-ts": "^2.2.20", 38 | "io-ts-types": "^0.5.19", 39 | "monocle-ts": "^2.3.13", 40 | "newtype-ts": "^0.3.5", 41 | "rdf-ext": "^2.2.0", 42 | "readable-stream": "^4.0.0" 43 | }, 44 | "devDependencies": { 45 | "@docmaps/build-configs": "workspace:^", 46 | "@rdfjs/types": "^1.1.0", 47 | "@types/jsonld": "^1.5.8", 48 | "@types/n3": "^1.10.4", 49 | "@types/node": "^20.9.0", 50 | "@types/rdf-ext": "^2.0.0", 51 | "@types/rdfjs__data-model": "^2.0.4", 52 | "@types/rdfjs__parser-n3": "^2.0.1", 53 | "@types/rdfjs__serializer-jsonld-ext": "^2.0.5", 54 | "@types/readable-stream": "^4.0.0", 55 | "@typescript-eslint/eslint-plugin": "^6.0.0", 56 | "@typescript-eslint/parser": "^6.0.0", 57 | "ava": "^5.2.0", 58 | "eslint": "^8.39.0", 59 | "eslint-config-prettier": "^9.0.0", 60 | "eslint-plugin-prettier": "^5.0.0", 61 | "into-stream": "^8.0.0", 62 | "prettier": "^3.0.0", 63 | "typedoc": "^0.25.3", 64 | "typescript": "^5.2.2" 65 | }, 66 | "ava": { 67 | "extensions": { 68 | "ts": "module" 69 | }, 70 | "nodeArguments": [ 71 | "--loader=tsx/esm" 72 | ], 73 | "files": [ 74 | "**/*.test.ts" 75 | ] 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /packages/widget/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test'; 2 | 3 | const IS_CI = !!process.env.CI; 4 | const ALL_BROWSERS = !!process.env.ALL_BROWSERS; 5 | 6 | // Locally we only run tests in Chromium 7 | const localBrowsers = [ 8 | { 9 | name: 'chromium', 10 | use: { ...devices['Desktop Chrome'] }, 11 | }, 12 | ]; 13 | 14 | // In CI, we run tests in all supported browsers 15 | const all_browsers = [ 16 | { 17 | name: 'chromium', 18 | use: { ...devices['Desktop Chrome'] }, 19 | }, 20 | 21 | { 22 | name: 'firefox', 23 | use: { ...devices['Desktop Firefox'] }, 24 | }, 25 | 26 | { 27 | name: 'webkit', 28 | use: { ...devices['Desktop Safari'] }, 29 | }, 30 | ]; 31 | 32 | /** 33 | * Read environment variables from file. 34 | * https://github.com/motdotla/dotenv 35 | */ 36 | // require('dotenv').config(); 37 | 38 | const url: string = 'http://localhost:5173'; 39 | /** 40 | * See https://playwright.dev/docs/test-configuration. 41 | */ 42 | export default defineConfig({ 43 | testDir: './test/integration', 44 | /* The base directory, relative to the config file, for snapshot files created with toMatchSnapshot and toHaveScreenshot. */ 45 | snapshotDir: './__snapshots__', 46 | 47 | /* Maximum time one test can run for. */ 48 | timeout: 30 * 1000, 49 | 50 | /* Run tests in files in parallel */ 51 | fullyParallel: true, 52 | 53 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 54 | forbidOnly: IS_CI, 55 | 56 | /* Retry on CI only */ 57 | retries: IS_CI ? 2 : 0, 58 | 59 | /* Opt out of parallel tests on CI. */ 60 | workers: IS_CI ? 1 : undefined, 61 | 62 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 63 | reporter: IS_CI ? [['list'], ['html']] : 'list', 64 | 65 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 66 | use: { 67 | /* Base URL to use in actions like `await page.goto('/')`. */ 68 | baseURL: url, 69 | 70 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 71 | trace: 'on-first-retry', 72 | }, 73 | 74 | /* Which browsers to run the tests in */ 75 | projects: IS_CI || ALL_BROWSERS ? all_browsers : localBrowsers, 76 | 77 | /* Run local dev server before starting the tests */ 78 | webServer: { 79 | command: 'pnpm run dev', 80 | url, 81 | reuseExistingServer: !IS_CI, 82 | }, 83 | }); 84 | -------------------------------------------------------------------------------- /scripts/upload_to_local_deployment.ts: -------------------------------------------------------------------------------- 1 | import { JsonLdParser } from "jsonld-streaming-parser"; 2 | import { Readable } from "stream"; 3 | import readline from "readline"; 4 | import SerializerNtriples from "@rdfjs/serializer-ntriples"; 5 | import util from "util"; 6 | import axios from "axios"; 7 | import { readFileSync } from "fs"; 8 | 9 | const OXIGRAPH_STORE_URL = 10 | process.env["DM_DEV_OXIGRAPH_URL"] || "http://localhost:33378"; 11 | 12 | const contextObject = JSON.parse( 13 | readFileSync("docmaps-context.jsonld").toString(), 14 | ); 15 | 16 | const ONLY_DOCMAPS_LOADER = { 17 | load: (url: string) => { 18 | return Promise.resolve(contextObject); 19 | }, 20 | }; 21 | 22 | async function uploadNtriples(nt: Buffer): Promise { 23 | return axios.post(`${OXIGRAPH_STORE_URL}/store?default`, nt, { 24 | headers: { 25 | "Content-Type": "application/n-triples", 26 | "Content-Length": Buffer.byteLength(nt), 27 | }, 28 | }); 29 | } 30 | 31 | const waiters: Promise[] = []; 32 | 33 | const rl = readline.createInterface({ 34 | input: process.stdin, 35 | // output: process.stdout, 36 | }); 37 | 38 | rl.on("line", (line) => { 39 | const parseAndUpload = new Promise((res, _rej) => { 40 | console.log(`>> Parsing this json line: ${line}`); 41 | const s = new Readable({ 42 | read: () => { 43 | s.push(line); 44 | s.push(null); 45 | }, 46 | }); 47 | 48 | const jsonld = new JsonLdParser({ documentLoader: ONLY_DOCMAPS_LOADER }); 49 | const nt = new SerializerNtriples(); 50 | let nt_str = ""; 51 | 52 | // read the input from stdin as json-ld streaming parser 53 | const p1 = jsonld.import(s); 54 | 55 | p1.on("data", (d) => { 56 | console.log(d); 57 | }); 58 | 59 | // stream output to nt 60 | const output = nt.import(p1); 61 | output.on("data", (d) => { 62 | nt_str += d.toString(); 63 | }); 64 | 65 | output.on("end", (_) => { 66 | console.log(`>>> to upload: ${nt_str}`); 67 | res(nt_str); 68 | }); 69 | }).then(async (str) => { 70 | const result = await uploadNtriples(Buffer.from(str)); 71 | console.log( 72 | `Uploading finished with response: ${util.inspect( 73 | { 74 | status: result.status, 75 | statusText: result.statusText, 76 | headers: result.headers, 77 | }, 78 | { depth: 1 }, 79 | )}`, 80 | ); 81 | }); 82 | 83 | waiters.push(parseAndUpload); 84 | }); 85 | 86 | rl.once("close", () => { 87 | // end of input 88 | }); 89 | 90 | await Promise.all(waiters); 91 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release all packages with semantic release 2 | 3 | permissions: 4 | actions: read 5 | checks: read 6 | contents: write 7 | packages: write 8 | pages: write 9 | id-token: write 10 | 11 | on: 12 | push: 13 | branches: 14 | - main 15 | 16 | env: 17 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 18 | 19 | jobs: 20 | test-sdk: 21 | uses: ./.github/workflows/sdk-tests.yaml 22 | test-http-server: 23 | uses: ./.github/workflows/http-server-tests.yaml 24 | test-etl: 25 | uses: ./.github/workflows/etl-tests.yaml 26 | test-example: 27 | uses: ./.github/workflows/example-tests.yaml 28 | test-specification: 29 | uses: ./.github/workflows/specification-tests.yaml 30 | nodejs_release: 31 | needs: 32 | - test-sdk 33 | - test-etl 34 | - test-http-server 35 | - test-specification 36 | - test-example 37 | 38 | runs-on: ubuntu-latest 39 | 40 | strategy: 41 | matrix: 42 | node-version: [18.14.0] 43 | 44 | steps: 45 | - uses: actions/checkout@v4 46 | 47 | - name: Use Node.js ${{ matrix.node-version }} 48 | uses: actions/setup-node@v4 49 | with: 50 | node-version: ${{ matrix.node-version }} 51 | 52 | - uses: pnpm/action-setup@v2 53 | name: Install pnpm 54 | id: pnpm-install 55 | with: 56 | version: 8 57 | run_install: false 58 | 59 | - name: Get pnpm store directory 60 | id: pnpm-cache 61 | shell: bash 62 | run: | 63 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 64 | 65 | - uses: actions/cache@v3 66 | name: Setup pnpm cache 67 | with: 68 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 69 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }} 70 | restore-keys: | 71 | ${{ runner.os }}-pnpm-store- 72 | 73 | - name: Install dependencies 74 | run: | 75 | pnpm install 76 | - name: Build everything 77 | run: | 78 | pnpm run -r build 79 | 80 | - name: Release 81 | env: 82 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 83 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 84 | # must ignore spa because we need spa in the workspace file for development, but spa is not released. 85 | run: npx multi-semantic-release --ignore-packages=packages/spa,packages/example,packages/http-server,packages/build-configs 86 | 87 | release-github-pages-docs: 88 | uses: ./.github/workflows/gh-pages.yaml 89 | -------------------------------------------------------------------------------- /packages/sdk/src/util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 0.1.0 3 | */ 4 | import * as t from 'io-ts' 5 | import { pipe } from 'fp-ts/lib/function' 6 | import { chain } from 'fp-ts/lib/Either' 7 | import { fromNullable } from 'io-ts-types' 8 | 9 | /** 10 | * Interface for a codec that parses a string and returns a URL object. 11 | * 12 | * @since 0.1.0 13 | */ 14 | export type UrlFromStringC = t.Type 15 | 16 | /** 17 | * Implementation of URL from String parser 18 | * 19 | * based on example there: 20 | * https://github.com/gcanti/io-ts-types/blob/master/src/BooleanFromString.ts 21 | * 22 | * @since 0.1.0 23 | */ 24 | export const UrlFromString: UrlFromStringC = new t.Type( 25 | 'UrlFromString', 26 | (u): u is URL => u instanceof URL, 27 | (u, c) => 28 | pipe( 29 | t.string.validate(u, c), 30 | chain((s) => { 31 | try { 32 | const url = new URL(s) 33 | return t.success(url) 34 | } catch (e) { 35 | return t.failure(u, c) 36 | } 37 | }), 38 | ), 39 | String, 40 | ) 41 | 42 | /** 43 | * Should be the same as a URL. 44 | * 45 | * @since 0.1.0 46 | */ 47 | export type UrlT = t.TypeOf 48 | 49 | /** 50 | * Interface for a Date from Anything 51 | * 52 | * based on example there: 53 | * https://github.com/gcanti/io-ts/blob/master/index.md#custom-types 54 | * 55 | * @since 0.9.0 56 | */ 57 | export type DateFromUnknownC = t.Type 58 | 59 | /** 60 | * Date from Date,Number,orString 61 | * 62 | * based on example there: 63 | * https://github.com/gcanti/io-ts/blob/master/index.md#custom-types 64 | * 65 | * @since 0.9.0 66 | */ 67 | export const DateFromUnknown: DateFromUnknownC = new t.Type( 68 | 'DateFromUnknown', 69 | (input: unknown): input is Date => input instanceof Date, 70 | (input, context) => { 71 | if (typeof input === 'string' || typeof input === 'number' || input instanceof Date) { 72 | const date = new Date(input) 73 | if (!isNaN(date.getTime())) { 74 | return t.success(date) 75 | } 76 | } 77 | return t.failure(input, context, 'Invalid date-like input') 78 | }, 79 | (date: Date) => date.toISOString(), 80 | ) 81 | 82 | /** 83 | * Function that takes a Codec and returns an "optional array" codec. 84 | * 85 | * It fails-over to returning an empty array if the input is null or absent, 86 | * but still errors if input is present and non matching. 87 | * 88 | * @since 0.11.0 89 | */ 90 | export function ArrayUpsertedEmpty(item: A) { 91 | return fromNullable(t.array(item), []) 92 | } 93 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yaml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # do deploy to github pages on merge to main 6 | push: 7 | branches: 8 | - main 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | workflow_call: 12 | 13 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 14 | permissions: 15 | contents: read 16 | pages: write 17 | id-token: write 18 | 19 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 20 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 21 | concurrency: 22 | group: "pages" 23 | cancel-in-progress: false 24 | 25 | jobs: 26 | # Single deploy job since we're just deploying 27 | deploy: 28 | 29 | environment: 30 | name: github-pages 31 | url: ${{ steps.deployment.outputs.page_url }} 32 | 33 | runs-on: ubuntu-latest 34 | 35 | strategy: 36 | matrix: 37 | node-version: [18.14.0] 38 | 39 | steps: 40 | - name: Checkout 41 | uses: actions/checkout@v4 42 | 43 | - name: Use Node.js ${{ matrix.node-version }} 44 | uses: actions/setup-node@v4 45 | with: 46 | node-version: ${{ matrix.node-version }} 47 | 48 | - uses: pnpm/action-setup@v2 49 | name: Install pnpm 50 | id: pnpm-install 51 | with: 52 | version: 8 53 | run_install: false 54 | 55 | - name: Setup Pages 56 | uses: actions/configure-pages@v4 57 | 58 | - name: Install dependencies 59 | run: | 60 | pnpm install; 61 | 62 | - name: Generate docs 63 | run: | 64 | cd packages/sdk ; 65 | pnpm docs:generate; 66 | 67 | - name: Build SPA 68 | run: | 69 | cd packages/spa ; 70 | pnpm run build:deps ; 71 | pnpm run build ; 72 | 73 | - name: Convert symlinks to hard links 74 | # adapted from https://superuser.com/questions/560597/convert-symlinks-to-hard-links 75 | # we use symlinks for local dev & because they are committable to repo, 76 | # but hard links are needed for building tape archive to upload 77 | run: | 78 | cd docs ; 79 | ../scripts/link-docs.sh ; 80 | 81 | - name: Upload artifact 82 | uses: actions/upload-pages-artifact@v2 83 | with: 84 | # Upload subdirectory 85 | path: './docs' 86 | - name: Deploy to GitHub Pages 87 | id: deployment 88 | uses: actions/deploy-pages@v3 89 | -------------------------------------------------------------------------------- /packages/example/docmap.jsonld: -------------------------------------------------------------------------------- 1 | {"type":"docmap","id":"https://docmaps-project.github.io/ex/docmap_for/10.5194/wes-2022-23","publisher":{},"created":"2023-07-16T15:30:51.889Z","updated":"2023-07-16T15:30:51.889Z","first-step":"_:b0","steps":{"_:b0":{"inputs":[],"actions":[{"participants":[{"actor":{"type":"person","name":"Potentier, Thomas"},"role":"author"},{"actor":{"type":"person","name":"Guilmineau, Emmanuel"},"role":"author"},{"actor":{"type":"person","name":"Finez, Arthur"},"role":"author"},{"actor":{"type":"person","name":"Le Bourdat, Colin"},"role":"author"},{"actor":{"type":"person","name":"Braud, Caroline"},"role":"author"}],"outputs":[{"published":"2022-03-08T00:00:00.000Z","doi":"10.5194/wes-2022-23","type":"preprint"}]}],"assertions":[{"status":"catalogued","item":"10.5194/wes-2022-23"}],"next-step":"_:b1"},"_:b1":{"actions":[{"participants":[],"outputs":[{"published":"2022-05-27T00:00:00.000Z","doi":"10.5194/wes-2022-23-rc1","type":"review"}]},{"participants":[],"outputs":[{"published":"2022-05-29T00:00:00.000Z","doi":"10.5194/wes-2022-23-rc2","type":"review"}]}],"inputs":[{"published":"2022-03-08T00:00:00.000Z","doi":"10.5194/wes-2022-23","type":"preprint"}],"assertions":[{"status":"reviewed","item":"10.5194/wes-2022-23"}],"previous-step":"_:b0","next-step":"_:b2"},"_:b2":{"inputs":[{"published":"2022-03-08T00:00:00.000Z","doi":"10.5194/wes-2022-23","type":"preprint"}],"actions":[{"participants":[{"actor":{"type":"person","name":"Potentier, Thomas"},"role":"author"},{"actor":{"type":"person","name":"Guilmineau, Emmanuel"},"role":"author"},{"actor":{"type":"person","name":"Finez, Arthur"},"role":"author"},{"actor":{"type":"person","name":"Le Bourdat, Colin"},"role":"author"},{"actor":{"type":"person","name":"Braud, Caroline"},"role":"author"}],"outputs":[{"published":"2022-08-30T00:00:00.000Z","doi":"10.5194/wes-7-1771-2022","type":"journal-article"}]}],"assertions":[{"status":"published","item":"10.5194/wes-7-1771-2022"}],"previous-step":"_:b1","next-step":"_:b3"},"_:b3":{"actions":[{"participants":[],"outputs":[{"published":"2022-05-27T00:00:00.000Z","doi":"10.5194/wes-2022-23-rc1","type":"review"}]},{"participants":[{"actor":{"type":"person","name":"Potentier, Thomas"},"role":"author"}],"outputs":[{"published":"2022-06-14T00:00:00.000Z","doi":"10.5194/wes-2022-23-ac1","type":"review"}]},{"participants":[],"outputs":[{"published":"2022-05-29T00:00:00.000Z","doi":"10.5194/wes-2022-23-rc2","type":"review"}]},{"participants":[{"actor":{"type":"person","name":"Potentier, Thomas"},"role":"author"}],"outputs":[{"published":"2022-06-14T00:00:00.000Z","doi":"10.5194/wes-2022-23-ac2","type":"review"}]}],"inputs":[{"published":"2022-08-30T00:00:00.000Z","doi":"10.5194/wes-7-1771-2022","type":"journal-article"}],"assertions":[{"status":"reviewed","item":"10.5194/wes-7-1771-2022"}],"previous-step":"_:b2"}},"@context":"https://w3id.org/docmaps/context.jsonld"} 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | # direnv 133 | .envrc 134 | **/.DS_Store 135 | 136 | # IntelliJ files 137 | .idea/**/* 138 | 139 | **/tmp 140 | -------------------------------------------------------------------------------- /packages/spa/README.md: -------------------------------------------------------------------------------- 1 | Docmap Visual Playground 2 | 3 | This Docmap visualizer is a simple stateless single-page web app that allows you to visualize a docmap based on our Crossref-to-Docmap ETL library. You can plug in any DOI, and if Crossref knows about it and it has reviews or a preprint, you'll get some interesting content back. 4 | 5 | ## Demo 6 | 7 | You can try the live demo hosted on GitHub Pages [here](https://docmaps-project.github.io/docmaps/demo/index.html). 8 | 9 | ## Getting Started for Development 10 | 11 | ### Prerequisites 12 | 13 | - Node.js (>= 12.x) 14 | - pnpm package manager 15 | 16 | ### Installation 17 | 18 | 1. Clone the repository: 19 | 20 | ```bash 21 | git clone https://github.com/docmaps-project/docmaps 22 | cd docmaps/packages/spa 23 | ``` 24 | 25 | 2. Install dependencies: 26 | 27 | ```bash 28 | pnpm install 29 | ``` 30 | 31 | ### Running the tests 32 | 33 | To run tests: 34 | 35 | ```bash 36 | pnpm test 37 | ``` 38 | 39 | Note that you need an installation of Chrome, and must set 40 | the `CHROME_PATH` variable. If you use Chromium, it might be something like 41 | `/usr/local/bin/chromium`. 42 | 43 | ### Running the App 44 | 45 | To start the development server: 46 | 47 | ```bash 48 | pnpm run dev 49 | ``` 50 | 51 | Then navigate to `http://localhost:8080` in your browser. 52 | 53 | ### Deploying to GH Pages 54 | 55 | The Github Pages is deployed from the `gh-pages` workflow in repository root. This workflow is called 56 | by the `release` workflow and is only triggered on merge to main. It first builds/bundles this package, 57 | then deploys the bundle. Note that because the repo name disagrees with the package name, the `index.html` 58 | is modified to use relative paths for all bundled resources. 59 | 60 | ## Built With 61 | 62 | - [Svelte](https://svelte.dev/) - The web framework used 63 | - [render-rev](github.com/source-data/render-rev) - Display component built by EMBO 64 | - [pnpm](https://pnpm.io/) - The package manager 65 | 66 | ## Contributing 67 | 68 | See the main repository contributing guidelines. 69 | 70 | ## License 71 | 72 | See main repository license. 73 | 74 | ## Acknowledgments 75 | 76 | - [render-rev](github.com/source-data/render-rev) 77 | - [Svelte](https://svelte.dev/) 78 | - [pnpm](https://pnpm.io/) 79 | - [crossref](https://crossref.org/) 80 | 81 | ## Screenshots 82 | 83 | [DocMap for Posted Preprint](screenshots/preprint_posted.png) 84 | 85 | [DocMap for Published Preprint](screenshots/preprint_published.png) 86 | 87 | [DocMap for Published/Reviewed Preprint (note linear issues)](screenshots/preprint_published_reviewed.png) 88 | 89 | [DocMap for Refereed Preprint](screenshots/preprint_refereed.png) -------------------------------------------------------------------------------- /packages/http-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@docmaps/http-server", 3 | "version": "0.1.0", 4 | "description": "Node.js runtime for serving docmaps using other composed docmaps-related npm modules", 5 | "type": "module", 6 | "main": "dist/httpserver/main.js", 7 | "scripts": { 8 | "test": "ava", 9 | "clean": "rm -rf dist/", 10 | "test:integration": "ava test/integration/", 11 | "test:cleanup": "docker compose -f test/integration/assets/docker-compose.yml down", 12 | "test:unit": "ava test/unit/", 13 | "compose:repave": "rm -rf tmp/oxigraph_data/*", 14 | "compose:up": "docker compose -f $INIT_CWD/docker-compose.local.yml up --build", 15 | "lint": "npx eslint .", 16 | "lint:fix": "npx eslint --fix .", 17 | "start": "tsx dist/httpserver/main.js", 18 | "build:deps": "pnpm run --filter=@docmaps/http-server^... build", 19 | "build": "tsc --declaration" 20 | }, 21 | "keywords": [], 22 | "author": "eve github.com/ships", 23 | "license": "ISC", 24 | "files": [ 25 | "dist/", 26 | "README.md", 27 | "package.json", 28 | "tsconfig.json" 29 | ], 30 | "dependencies": { 31 | "@commander-js/extra-typings": "^11.0.0", 32 | "@docmaps/http-client": "workspace:^0.1.0", 33 | "@rdfjs/data-model": "^2.0.1", 34 | "@rdfjs/namespace": "^2.0.0", 35 | "@tpluscode/sparql-builder": "^1.1.0", 36 | "@ts-rest/core": "^3.30.2", 37 | "@ts-rest/express": "^3.30.2", 38 | "@zazuko/rdf-vocabularies": "^2023.1.19", 39 | "commander": "^11.0.0", 40 | "cors": "^2.8.5", 41 | "@docmaps/sdk": "workspace:^0.0.0", 42 | "express": "^4.18.2", 43 | "fetch-sparql-endpoint": "^4.0.0", 44 | "fp-ts": "^2.14.0", 45 | "isomorphic-fetch": "^3.0.0", 46 | "n3": "^1.17.1", 47 | "oxigraph": "^0.3.19", 48 | "pino": "^8.16.1", 49 | "pino-http": "^8.5.1", 50 | "streaming-iterables": "^8.0.0", 51 | "tsx": "^4.0.0" 52 | }, 53 | "devDependencies": { 54 | "@docmaps/build-configs": "workspace:^", 55 | "@rdfjs/types": "^1.1.0", 56 | "@types/cors": "^2.8.14", 57 | "@types/express": "^4.17.17", 58 | "@types/isomorphic-fetch": "^0.0.39", 59 | "@types/n3": "^1.16.0", 60 | "@types/node": "^20.0.0", 61 | "@types/rdfjs__data-model": "^2.0.4", 62 | "@types/rdfjs__namespace": "^2.0.5", 63 | "@typescript-eslint/eslint-plugin": "^6.0.0", 64 | "@typescript-eslint/parser": "^6.0.0", 65 | "ava": "^5.2.0", 66 | "eslint": "^8.39.0", 67 | "eslint-config-prettier": "^9.0.0", 68 | "eslint-plugin-prettier": "^5.0.0", 69 | "prettier": "^3.0.0", 70 | "ts-mockito": "^2.6.1", 71 | "typescript": "^5.2.2", 72 | "zod": "^3.22.2" 73 | }, 74 | "ava": { 75 | "extensions": { 76 | "ts": "module" 77 | }, 78 | "nodeArguments": [ 79 | "--loader=tsx/esm" 80 | ], 81 | "files": [ 82 | "**/*.test.ts" 83 | ] 84 | }, 85 | "engines": { 86 | "node": ">=18.14.0" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /packages/etl/test/unit/crossref/functions.test.ts: -------------------------------------------------------------------------------- 1 | import test, { ExecutionContext } from 'ava' 2 | import * as cm from '../__fixtures__/crossref' 3 | import * as E from 'fp-ts/lib/Either' 4 | import { inspect } from 'util' 5 | import * as F from '../../../src/plugins/crossref/functions' 6 | import type { ActionT } from '@docmaps/sdk' 7 | 8 | function rightOrInspectError( 9 | t: ExecutionContext, 10 | f: (m: T) => void, 11 | e: E.Either, 12 | ): boolean { 13 | if (E.isLeft(e)) { 14 | t.fail(`got unexpected error: ${inspect(e, { depth: 8 })}`) 15 | return false 16 | } 17 | 18 | f(e.right) 19 | return true 20 | } 21 | 22 | test('decodeActionForWork: type mappings: posted-content -> preprint', async (t) => { 23 | const result = F.decodeActionForWork({ 24 | ...cm.GENERIC_WORK_DATA, 25 | DOI: 'mock.decodeAction', 26 | prefix: 'mock', 27 | type: 'posted-content', 28 | URL: 'none', 29 | author: [], 30 | }) 31 | 32 | t.true( 33 | rightOrInspectError( 34 | t, 35 | (action: ActionT) => { 36 | t.is(action.outputs[0]?.doi, 'mock.decodeAction') 37 | t.is(action.outputs[0]?.type, 'preprint') 38 | t.deepEqual(action.participants, []) 39 | }, 40 | result, 41 | ), 42 | ) 43 | }) 44 | 45 | test('decodeActionForWork: no authors', async (t) => { 46 | const result = F.decodeActionForWork({ 47 | ...cm.GENERIC_WORK_DATA, 48 | DOI: 'mock.decodeAction', 49 | prefix: 'mock', 50 | type: 'preprint', 51 | URL: 'none', 52 | author: [], 53 | }) 54 | 55 | t.true( 56 | rightOrInspectError( 57 | t, 58 | (action: ActionT) => { 59 | t.is(action.outputs[0]?.doi, 'mock.decodeAction') 60 | t.deepEqual(action.participants, []) 61 | }, 62 | result, 63 | ), 64 | ) 65 | }) 66 | 67 | test('decodeActionForWork: with authors', async (t) => { 68 | const result = F.decodeActionForWork({ 69 | ...cm.GENERIC_WORK_DATA, 70 | DOI: 'mock.decodeAction', 71 | prefix: 'mock', 72 | URL: 'none', 73 | type: 'preprint', 74 | author: [ 75 | { family: 'name', affiliation: [], sequence: 'mockseq' }, 76 | { family: 'fam', given: 'first', affiliation: [], sequence: 'mockseq' }, 77 | ], 78 | }) 79 | 80 | t.true( 81 | rightOrInspectError( 82 | t, 83 | (action: ActionT) => { 84 | t.is(action.outputs[0]?.doi, 'mock.decodeAction') 85 | t.is(action.participants[0]?.role, 'author') 86 | t.is(action.participants[1]?.role, 'author') 87 | t.deepEqual(action.participants[0]?.actor, { 88 | name: 'name', 89 | type: 'person', 90 | }) 91 | t.deepEqual(action.participants[1]?.actor, { 92 | name: 'fam, first', 93 | type: 'person', 94 | }) 95 | }, 96 | result, 97 | ), 98 | ) 99 | }) 100 | -------------------------------------------------------------------------------- /packages/etl/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to docmaps ts-etl 2 | 3 | We welcome contributions from anyone interested in improving the docmaps project! Before you get started, please read through the guidelines below to ensure that your contributions are effective and useful. 4 | 5 | ## Workflow 6 | 1. Fork the repository and clone it locally, or create a branch. 7 | 2. [Recommended] Install pnpm if you haven't already: `npm install -g pnpm` 8 | 3. Run pnpm install in the package directory to install all dependencies for the project. 9 | 4. Add, commit, and push your changes to your fork/branch. 10 | 5. Submit a pull request (PR) to the main branch of the `docmaps-project/docmaps` repository. 11 | 12 | ## Contributing Guidelines 13 | 1. Follow the code of conduct. 14 | 2. Before starting any work, make sure to check the issues and pull requests to see if your contribution has already been discussed or implemented. 15 | 3. If you are working on a new feature or bug fix, create a new issue to discuss it with the maintainers and other contributors. 16 | 4. Before submitting a PR, make sure your code is properly formatted, tested, and documented. 17 | 5. Make sure your commit messages are descriptive and follow the conventional commit format (imperative tense). Your PR will be merged with a squash. 18 | 19 | Write new tests to cover any new functionality or bug fixes. 20 | 21 | ## Code Review 22 | All PRs will be reviewed by at least one maintainer or contributor. 23 | Reviewers may request changes or ask for clarifications on the PR. 24 | Once the changes have been made, the PR will be merged by a maintainer or contributor. 25 | 26 | ## Local development 27 | 28 | [`nvm`](https://github.com/nvm-sh/nvm) is a good local Node version manager. 29 | 30 | ``` 31 | nvm use 18.14.0 32 | ``` 33 | 34 | I recommend you use `pnpm` for best performance. Alternatively you can use `npm`. 35 | 36 | ```bash 37 | pnpm install 38 | pnpm test && pnpm build 39 | ``` 40 | 41 | If these exit zero, you're good to get started with your changes. 42 | 43 | ## Tests 44 | 45 | Tests are written BDD-style. You should make meaningful assertions that cover 46 | any new complex logic. You don't have to cover every possible case. It is recommended 47 | to follow the red-green-refactor pattern by writing tests first. As a rule of thumb, 48 | if your code change can be reverted while leaving your test changes in place, and the 49 | suites still pass, your test coverage or specificity should be increased. 50 | 51 | **Hanging tests.** 52 | Test are run using [AVA](https://github.com/avajs/ava). This has much smaller dependency footprint than Jest. 53 | However it runs `tsc` in a hidden way such that if compilation fails, you will get `Timed out while running tests` 54 | rather than a useful error. Diagnose this issue by running `pnpm build` yourself to get a better error message. 55 | 56 | Every PR is validated by a Github Actions workflow for EVERY package in the repo, not just the 57 | one you are developing on. 58 | 59 | Thanks for contributing! 60 | -------------------------------------------------------------------------------- /packages/sdk/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to @docmaps/sdk 2 | 3 | We welcome contributions from anyone interested in improving [Project Name]! Before you get started, please read through the guidelines below to ensure that your contributions are effective and useful. 4 | 5 | ## Workflow 6 | 1. Fork the repository and clone it locally, or create a branch. 7 | 2. [Recommended] Install pnpm if you haven't already: `npm install -g pnpm@^8`. Version 8+ is required. 8 | 3. Run pnpm install in the package directory to install all dependencies for the project. 9 | 4. Add, commit, and push your changes to your fork/branch. 10 | 5. Submit a pull request (PR) to the main branch of the `docmaps-project/docmaps` repository. 11 | 12 | ## Contributing Guidelines 13 | 1. Follow the code of conduct. 14 | 2. Before starting any work, make sure to check the issues and pull requests to see if your contribution has already been discussed or implemented. 15 | 3. If you are working on a new feature or bug fix, create a new issue to discuss it with the maintainers and other contributors. 16 | 4. Before submitting a PR, make sure your code is properly formatted, tested, and documented. 17 | 5. Make sure your commit messages are descriptive and follow the conventional commit format (imperative tense). Your PR will be merged with a squash. 18 | 19 | Write new tests to cover any new functionality or bug fixes. 20 | 21 | ## Code Review 22 | All PRs will be reviewed by at least one maintainer or contributor. 23 | Reviewers may request changes or ask for clarifications on the PR. 24 | Once the changes have been made, the PR will be merged by a maintainer or contributor. 25 | 26 | ## Local development 27 | 28 | [`nvm`](https://github.com/nvm-sh/nvm) is a good local Node version manager. 29 | 30 | ``` 31 | nvm use 18.14.0 32 | ``` 33 | 34 | I recommend you use `pnpm` for best performance. Alternatively you can use `npm`. 35 | 36 | ```bash 37 | pnpm install 38 | pnpm test && pnpm build 39 | ``` 40 | 41 | If these exit zero, you're good to get started with your changes. 42 | 43 | ## Tests 44 | 45 | Tests are written BDD-style. You should make meaningful assertions that cover 46 | any new complex logic. You don't have to cover every possible case. It is recommended 47 | to follow the red-green-refactor pattern by writing tests first. As a rule of thumb, 48 | if your code change can be reverted while leaving your test changes in place, and the 49 | suites still pass, your test coverage or specificity should be increased. 50 | 51 | **Hanging tests.** 52 | Test are run using [AVA](https://github.com/avajs/ava). This has much smaller dependency footprint than Jest. 53 | However it runs `tsc` in a hidden way such that if compilation fails, you will get `Timed out while running tests` 54 | rather than a useful error. Diagnose this issue by running `pnpm build` yourself to get a better error message. 55 | 56 | Every PR is validated by a Github Actions workflow for EVERY package in the repo, not just the 57 | one you are developing on. 58 | 59 | Thanks for contributing! 60 | -------------------------------------------------------------------------------- /packages/sdk/src/test/types.test.ts: -------------------------------------------------------------------------------- 1 | import test, { ExecutionContext } from 'ava' 2 | import { PartialExamples as ex } from './__fixtures__' 3 | import * as dm from '../types' 4 | import * as E from 'fp-ts/lib/Either' 5 | import * as util from 'util' 6 | import { pipe } from 'fp-ts/lib/function' 7 | 8 | function isRightArray( 9 | t: ExecutionContext, 10 | arr: E.Either[], 11 | len: number, 12 | proc?: (_a: readonly T[]) => void, 13 | ) { 14 | pipe( 15 | arr, 16 | E.sequenceArray, 17 | E.mapLeft((e) => t.fail(`Error parsing: ${util.inspect(e, { depth: 1 })}`)), 18 | //eslint-disable-next-line 19 | E.map(proc || (() => {})), 20 | ) 21 | 22 | t.is(arr.length, len) 23 | } 24 | 25 | test('Codec parsing OnlineAccount', (t) => { 26 | const v = ex.elife.OnlineAccount.flatMap((x) => { 27 | return dm.OnlineAccount.decode(x) 28 | }) 29 | isRightArray(t, v, 3) 30 | }) 31 | 32 | test('Codec parsing Publisher', (t) => { 33 | const v = ex.elife.Publisher.flatMap((x) => { 34 | return dm.Publisher.decode(x) 35 | }) 36 | isRightArray(t, v, 4) 37 | }) 38 | 39 | test('Codec parsing Manifestation', (t) => { 40 | const v = ex.elife.Manifestation.flatMap((x) => { 41 | return dm.Manifestation.decode(x) 42 | }) 43 | isRightArray(t, v, 42) 44 | }) 45 | 46 | test('Codec parsing Thing', (t) => { 47 | const v = ex.elife.Thing.flatMap((x) => { 48 | return dm.Thing.decode(x) 49 | }) 50 | isRightArray(t, v, 18) 51 | }) 52 | 53 | test('Codec parsing RoleInTime', (t) => { 54 | const v = ex.elife.RoleInTime.flatMap((x) => { 55 | return dm.RoleInTime.decode(x) 56 | }) 57 | isRightArray(t, v, 19) 58 | }) 59 | 60 | test('Codec parsing Actor', (t) => { 61 | const v = ex.elife.Actor.flatMap((x) => { 62 | return dm.Actor.decode(x) 63 | }) 64 | isRightArray(t, v, 19) 65 | }) 66 | 67 | test('Codec parsing Action', (t) => { 68 | const v = ex.elife.Action.flatMap((x) => { 69 | return dm.Action.decode(x) 70 | }) 71 | isRightArray(t, v, 18) 72 | 73 | // assert that the steps->actiuons->inputs were all found 74 | t.is(ex.elife.ActionInputs.length, 3) 75 | }) 76 | 77 | test('Codec parsing Assertion', (t) => { 78 | const v = ex.elife.Assertion.flatMap((x) => { 79 | return dm.Assertion.decode(x) 80 | }) 81 | isRightArray(t, v, 10) 82 | }) 83 | 84 | test('Codec parsing Step', (t) => { 85 | const v = ex.elife.Step.flatMap((x) => { 86 | return dm.Step.decode(x) 87 | }) 88 | isRightArray(t, v, 9) 89 | }) 90 | 91 | test('Codec parsing Docmap', (t) => { 92 | const v = ex.elife.Docmap.flatMap((x) => { 93 | return dm.Docmap.decode(x) 94 | }) 95 | isRightArray(t, v, 4, (arr) => { 96 | t.deepEqual(arr[0]?.['@context'], 'https://w3id.org/docmaps/context.jsonld') 97 | }) 98 | }) 99 | 100 | test('Codec inserts missing @context key for Docmap', (t) => { 101 | const v = ex.elife.Docmap.flatMap((x) => { 102 | const { ['@context']: _, ...stripped } = x 103 | return dm.Docmap.decode(stripped) 104 | }) 105 | isRightArray(t, v, 4, (arr) => { 106 | t.deepEqual(arr[0]?.['@context'], 'https://w3id.org/docmaps/context.jsonld') 107 | }) 108 | }) 109 | -------------------------------------------------------------------------------- /packages/etl/src/plugins/crossref/functions.ts: -------------------------------------------------------------------------------- 1 | import { Work, DatemorphISOString } from 'crossref-openapi-client-ts' 2 | import * as E from 'fp-ts/lib/Either' 3 | import * as A from 'fp-ts/lib/Array' 4 | import { pipe } from 'fp-ts/lib/function' 5 | import * as D from '@docmaps/sdk' 6 | import { mapLeftToUnknownError, nameForAuthor } from '../../utils' 7 | import { Eq } from 'fp-ts/lib/string' 8 | 9 | /** 10 | * Mappings from strings used in Crossref that are not in Docmaps semantics. 11 | * 12 | * see comments for individual elements. 13 | */ 14 | const typeOverrides: Record = { 15 | /* 16 | * So far we have only found `has-preprint` pointing to 17 | * objects of type `posted-content`. Unclear if this is 18 | * a bijective relationship. TODO: take note if we discover 19 | * cases where we wrongly mark things as Preprints because of this. 20 | */ 21 | 'posted-content': 'preprint', 22 | 23 | /* 24 | * We chose our terminology out of the FaBio ontologies, In these ontologies 25 | * the review object does not concern itself with whether the review is a 26 | * Peer Review or not. That information might appear in where we mark 27 | * the Acquired Status as `peer-reviewed` or similar, in `assertions`. 28 | */ 29 | 'peer-review': 'review', 30 | } 31 | 32 | function overrideContentType(contentType: string): string { 33 | return typeOverrides[contentType] || contentType 34 | } 35 | 36 | export function thingForCrossrefWork(work: Work) { 37 | return { 38 | // TODO: should we include arbitrary keys? make that parametric? 39 | // ...work, 40 | // FIXME: is this possibly fake news? should it fail instead if no published date? 41 | published: DatemorphISOString(work.published || work.created), 42 | doi: work.DOI, 43 | type: overrideContentType(work.type), 44 | // TODO: other fields we ignore: id, content 45 | } 46 | } 47 | 48 | export function decodeActionForWork(work: Work): E.Either { 49 | return pipe( 50 | E.Do, 51 | // prepare the Thing which will be output 52 | E.bind('wo', () => pipe(work, thingForCrossrefWork, E.right)), 53 | // prepare the Authors 54 | E.bind('wa', () => 55 | pipe( 56 | work.author || [], 57 | A.map((a) => ({ 58 | type: 'person', 59 | name: nameForAuthor(a), 60 | })), 61 | E.traverseArray((a) => 62 | pipe(a, D.Actor.decode, mapLeftToUnknownError('decoding actor in decodeActionForWork')), 63 | ), 64 | E.map((auths) => 65 | auths.map((a) => ({ 66 | actor: a, 67 | role: 'author', 68 | })), 69 | ), 70 | ), 71 | ), 72 | // construct and decode the Action 73 | E.chain(({ wo, wa }) => 74 | pipe( 75 | { 76 | participants: wa, 77 | outputs: [wo], 78 | }, 79 | D.Action.decode, 80 | mapLeftToUnknownError('decoding action in decodeActionForWork'), 81 | ), 82 | ), 83 | ) 84 | } 85 | 86 | export function relatedDoisForWork(w: Work, relation: string): string[] { 87 | const reviews = w.relation?.[relation] 88 | if (!reviews) { 89 | return [] 90 | } 91 | 92 | return pipe( 93 | reviews, 94 | // get unique IDs 95 | A.map((wre) => wre.id), 96 | A.uniq(Eq), 97 | ) 98 | } 99 | -------------------------------------------------------------------------------- /packages/http-server/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | # Comments are provided throughout this file to help you get started. 4 | # If you need more help, visit the Dockerfile reference guide at 5 | # https://docs.docker.com/engine/reference/builder/ 6 | 7 | # NOTE: due to use of pnpm workspace, this dockerfile needs to be built 8 | # from the repository root context. 9 | 10 | ARG NODE_VERSION=18.14.0 11 | ARG PNPM_VERSION=8.7.6 12 | 13 | ################################################################################ 14 | # Use node image for base image for all stages. 15 | FROM node:${NODE_VERSION}-alpine as base 16 | 17 | ENV PNPM_HOME="/pnpm" 18 | ENV PATH="$PNPM_HOME:$PATH" 19 | RUN corepack enable 20 | 21 | # Set working directory for all build stages. 22 | COPY . /app 23 | WORKDIR /app 24 | 25 | ################################################################################ 26 | # Create a stage for building the application. 27 | FROM base AS build 28 | # Download additional development dependencies before building, as some projects require 29 | # "devDependencies" to be installed to build. If you don't need this, remove this step. 30 | RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ 31 | pnpm install \ 32 | --filter "@docmaps/http-server" \ 33 | --frozen-lockfile 34 | 35 | # Build the dependencies from this workspace 36 | RUN pnpm --filter @docmaps/sdk --filter @docmaps/http-client run build 37 | # Build the application 38 | RUN pnpm --filter @docmaps/http-server run build 39 | 40 | ################################################################################ 41 | # Download dependencies as a separate step to take advantage of Docker's caching. 42 | # Leverage a cache mount to /root/.local/share/pnpm/store to speed up subsequent builds. 43 | # Leverage bind mounts to package.json and pnpm-lock.yaml to avoid having to copy them 44 | # into this layer. 45 | FROM base AS prod 46 | 47 | RUN npm i -g typescript@4.9.5 48 | 49 | RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ 50 | pnpm install \ 51 | --filter "@docmaps/http-server" \ 52 | --prod \ 53 | --frozen-lockfile 54 | 55 | ################################################################################ 56 | # Create a new stage to run the application with minimal runtime dependencies 57 | # where the necessary files are copied from the build stage. 58 | 59 | # FIXME: build from Prod instead, once we have resolved the prepare script issue: 60 | # https://github.com/Docmaps-Project/docmaps/issues/118 61 | FROM prod AS runtime 62 | 63 | # Copy the production dependencies from the deps stage and also 64 | # the built application from the build stage into the image. 65 | COPY --from=build /app/packages/http-server/dist /app/packages/http-server/dist 66 | 67 | # Copy built sub-dependencies from this same package 68 | COPY --from=build /app/packages/http-client/dist /app/packages/http-server/node_modules/@docmaps/http-client/dist 69 | COPY --from=build /app/packages/sdk/dist /app/packages/http-server/node_modules/@docmaps/sdk/dist 70 | WORKDIR /app/packages/http-server 71 | 72 | # Use production node environment by default. 73 | ENV NODE_ENV production 74 | 75 | # Run the application as a non-root user. 76 | USER node 77 | 78 | # Expose the port that the application listens on. 79 | EXPOSE 8000 80 | 81 | # Run the application. 82 | ENTRYPOINT [ "pnpm", "start" ] 83 | -------------------------------------------------------------------------------- /packages/http-server/test/unit/api.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { SparqlAdapter } from '../../src/adapter' 3 | import { ApiInstance } from '../../src/api' 4 | import * as TE from 'fp-ts/lib/TaskEither' 5 | import * as D from '@docmaps/sdk' 6 | 7 | import { deepEqual, mock, instance, when } from 'ts-mockito' 8 | import { ThingSpec } from '../../src/types' 9 | 10 | // TODO: export this in shared module 11 | export function whenThenRight( 12 | functor: (input1: T) => TE.TaskEither, 13 | filter: T, 14 | response: V, 15 | ) { 16 | when(functor(deepEqual(filter))).thenReturn(TE.of(response)) 17 | } 18 | 19 | const TEST_DATE = new Date() 20 | 21 | test('info: default values', (t) => { 22 | const _adapter = instance(mock(SparqlAdapter)) 23 | const res = new ApiInstance(_adapter, new URL('https://example.com')).get_info() 24 | 25 | t.is(res.api_url, 'https://example.com/') 26 | t.deepEqual(res.ephemeral_document_expiry, { 27 | max_seconds: 60, 28 | max_retrievals: 1, 29 | }) 30 | t.deepEqual(res.peers, []) 31 | }) 32 | 33 | test('info: configured values', (t) => { 34 | const _adapter = instance(mock(SparqlAdapter)) 35 | const res = new ApiInstance( 36 | _adapter, 37 | new URL('https://example.com'), 38 | [new URL('https://other.com')], 39 | 30, 40 | 10, 41 | ).get_info() 42 | 43 | t.is(res.api_url, 'https://example.com/') 44 | t.deepEqual(res.ephemeral_document_expiry, { 45 | max_seconds: 30, 46 | max_retrievals: 10, 47 | }) 48 | t.deepEqual(res.peers, [{ api_url: 'https://other.com/' }]) 49 | }) 50 | 51 | test('docmap_by_id: consults the adapter', async (t) => { 52 | const adapterC = mock(SparqlAdapter) 53 | 54 | const dm_content: D.DocmapT = { 55 | '@context': 'https://w3id.org/docmaps/context.jsonld', 56 | type: 'docmap', 57 | id: 'http://ex/test-docmap', 58 | publisher: {}, 59 | created: TEST_DATE, 60 | } 61 | 62 | whenThenRight(adapterC.docmapWithIri, dm_content.id, dm_content) 63 | 64 | const adapter = instance(adapterC) 65 | 66 | const res = await new ApiInstance( 67 | adapter, 68 | new URL('https://example.com'), 69 | [new URL('https://other.com')], 70 | 30, 71 | 10, 72 | ).get_docmap_by_id(dm_content.id)() 73 | 74 | // TODO awkward use of `await .. ()`, is there a more natural way? 75 | // The alternative is all that `rightAnd` business in @docmaps/sdk 76 | t.deepEqual(res, await TE.of(dm_content)()) 77 | }) 78 | 79 | test('docmap_for: consults the adapter', async (t) => { 80 | const adapterC = mock(SparqlAdapter) 81 | 82 | const dm_content: D.DocmapT = { 83 | '@context': 'https://w3id.org/docmaps/context.jsonld', 84 | type: 'docmap', 85 | id: 'http://ex/test-docmap', 86 | publisher: {}, 87 | created: TEST_DATE, 88 | } 89 | 90 | const spec: ThingSpec = { identifier: '10.0000/test-thing', kind: 'doi' } 91 | whenThenRight(adapterC.docmapForThing, spec, dm_content) 92 | 93 | const adapter = instance(adapterC) 94 | 95 | const res = await new ApiInstance( 96 | adapter, 97 | new URL('https://example.com'), 98 | [new URL('https://other.com')], 99 | 30, 100 | 10, 101 | ).get_docmap_for_thing(spec)() 102 | 103 | // TODO awkward use of `await .. ()`, is there a more natural way? 104 | // The alternative is all that `rightAnd` business in @docmaps/sdk 105 | t.deepEqual(res, await TE.of(dm_content)()) 106 | }) 107 | -------------------------------------------------------------------------------- /packages/etl/test/unit/__fixtures__/abstract.ts: -------------------------------------------------------------------------------- 1 | import type { InductiveStepResult, Plugin } from '../../../src/types' 2 | import { mock, instance } from 'ts-mockito' 3 | 4 | import type * as D from '@docmaps/sdk' 5 | import * as TE from 'fp-ts/lib/TaskEither' 6 | import { MANUSCRIPT_DOI } from './crossref' 7 | 8 | class AbstractPlugin implements Plugin { 9 | stepForId(id: string): TE.TaskEither> { 10 | // to allow variable names without change 11 | const _id = id 12 | return TE.left(new Error('must mock method or this will fail!')) 13 | } 14 | 15 | actionForReviewId(id: string): TE.TaskEither { 16 | const _id = id 17 | return TE.left(new Error('must mock method or this will fail!')) 18 | } 19 | } 20 | 21 | export function AbstractPluginMocks() { 22 | const pluginT = mock(AbstractPlugin) 23 | const plugin = instance(pluginT) 24 | return { 25 | pluginT, 26 | plugin, 27 | } 28 | } 29 | 30 | export const PREPRINT_ID = 'preprint' 31 | export const MANUSCRIPT_ID = 'manuscript' 32 | export const REVIEW_1_ID = 'review_1' 33 | export const REVIEW_2_ID = 'review_2' 34 | 35 | export const PREPRINT_THING: D.ThingT = { 36 | type: 'preprint', 37 | id: PREPRINT_ID, 38 | } 39 | 40 | export const manuscriptStep: D.StepT = { 41 | actions: [ 42 | { 43 | inputs: [], 44 | outputs: [ 45 | { 46 | type: 'journal-article', 47 | id: MANUSCRIPT_ID, 48 | }, 49 | ], 50 | participants: [ 51 | { 52 | actor: { 53 | name: 'mock ln', 54 | }, 55 | role: 'author', 56 | }, 57 | ], 58 | }, 59 | ], 60 | assertions: [ 61 | { 62 | item: { 63 | type: 'journal-article', 64 | id: MANUSCRIPT_ID, 65 | }, 66 | status: 'published', 67 | }, 68 | ], 69 | } 70 | 71 | export const preprintStep: D.StepT = { 72 | actions: [ 73 | { 74 | inputs: [], 75 | outputs: [ 76 | { 77 | type: 'preprint', 78 | id: PREPRINT_ID, 79 | }, 80 | ], 81 | participants: [ 82 | { 83 | actor: { 84 | name: 'mock ln', 85 | }, 86 | role: 'author', 87 | }, 88 | ], 89 | }, 90 | ], 91 | assertions: [ 92 | { 93 | item: PREPRINT_THING, 94 | status: 'catalogued', 95 | }, 96 | ], 97 | } 98 | 99 | export const reviewActions: D.ActionT[] = [ 100 | { 101 | inputs: [{ doi: MANUSCRIPT_DOI }], 102 | outputs: [{ id: REVIEW_1_ID, type: 'review' }], 103 | participants: [], 104 | }, 105 | { 106 | inputs: [{ doi: MANUSCRIPT_DOI }], 107 | outputs: [{ id: REVIEW_2_ID, type: 'review' }], 108 | participants: [], 109 | }, 110 | ] 111 | 112 | export const mockManuscriptWithPreprintResponse: InductiveStepResult = { 113 | step: manuscriptStep, 114 | preprints: [PREPRINT_ID], 115 | reviews: [], 116 | manuscripts: [], 117 | } 118 | 119 | export const mockManuscriptWithReviewsResponse: InductiveStepResult = { 120 | step: manuscriptStep, 121 | preprints: [], 122 | reviews: [REVIEW_1_ID, REVIEW_2_ID], 123 | manuscripts: [], 124 | } 125 | 126 | export const mockManuscriptResponse: InductiveStepResult = { 127 | step: manuscriptStep, 128 | preprints: [], 129 | reviews: [], 130 | manuscripts: [], 131 | } 132 | 133 | export const mockPreprintWithManuscriptResponse: InductiveStepResult = { 134 | step: preprintStep, 135 | preprints: [], 136 | reviews: [], 137 | manuscripts: [MANUSCRIPT_ID], 138 | } 139 | -------------------------------------------------------------------------------- /.github/workflows/http-server-tests.yaml: -------------------------------------------------------------------------------- 1 | name: Test http-server 2 | 3 | on: 4 | push: 5 | # Allows you to run this workflow manually from the Actions tab 6 | workflow_dispatch: 7 | workflow_call: 8 | 9 | env: 10 | PKG_DIR: "packages/http-server" 11 | 12 | jobs: 13 | unit_tests: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [18.18.0] 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | 28 | - uses: pnpm/action-setup@v2 29 | name: Install pnpm 30 | id: pnpm-install 31 | with: 32 | version: 8 33 | run_install: false 34 | 35 | - name: Get pnpm store directory 36 | id: pnpm-cache 37 | shell: bash 38 | run: | 39 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 40 | 41 | - uses: actions/cache@v3 42 | name: Setup pnpm cache 43 | with: 44 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 45 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('${{env.PKG_DIR}}/pnpm-lock.yaml') }} 46 | restore-keys: | 47 | ${{ runner.os }}-pnpm-store- 48 | 49 | - name: Install dependencies 50 | run: | 51 | cd ${{env.PKG_DIR}} ; 52 | pnpm install; 53 | 54 | - name: Verify builds 55 | run: | 56 | cd ${{env.PKG_DIR}} ; 57 | pnpm run build:deps ; 58 | 59 | - name: Test 60 | run: | 61 | cd ${{env.PKG_DIR}} ; 62 | pnpm test:unit; 63 | 64 | - name: Lint Check 65 | run: | 66 | cd ${{env.PKG_DIR}} ; 67 | pnpm lint; 68 | 69 | integration_tests: 70 | runs-on: ubuntu-latest 71 | 72 | strategy: 73 | matrix: 74 | node-version: [18.18.0] 75 | 76 | # Service containers to run with `runner-job` 77 | services: 78 | # Label used to access the service container 79 | sparql_backend: 80 | # Docker Hub image 81 | image: ghcr.io/oxigraph/oxigraph 82 | # 83 | ports: 84 | # Opens tcp port 6379 on the host and service container 85 | - 33078:7878 86 | 87 | steps: 88 | - uses: actions/checkout@v4 89 | 90 | - name: Use Node.js ${{ matrix.node-version }} 91 | uses: actions/setup-node@v4 92 | with: 93 | node-version: ${{ matrix.node-version }} 94 | 95 | - uses: pnpm/action-setup@v2 96 | name: Install pnpm 97 | id: pnpm-install 98 | with: 99 | version: 8 100 | run_install: false 101 | 102 | - name: Get pnpm store directory 103 | id: pnpm-cache 104 | shell: bash 105 | run: | 106 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 107 | 108 | - uses: actions/cache@v3 109 | name: Setup pnpm cache 110 | with: 111 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 112 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('${{env.PKG_DIR}}/pnpm-lock.yaml') }} 113 | restore-keys: | 114 | ${{ runner.os }}-pnpm-store- 115 | 116 | - name: Install dependencies 117 | run: | 118 | cd ${{env.PKG_DIR}} ; 119 | pnpm install; 120 | 121 | - name: Build cross-workspace deps 122 | run: | 123 | cd ${{env.PKG_DIR}} ; 124 | pnpm run build:deps ; 125 | 126 | - name: Test 127 | run: | 128 | cd ${{env.PKG_DIR}} ; 129 | pnpm test:integration --timeout=30s ; 130 | -------------------------------------------------------------------------------- /packages/sdk/src/test/util.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { DateFromUnknown, UrlFromString, ArrayUpsertedEmpty } from '../util' 3 | import * as iots from 'io-ts' 4 | import { isRight } from 'fp-ts/lib/Either' 5 | import { rightAnd } from './utils' 6 | 7 | test('UrlFromString success cases', (t) => { 8 | const url1 = UrlFromString.decode('https://docmaps.knowledgefutures.org') 9 | t.true( 10 | rightAnd(url1, (u) => { 11 | t.is(u.host, 'docmaps.knowledgefutures.org') 12 | t.is(u.protocol, 'https:') 13 | }), 14 | ) 15 | 16 | const url2 = UrlFromString.decode('file:///tmp/some-file.txt') 17 | t.true( 18 | rightAnd(url2, (u) => { 19 | t.is(u.pathname, '/tmp/some-file.txt') 20 | t.is(u.protocol, 'file:') 21 | }), 22 | ) 23 | 24 | const url3 = UrlFromString.decode('http://localhost:8080/?someQuery=true') 25 | t.true( 26 | rightAnd(url3, (u) => { 27 | t.is(u.hostname, 'localhost') 28 | t.is(u.protocol, 'http:') 29 | t.is(u.port, '8080') 30 | t.is(u.search, '?someQuery=true') 31 | }), 32 | ) 33 | }) 34 | 35 | test('UrlFromString failure cases', (t) => { 36 | const url1 = UrlFromString.decode('NOT_A_URL') 37 | t.false(isRight(url1)) 38 | 39 | const url2 = UrlFromString.decode(409) 40 | t.false(isRight(url2)) 41 | 42 | const url3 = UrlFromString.decode({}) 43 | t.false(isRight(url3)) 44 | }) 45 | 46 | test('DateFromUnknown success cases', (t) => { 47 | const d1 = DateFromUnknown.decode(new Date(0)) 48 | t.true( 49 | rightAnd(d1, (d) => { 50 | t.is(d.valueOf(), 0) 51 | t.is(d.getUTCFullYear(), 1970) 52 | t.is(DateFromUnknown.encode(d), '1970-01-01T00:00:00.000Z') 53 | }), 54 | ) 55 | 56 | const d2 = DateFromUnknown.decode(0) 57 | t.true( 58 | rightAnd(d2, (d) => { 59 | t.is(d.valueOf(), 0) 60 | t.is(d.getUTCFullYear(), 1970) 61 | t.is(DateFromUnknown.encode(d), '1970-01-01T00:00:00.000Z') 62 | }), 63 | ) 64 | 65 | const d4 = DateFromUnknown.decode('1970-01-01T00:00:00.000Z') 66 | t.true( 67 | rightAnd(d4, (d) => { 68 | t.is(d.valueOf(), 0) 69 | t.is(d.getUTCFullYear(), 1970) 70 | t.is(DateFromUnknown.encode(d), '1970-01-01T00:00:00.000Z') 71 | }), 72 | ) 73 | }) 74 | 75 | test('DateFromUnknown failure cases', (t) => { 76 | const d1 = DateFromUnknown.decode('NOT_A_DATE') 77 | t.false(isRight(d1)) 78 | 79 | const d2 = DateFromUnknown.decode(undefined) 80 | t.false(isRight(d2)) 81 | 82 | const d4 = DateFromUnknown.decode({ some: 'thing' }) 83 | t.false(isRight(d4)) 84 | }) 85 | 86 | test('ArrayUpsertedEmpty success cases', (t) => { 87 | const a1 = ArrayUpsertedEmpty(iots.string).decode(['one', 'two']) 88 | t.true( 89 | rightAnd(a1, (a) => { 90 | t.deepEqual(a, ['one', 'two']) 91 | }), 92 | ) 93 | 94 | const a2 = ArrayUpsertedEmpty(iots.string).decode(null) 95 | t.true( 96 | rightAnd(a2, (a) => { 97 | t.deepEqual(a, []) 98 | }), 99 | ) 100 | 101 | const a3 = iots 102 | .type({ 103 | something: ArrayUpsertedEmpty(iots.number), 104 | }) 105 | .decode({}) 106 | t.true( 107 | rightAnd(a3, (a) => { 108 | t.deepEqual(a, { something: [] }) 109 | }), 110 | ) 111 | }) 112 | 113 | test('ArrayUpsertedEmpty failure cases', (t) => { 114 | const a1 = ArrayUpsertedEmpty(iots.string).decode([1, 2, 3]) 115 | t.false(isRight(a1)) 116 | 117 | const a2 = ArrayUpsertedEmpty(iots.string).decode('single value') 118 | t.false(isRight(a2)) 119 | 120 | const a3 = iots 121 | .partial({ 122 | something: ArrayUpsertedEmpty(iots.number), 123 | }) 124 | .decode({ something: ['string not number'] }) 125 | t.false(isRight(a3)) 126 | }) 127 | -------------------------------------------------------------------------------- /packages/etl/src/plugins/crossref/api.ts: -------------------------------------------------------------------------------- 1 | import type { CrossrefClient } from 'crossref-openapi-client-ts' 2 | import * as E from 'fp-ts/lib/Either' 3 | import type { Plugin } from '../../types' 4 | import { pipe } from 'fp-ts/lib/function' 5 | import * as TE from 'fp-ts/lib/TaskEither' 6 | import * as D from '@docmaps/sdk' 7 | import { relatedDoisForWork, decodeActionForWork } from './functions' 8 | import { mapLeftToUnknownError } from '../../utils' 9 | 10 | export { CreateCrossrefClient } from 'crossref-openapi-client-ts' 11 | 12 | export const CrossrefPlugin: (client: CrossrefClient) => Plugin = ( 13 | client: CrossrefClient, 14 | ) => { 15 | const service = client.works 16 | return { 17 | stepForId: (id: string) => { 18 | return pipe( 19 | TE.Do, 20 | TE.bind('w', () => 21 | TE.tryCatch( 22 | () => service.getWorks({ doi: id }), 23 | (reason: unknown) => 24 | new Error(`failed to fetch crossref body for DOI ${id}`, { cause: reason }), 25 | ), 26 | ), 27 | TE.bind('status', ({ w }) => 28 | pipe( 29 | w, 30 | (w) => { 31 | switch (w.message.type) { 32 | case 'posted-content': 33 | return E.right('catalogued') 34 | case 'journal-article': 35 | return E.right('published') 36 | default: 37 | return E.left( 38 | new Error( 39 | `requested root docmap for crossref entity of type '${w.message.type}'`, 40 | ), 41 | ) 42 | } 43 | }, 44 | TE.fromEither, 45 | ), 46 | ), 47 | // 1. get step for this 48 | TE.bind('step', ({ w, status }) => 49 | pipe( 50 | w.message, 51 | decodeActionForWork, 52 | E.map((action) => ({ 53 | inputs: [], 54 | actions: [action], 55 | assertions: [ 56 | { 57 | status: status, //TODO : choose this key carefully 58 | item: w.message.DOI, 59 | }, 60 | ], 61 | })), 62 | E.chain((action) => 63 | pipe( 64 | D.Step.decode(action), 65 | mapLeftToUnknownError('decoding action in stepsForDoiRecursive'), 66 | ), 67 | ), 68 | TE.fromEither, 69 | ), 70 | ), 71 | TE.bind('reviews', ({ w }) => TE.of(relatedDoisForWork(w.message, 'has-review'))), 72 | TE.bind('manuscripts', ({ w }) => TE.of(relatedDoisForWork(w.message, 'is-preprint-of'))), 73 | TE.bind('preprints', ({ w }) => TE.of(relatedDoisForWork(w.message, 'has-preprint'))), 74 | TE.map(({ step, reviews, preprints, manuscripts }) => ({ 75 | step, 76 | reviews, 77 | preprints, 78 | manuscripts, 79 | })), 80 | ) 81 | }, 82 | 83 | actionForReviewId: (id: string) => { 84 | return actionForReviewDOI(client, id) 85 | }, 86 | } 87 | } 88 | 89 | export function actionForReviewDOI( 90 | client: CrossrefClient, 91 | doi: string, 92 | ): TE.TaskEither { 93 | const service = client.works 94 | return pipe( 95 | TE.tryCatch( 96 | () => service.getWorks({ doi: doi }), // FIXME throw/reject if not status OK ? (what is client behavior if not 200?) 97 | (reason: unknown) => 98 | new Error(`failed to fetch crossref body for review DOI ${doi}`, { cause: reason }), 99 | ), 100 | TE.map((w) => w.message), 101 | TE.chain((m) => TE.fromEither(decodeActionForWork(m))), 102 | ) 103 | } 104 | -------------------------------------------------------------------------------- /packages/sdk/src/test/__fixtures__/from_root_examples.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import factory from 'rdf-ext' 3 | import ParserN3 from '@rdfjs/parser-n3' 4 | 5 | function loadDatasetNtriples(filePath: string) { 6 | const stream = fs.createReadStream(filePath) 7 | const parser = new ParserN3({ factory }) 8 | return parser.import(stream) 9 | } 10 | 11 | function loadDataset(filePath: string) { 12 | // TODO - note that in the eLife case, we have to parse out from the top-level array or graph 13 | return JSON.parse(fs.readFileSync(filePath).toString()) 14 | } 15 | 16 | // because these create streams in the NTriples, if two tests consume the same 17 | // stream the second one will fail. This method lets us construct a new tests 18 | // object anytime. 19 | export const FromRootExamplesNew = () => ({ 20 | biorxiv_01_nt: loadDatasetNtriples('../../examples/docmaps-example-biorxiv-01.jsonld.nt'), 21 | elife_01_nt: loadDatasetNtriples('../../examples/docmaps-example-elife-01.jsonld.nt'), 22 | elife_02_nt: loadDatasetNtriples('../../examples/docmaps-example-elife-02.jsonld.nt'), 23 | embo_01_nt: loadDatasetNtriples('../../examples/docmaps-example-embo-01.jsonld.nt'), 24 | epmc_01_nt: loadDatasetNtriples('../../examples/docmaps-example-epmc-01.jsonld.nt'), // TODO not currently used in any test 25 | // This one has the new-styled `steps->actions->inputs` format 26 | epmc_01_updated_nt: loadDatasetNtriples( 27 | '../../examples/docmaps-example-epmc-01-updated.jsonld.nt', 28 | ), 29 | 30 | biorxiv_01_jsonld: loadDataset('../../examples/docmaps-example-biorxiv-01.jsonld'), 31 | epmc_01_jsonld: loadDataset('../../examples/docmaps-example-epmc-01.jsonld'), 32 | // This one has the new-styled `steps->actions->inputs` format 33 | epmc_01_updated_jsonld: loadDataset('../../examples/docmaps-example-epmc-01-updated.jsonld'), 34 | // TODO - note the difference in structure of the loading here 35 | elife_01_jsonld: loadDataset('../../examples/docmaps-example-elife-01.jsonld')[0], 36 | elife_02_jsonld: loadDataset('../../examples/docmaps-example-elife-02.jsonld')[0], 37 | // TODO - note the difference in structure of the loading here 38 | embo_01_jsonld: loadDataset('../../examples/docmaps-example-embo-01.jsonld')['@graph'][0][ 39 | 'docmap' 40 | ], 41 | }) 42 | 43 | // in test suites where the streams are not consumed, duplication is OK. 44 | export const FromRootExamples = FromRootExamplesNew() 45 | 46 | // valid docmaps we can currently parse 47 | const el_dm = [ 48 | FromRootExamples.elife_01_jsonld, 49 | FromRootExamples.elife_02_jsonld, 50 | 51 | // TODO we are unable to currently assert on the bioRxiv example due to malformation: 52 | // - the RoleInTime puts the Role under the Actor rather than sibling to 53 | // - the Manifestation uses `webpage` rather than `web-page`. 54 | // and maybe more! 55 | // 56 | // FromRootExamples.biorxiv_01_jsonld, 57 | 58 | FromRootExamples.embo_01_jsonld, 59 | FromRootExamples.epmc_01_updated_jsonld, 60 | ] 61 | 62 | const el_dm_publisher = el_dm.flatMap((dm) => dm['publisher'] || []) 63 | const el_dm_acc = el_dm_publisher.flatMap((p) => p['account'] || []) 64 | const el_dm_step: any[] = el_dm.flatMap((dm) => Object.values(dm['steps']) || []) 65 | const el_dm_action = el_dm_step.flatMap((s) => s['actions'] || []) 66 | const el_dm_assertion = el_dm_step.flatMap((s) => s['assertions'] || []) 67 | const el_dm_thing = el_dm_action.flatMap((a) => a['outputs'] || []) 68 | const el_dm_action_inputs = el_dm_action.flatMap((a) => a['inputs'] || []) 69 | const el_dm_mani = el_dm_thing.flatMap((t) => t['content'] || []) 70 | const el_dm_role = el_dm_action.flatMap((a) => a['participants'] || []) 71 | const el_dm_actor = el_dm_role.map((r) => r['actor'] || []) 72 | 73 | export const PartialExamples = { 74 | elife: { 75 | Docmap: el_dm, 76 | Publisher: el_dm_publisher, 77 | OnlineAccount: el_dm_acc, 78 | Step: el_dm_step, 79 | Action: el_dm_action, 80 | Assertion: el_dm_assertion, 81 | Thing: el_dm_thing, 82 | ActionInputs: el_dm_action_inputs, 83 | Actor: el_dm_actor, 84 | RoleInTime: el_dm_role, 85 | Manifestation: el_dm_mani, 86 | }, 87 | } 88 | -------------------------------------------------------------------------------- /packages/sdk/src/test/__fixtures__/small_quadstore.ts: -------------------------------------------------------------------------------- 1 | import { Readable } from 'readable-stream' 2 | import rdf from '@rdfjs/data-model' 3 | 4 | const urlType = rdf.namedNode('http://www.w3.org/2001/XMLSchema#anyURI') 5 | 6 | export const OnePublisherQuadstore = () => { 7 | const r = new Readable({ 8 | objectMode: true, 9 | read: () => { 10 | r.push( 11 | rdf.quad( 12 | rdf.namedNode('https://w3id.org/docmaps/examples/publisher'), 13 | rdf.namedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'), 14 | rdf.namedNode('http://xmlns.com/foaf/0.1/organization'), 15 | ), 16 | ) 17 | r.push( 18 | rdf.quad( 19 | rdf.namedNode('https://w3id.org/docmaps/examples/publisher'), 20 | rdf.namedNode('http://xmlns.com/foaf/0.1/name'), 21 | rdf.literal('Example Publisher'), 22 | ), 23 | ) 24 | r.push( 25 | rdf.quad( 26 | rdf.namedNode('https://w3id.org/docmaps/examples/publisher'), 27 | rdf.namedNode('http://xmlns.com/foaf/0.1/logo'), 28 | rdf.literal('https://w3id.org/docmaps/examples/publisher#logo.img'), 29 | ), 30 | ) 31 | r.push( 32 | rdf.quad( 33 | rdf.namedNode('https://w3id.org/docmaps/examples/publisher'), 34 | rdf.namedNode('http://xmlns.com/foaf/0.1/homepage'), 35 | rdf.literal('https://w3id.org/docmaps/examples/publisher#www'), 36 | ), 37 | ) 38 | r.push( 39 | rdf.quad( 40 | rdf.namedNode('https://w3id.org/docmaps/examples/publisher'), 41 | rdf.namedNode('http://purl.org/spar/fabio/hasURL'), 42 | // TODO because of the jsonld compaction algorithm, and that the hasURL key in our 43 | // context has a datatype associated, this literal value MUST have a datatype 44 | // included in its object in order to be correctly parsed into a Term from the 45 | // context, rather than into a CURIE calculated on the fly. 46 | rdf.literal('https://w3id.org/docmaps/examples/publisher#www', urlType), 47 | ), 48 | ) 49 | 50 | r.push( 51 | rdf.quad( 52 | rdf.namedNode('https://w3id.org/docmaps/examples/publisher'), 53 | rdf.namedNode('http://xmlns.com/foaf/0.1/onlineAccount'), 54 | rdf.namedNode('https://w3id.org/docmaps/examples/publisher_account'), 55 | ), 56 | ) 57 | 58 | r.push( 59 | rdf.quad( 60 | rdf.namedNode('https://w3id.org/docmaps/examples/publisher_account'), 61 | rdf.namedNode('http://xmlns.com/foaf/0.1/accountServiceHomepage'), 62 | rdf.literal('https://w3id.org/docmaps/examples/publisher_account#HOMEPAGE'), 63 | ), 64 | ) 65 | r.push( 66 | rdf.quad( 67 | rdf.namedNode('https://w3id.org/docmaps/examples/docmap'), 68 | rdf.namedNode('http://purl.org/dc/terms/publisher'), 69 | rdf.namedNode('https://w3id.org/docmaps/examples/publisher'), 70 | ), 71 | ) 72 | r.push(null) 73 | }, 74 | }) 75 | 76 | return r 77 | } 78 | 79 | export const OneManifestationQuadstore = () => { 80 | const r = new Readable({ 81 | objectMode: true, 82 | read: () => { 83 | r.push( 84 | rdf.quad( 85 | rdf.namedNode('https://w3id.org/docmaps/examples/manifestation'), 86 | rdf.namedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'), 87 | rdf.namedNode('http://purl.org/spar/fabio/WebPage'), 88 | ), 89 | ) 90 | r.push( 91 | rdf.quad( 92 | rdf.namedNode('https://w3id.org/docmaps/examples/manifestation'), 93 | rdf.namedNode('http://purl.org/spar/fabio/hasURL'), 94 | rdf.literal('https://w3id.org/docmaps/examples/manifestation#URL', urlType), 95 | ), 96 | ) 97 | r.push( 98 | rdf.quad( 99 | rdf.namedNode('https://w3id.org/docmaps/examples/manifestation'), 100 | rdf.namedNode('http://xmlns.com/foaf/0.1/accountServiceHomepage'), 101 | rdf.literal('https://w3id.org/docmaps/examples/manifestation#HOMEPAGE'), 102 | ), 103 | ) 104 | r.push(null) 105 | }, 106 | }) 107 | return r 108 | } 109 | -------------------------------------------------------------------------------- /packages/widget/src/detail-navigation-header.ts: -------------------------------------------------------------------------------- 1 | import { nothing, svg, SVGTemplateResult } from 'lit'; 2 | import { DisplayObject, TYPE_DISPLAY_OPTIONS } from './display-object'; 3 | 4 | const TIMELINE_WIDTH: number = 368; 5 | const FIRST_NODE_X: number = 6.5; 6 | const LAST_NODE_X: number = TIMELINE_WIDTH - 9.5; 7 | 8 | const backButton = ( 9 | allNodes: DisplayObject[], 10 | selectedNode: DisplayObject, 11 | updateSelectedNode: (node: DisplayObject) => void, 12 | ) => { 13 | const selectedIndex = allNodes.findIndex((node) => node.nodeId === selectedNode.nodeId); 14 | const previousIndex = selectedIndex > 0 ? selectedIndex - 1 : allNodes.length - 1; 15 | const previousNode = allNodes[previousIndex]; 16 | return svg` 17 | 19 | 20 | 21 | 22 | `; 23 | }; 24 | 25 | const forwardButton = ( 26 | allNodes: DisplayObject[], 27 | selectedNode: DisplayObject, 28 | updateSelectedNode: (node: DisplayObject) => void, 29 | ) => { 30 | const index = allNodes.findIndex((node) => node.nodeId === selectedNode.nodeId); 31 | const nextIndex = index < allNodes.length - 1 ? index + 1 : 0; 32 | const nextNode = allNodes[nextIndex]; 33 | return svg` 34 | 36 | 37 | 38 | 39 | `; 40 | }; 41 | 42 | function getNodeX(i: number, numberOfNodes: number): number { 43 | const interval = (LAST_NODE_X - FIRST_NODE_X) / (numberOfNodes - 1); 44 | return FIRST_NODE_X + interval * i; 45 | } 46 | 47 | const timeline = ( 48 | allNodes: DisplayObject[], 49 | selectedNode: DisplayObject, 50 | updateSelectedNode: (node: DisplayObject) => void, 51 | ) => { 52 | const timelineNodes: SVGTemplateResult[] = allNodes.map((node, i) => { 53 | const x = getNodeX(i, allNodes.length); 54 | const displayOpts = TYPE_DISPLAY_OPTIONS[node.type]; 55 | const color = displayOpts.detailViewBackgroundColor ?? displayOpts.backgroundColor; 56 | 57 | const thisNodeIsSelected: boolean = node.nodeId === selectedNode.nodeId; 58 | const selectedNodeIndicator: SVGTemplateResult | typeof nothing = thisNodeIsSelected 59 | ? svg` 60 | ` 61 | : nothing; 62 | 63 | return svg` 64 | 66 | ${selectedNodeIndicator} 67 | `; 68 | }); 69 | 70 | const timeline = svg` 76 | ${timeline}> 77 | ${timelineNodes} 78 | `; 79 | }; 80 | 81 | type RenderDetailNavigationHeader = ( 82 | allNodes: DisplayObject[], 83 | selectedNode: DisplayObject, 84 | updateSelectedNode: (node: DisplayObject) => void, 85 | ) => SVGTemplateResult; 86 | 87 | export const renderDetailNavigationHeader: RenderDetailNavigationHeader = ( 88 | allNodes: DisplayObject[], 89 | selectedNode: DisplayObject, 90 | updateSelectedNode, 91 | ) => svg` 92 | ${backButton(allNodes, selectedNode, updateSelectedNode)} 93 | ${forwardButton(allNodes, selectedNode, updateSelectedNode)} 94 | ${timeline(allNodes, selectedNode, updateSelectedNode)}`; 95 | -------------------------------------------------------------------------------- /packages/http-server/test/unit/sparql.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { SparqlAdapter } from '../../src/adapter' 3 | import * as E from 'fp-ts/lib/Either' 4 | import { inspect } from 'util' 5 | import * as fixtures from '../__fixtures__/docmaps' 6 | import { testLoggerWithPino } from '../utils' 7 | 8 | test('docmapForThing: extracts a realistic docmap from a sparql backend when the steps already exist', async (t) => { 9 | const backend = fixtures.CreateDatastore() 10 | 11 | const processor = new SparqlAdapter(backend, testLoggerWithPino(t.log)) 12 | 13 | const docmap = await processor.docmapForThing({ 14 | identifier: '10.1101/2022.11.08.515698', 15 | kind: 'doi', 16 | })() 17 | 18 | if (E.isLeft(docmap)) { 19 | t.fail(`got error instead of docmap: ${inspect(docmap.left, { depth: 4 })}`) 20 | return 21 | } 22 | 23 | t.is( 24 | docmap.right.id, 25 | 'https://data-hub-api.elifesciences.org/enhanced-preprints/docmaps/v1/get-by-doi?preprint_doi=10.1101%2F2022.11.08.515698', 26 | ) 27 | 28 | const steps = docmap.right.steps 29 | if (!steps) { 30 | t.fail('expected to find steps, but was null') 31 | return 32 | } 33 | 34 | t.is(Object.keys(steps).length, 3) 35 | }) 36 | 37 | test('docmapForThing: extracts a realistic docmap from a sparql backend when there are multiple candidates', async (t) => { 38 | const backend = fixtures.CreateDatastore() 39 | 40 | const processor = new SparqlAdapter(backend, testLoggerWithPino(t.log)) 41 | 42 | const docmap = await processor.docmapForThing({ 43 | identifier: '10.1101/2022.11.08.515698', 44 | kind: 'doi', 45 | })() 46 | 47 | if (E.isLeft(docmap)) { 48 | t.fail(`got error instead of docmap: ${inspect(docmap.left, { depth: 4 })}`) 49 | return 50 | } 51 | 52 | t.is( 53 | docmap.right.id, 54 | 'https://data-hub-api.elifesciences.org/enhanced-preprints/docmaps/v1/get-by-doi?preprint_doi=10.1101%2F2022.11.08.515698', 55 | ) 56 | 57 | const steps = docmap.right.steps 58 | if (!steps) { 59 | t.fail('expected to find steps, but was null') 60 | return 61 | } 62 | 63 | t.is(Object.keys(steps).length, 3) 64 | }) 65 | 66 | test('docmapForThing: returns a failure case if no docmap can be found for a doi', async (t) => { 67 | const backend = fixtures.CreateDatastore() 68 | 69 | const processor = new SparqlAdapter(backend, testLoggerWithPino(t.log)) 70 | 71 | const docmap = await processor.docmapForThing({ 72 | identifier: '10.1101/2022.11.08.nonexistent', 73 | kind: 'doi', 74 | })() 75 | 76 | if (E.isRight(docmap)) { 77 | t.fail(`got docmap instead of error: ${inspect(docmap.right, { depth: 4 })}`) 78 | return 79 | } 80 | 81 | t.regex(docmap.left.message, /zero quads/) 82 | }) 83 | 84 | test('docmapWithiri: constructs and extracts a realistic docmap from a sparql backend', async (t) => { 85 | const backend = fixtures.CreateDatastore() 86 | 87 | const processor = new SparqlAdapter(backend, testLoggerWithPino(t.log)) 88 | 89 | const docmap = await processor.docmapWithIri( 90 | 'https://data-hub-api.elifesciences.org/enhanced-preprints/docmaps/v1/get-by-doi?preprint_doi=10.1101%2F2022.11.08.515698', 91 | )() 92 | 93 | if (E.isLeft(docmap)) { 94 | t.fail(`got error instead of docmap: ${inspect(docmap.left, { depth: 4 })}`) 95 | return 96 | } 97 | 98 | t.is( 99 | docmap.right.id, 100 | 'https://data-hub-api.elifesciences.org/enhanced-preprints/docmaps/v1/get-by-doi?preprint_doi=10.1101%2F2022.11.08.515698', 101 | ) 102 | 103 | const steps = docmap.right.steps 104 | if (!steps) { 105 | t.fail('expected to find steps, but was null') 106 | return 107 | } 108 | 109 | t.is(Object.keys(steps).length, 3) 110 | }) 111 | 112 | /* This test is removed because although it passes, it 113 | * causes the suite to timeout mysteriously: 114 | * https://github.com/avajs/ava/discussions/3248 115 | * 116 | * test.serial('query performance in simple scenario: < 10ms each', async (t) => { 117 | * t.timeout(20000) 118 | * const backend = fixtures.CreateDatastore() 119 | 120 | * const processor = new SparqlAdapter(backend) 121 | 122 | * const start = new Date() 123 | 124 | * for (let i = 0; i < 1000; i++) { 125 | * await processor.docmapWithIri( 126 | * 'https://data-hub-api.elifesciences.org/enhanced-preprints/docmaps/v1/get-by-doi?preprint_doi=10.1101%2F2022.11.08.515698', 127 | * )() 128 | * } 129 | 130 | * const end = new Date() 131 | * t.true(end.getTime() - start.getTime() < 10000) 132 | * }) 133 | */ 134 | -------------------------------------------------------------------------------- /packages/http-server/src/httpserver/command.ts: -------------------------------------------------------------------------------- 1 | import { Command, Option } from '@commander-js/extra-typings' 2 | import { API_VERSION } from '../api_version' 3 | import * as hs from './server' 4 | import pino from 'pino' 5 | 6 | // const stdoutWrite = (str: string) => process.stdout.write(str) 7 | 8 | /** 9 | * CLI 10 | * 11 | * the MakeCli function creates instances of the Cli, 12 | * which is useful for re-use in test. The executable 13 | * script for this tool will use the singleton cli 14 | * (the default export). 15 | * 16 | * Invoke this as a library by calling `MakeCli().parseAsync`. 17 | * with no args, as in index.ts, parseAsync uses argv. 18 | * or pass in string array to parse flags. 19 | */ 20 | 21 | export function MakeCli() { 22 | const cli = new Command() 23 | 24 | const BACKEND_TYPES = ['memory', 'sparql-endpoint'] 25 | const LOG_LEVELS = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'] 26 | 27 | cli 28 | .name('docmaps-api-server') 29 | .description( 30 | `API server in nodejs conforming to the Docmaps API Server Specification v${API_VERSION}`, 31 | ) 32 | .version('0.1.0') 33 | 34 | cli 35 | .command('start') 36 | .description('start the server') 37 | .addOption( 38 | new Option('--logLevel ', `the maximum log level to emit to stdout`) 39 | .env('DM_LOG_LEVEL') 40 | .default('info') 41 | .choices(LOG_LEVELS) 42 | .makeOptionMandatory(), 43 | ) 44 | .addOption( 45 | new Option('--backendType ', `the plugin source name`) 46 | .env('DM_BACKEND_TYPE') 47 | .default(BACKEND_TYPES[0]) 48 | .choices(BACKEND_TYPES) 49 | .makeOptionMandatory(), 50 | ) 51 | .addOption( 52 | new Option('--server.port ', `port to listen on`) 53 | .env('DM_SERVER_PORT') 54 | .argParser(Number) 55 | .makeOptionMandatory(), 56 | ) 57 | .addOption( 58 | new Option( 59 | '--server.apiUrl ', 60 | 'the URL that this server can be reached at (for advertising purposes only)', 61 | ) 62 | .env('DM_SERVER_API_URL') 63 | .makeOptionMandatory(), 64 | ) 65 | .addOption( 66 | new Option( 67 | '--backend.sparqlEndpoint.url ', 68 | 'url including scheme, host, port where the sparql endpoint can be reached', 69 | ).env('DM_BACKEND_SPARQL_ENDPOINT_URL'), 70 | ) 71 | .addOption( 72 | new Option( 73 | '--backend.memory.baseIri ', 74 | 'IRI to use as the base/prefix IRI for the in memory triplestore', 75 | ).env('DM_BACKEND_MEMORY_BASE_IRI'), 76 | ) 77 | .action(async (options) => { 78 | const serveConfig = { 79 | apiUrl: options['server.apiUrl'], 80 | port: options['server.port'], 81 | } 82 | 83 | let config: hs.ServerConfig 84 | switch (options['backendType']) { 85 | case 'memory': 86 | if (!options['backend.memory.baseIri']) { 87 | throw 'specified memory backend but no baseIri' 88 | } 89 | 90 | config = { 91 | server: serveConfig, 92 | backend: { 93 | type: 'memory', 94 | memory: { 95 | baseIri: options['backend.memory.baseIri'], 96 | }, 97 | }, 98 | } 99 | break 100 | 101 | case 'sparql-endpoint': 102 | if (!options['backend.sparqlEndpoint.url']) { 103 | throw 'specified sparqlEndpoint backend but no url' 104 | } 105 | 106 | config = { 107 | server: serveConfig, 108 | backend: { 109 | type: 'sparqlEndpoint', 110 | sparqlEndpoint: { 111 | url: options['backend.sparqlEndpoint.url'], 112 | }, 113 | }, 114 | } 115 | break 116 | default: 117 | throw 'specified illegal backendType: choose one of `memory`,`sparql-endpoint`' 118 | } 119 | 120 | const logger = pino({ name: '@docmaps/http-server', level: options.logLevel }).child({ 121 | lang: 'ts', 122 | api_version: API_VERSION, 123 | }) 124 | 125 | const server = new hs.HttpServer(config, { logger: logger }) 126 | // Inject logging into the server config: 127 | // 128 | // const writer = cli.configureOutput()?.writeOut || stdoutWrite 129 | // writer(out) 130 | 131 | logger.info('finished setup') 132 | await server.listen() 133 | }) 134 | 135 | return cli 136 | } 137 | -------------------------------------------------------------------------------- /packages/sdk/README.md: -------------------------------------------------------------------------------- 1 | > DEPRECATION NOTICE: the NPM package `docmaps-sdk` has been moved to `@docmaps/sdk`. 2 | > (the source code still lives here.) 3 | > Update your installs and imports accordingly. 4 | 5 | # Typescript SDK for Docmaps 6 | 7 | This typescript library is designed to provide core, highly-general docmaps 8 | functionality for ease-of-use in Typescript. It provides out-of-the-box 9 | validation of JSON-LD documents interpreted as docmaps directly. It is intended 10 | to additionally support validation of Docmap sub-elements, such as individual 11 | Actions or Actors that might be published separately from a whole Docmap. It 12 | will also be integrated into concrete tools such as a docmap-from-meca ETL pipeline 13 | and general visualization tools. 14 | 15 | ## Implementation 16 | 17 | The core types are written using [`io-ts`](https://github.com/gcanti/io-ts), whose 18 | expressive language defines codecs for validation, encoding and decoding of objects 19 | between various types. 20 | 21 | These codecs are then used to extract the Typescript interfaces that most narrowly 22 | describe their parsed outputs. Optional fields are described using `t.partial`, whereas 23 | required fields are described using `t.type`. `t.intersection` allows both required and 24 | optional fields. None of these types will fail to parse due to extra keys present, but those 25 | keys will be dropped. We can disallow extra keys using `t.exact`. 26 | 27 | Any codec can be used directly with a JSON string or `any`/`unknown` object to try and 28 | create the instantiation of the Typescript type. `io-ts` is designed to work with 29 | [`fp-ts`](https://github.com/gcanti/fp-ts), so you get an instance of Either which must 30 | be deconstructed by case to determine whether the input was valid. See examples of this 31 | in the [`typed_graph`](https://github.com/Docmaps-Project/docmaps/blob/main/packages/sdk/src/typed_graph.ts), 32 | where we use `isLeft` to check if the decode failed. 33 | 34 | **For examples of usage of `fp-ts` pipelines with our `io-ts` codecs, review the 35 | [`ts-etl` implementation](https://github.com/Docmaps-Project/docmaps/blob/main/packages/ts-etl/src/plugins/crossref/api.ts). 36 | 37 | ### Extended usage with `typed_graph` 38 | 39 | Alternatively, the `typed_graph` class is used for choosing the codec to use based on the 40 | `@type` key present in the jsonld. This is mainly here to support to-be-implemented RDF 41 | use-cases rather than JsonLD use cases, because it only works when the `@type` field is set 42 | the input objects, which we generally do not expect except in `Docmap` and `Manifestation` 43 | at the moment. Generally I recommend you to ignore `typed_graph` until further development 44 | makes it more useful. 45 | 46 | A utility function is available on the `TypedGraph` class that can ingest an RDF Quad stream 47 | and return a TaskEither that will eventually resolve to a JSONLD object or error. It is async, 48 | and not time-bound, and so is not recommended for production use at the current time. However 49 | it does allow type-safe extraction of Docmaps from unstructured in-memory triplestores, such 50 | as the results of a SPARQL query. 51 | 52 | ## Documentation 53 | 54 | Documentation is [served by github pages](https://docmaps-project.github.io/docmaps/sdk/index.html). 55 | If you wish to view documentation for an off-branch edition of this package, the directory `/docs` 56 | can be populated by the command `pnpm docs:generate`. The inputs to the 57 | generation script include all Markdown and source files in this directory. These docs are generated 58 | dynamically during GH Pages release on merge to main, so the directory can be empty on check-in. 59 | 60 | ## Contributing 61 | 62 | For Code of Conduct, see the repository-wide 63 | [CODE_OF_CONDUCT.md](https://github.com/Docmaps-Project/docmaps/blob/main/CODE_OF_CONDUCT.md). 64 | 65 | For info about local development of this repository, see 66 | [CONTRIBUTING.md](https://github.com/Docmaps-Project/docmaps/blob/main/packages/sdk/CONTRIBUTING.md). 67 | 68 | ## Releases 69 | 70 | Packages are hosted on NPM and automated by senmantic-release (see repository root for more info). 71 | 72 | ## Current next steps 73 | 74 | Review the issues on this repository for up-to-date info of desired improvements. 75 | There are also expressive TODOs in the codebase. 76 | Here are some examples: 77 | 78 | - [x] use more specific types in `io-ts-types` to validate that strings which should 79 | be URLs and dates contain their respective value types. 80 | - [ ] build out the typed-graph functionality to support parsing various types from streams. 81 | - [ ] validate the semantics of docmaps, not just structure (i.e., the first-step refers to a real step). 82 | -------------------------------------------------------------------------------- /packages/http-server/src/httpserver/server.ts: -------------------------------------------------------------------------------- 1 | import express, { Application } from 'express' 2 | import { createServer as createHttpServer, Server as ServerHttp } from 'http' 3 | import { Server as ServerHttps } from 'https' 4 | import { initServer, createExpressEndpoints } from '@ts-rest/express' 5 | import { contract } from '@docmaps/http-client' 6 | import { ApiInstance } from '../api' 7 | import { OxigraphInmemBackend } from '../adapter/oxigraph_inmem' 8 | import { SparqlAdapter, SparqlFetchBackend } from '../adapter' 9 | import { isLeft } from 'fp-ts/lib/Either' 10 | import { BackendAdapter } from '../types' 11 | import { Logger } from 'pino' 12 | import phttp from 'pino-http' 13 | import cors from 'cors' 14 | 15 | export type ServerConfig = { 16 | server: { 17 | port: number 18 | apiUrl: string 19 | } 20 | backend: 21 | | { 22 | type: 'sparqlEndpoint' 23 | sparqlEndpoint: { 24 | url: string 25 | } 26 | } 27 | | { 28 | type: 'memory' 29 | memory: { 30 | baseIri: string 31 | } 32 | } 33 | } 34 | 35 | export interface ServerIO { 36 | logger: Logger 37 | } 38 | 39 | // TODO: rename? 40 | export class HttpServer { 41 | api: ApiInstance 42 | app: Application 43 | server: ServerHttp | ServerHttps // FIXME : support https 44 | config: ServerConfig 45 | io: ServerIO 46 | 47 | constructor(config: ServerConfig, io: ServerIO) { 48 | this.config = config 49 | this.io = { 50 | ...io, 51 | } 52 | 53 | let adapter: BackendAdapter 54 | switch (config.backend.type) { 55 | case 'memory': 56 | adapter = new SparqlAdapter( 57 | new OxigraphInmemBackend(config.backend.memory.baseIri), 58 | io.logger, 59 | ) 60 | break 61 | case 'sparqlEndpoint': 62 | adapter = new SparqlAdapter( 63 | new SparqlFetchBackend(config.backend.sparqlEndpoint.url), 64 | io.logger, 65 | ) 66 | break 67 | } 68 | 69 | this.api = new ApiInstance(adapter, new URL(config.server.apiUrl)) 70 | 71 | this.app = express() 72 | 73 | // TODO Allow CORS to be configured in production 74 | this.app.use(cors()) 75 | 76 | // enable per-request logging 77 | this.app.use( 78 | phttp({ 79 | // delegate to parent logger 80 | logger: this.io.logger, 81 | customLogLevel: (_req, res, err) => { 82 | if (res.statusCode >= 400 || err) { 83 | return 'info' 84 | } 85 | 86 | return 'debug' 87 | }, 88 | }), 89 | ) 90 | 91 | // app.use(bodyParser.urlencoded({ extended: false })) 92 | // app.use(bodyParser.json()) 93 | 94 | const s = initServer() 95 | 96 | const router = s.router(contract, { 97 | getInfo: async () => { 98 | const info = this.api.get_info() 99 | 100 | return { 101 | status: 200, 102 | body: info, 103 | } 104 | }, 105 | getDocmapById: async (req) => { 106 | const iri = decodeURIComponent(req.params.id) 107 | const result = await this.api.get_docmap_by_id(iri)() 108 | 109 | if (isLeft(result)) { 110 | return { 111 | status: 501, // FIXME: more expressive errors. 112 | body: { message: result.left.message }, 113 | } 114 | } 115 | 116 | return { 117 | status: 200, 118 | body: result.right, 119 | } 120 | }, 121 | getDocmapForDoi: async (req) => { 122 | const doi = req.query.subject 123 | const result = await this.api.get_docmap_for_thing({ identifier: doi, kind: 'doi' })() 124 | 125 | if (isLeft(result)) { 126 | return { 127 | status: 404, // FIXME: more expressive errors. 128 | body: { message: result.left.message }, 129 | } 130 | } 131 | 132 | return { 133 | status: 200, 134 | body: result.right, 135 | } 136 | }, 137 | }) 138 | 139 | createExpressEndpoints(contract, router, this.app, { logInitialization: false }) 140 | this.server = createHttpServer(this.app) 141 | } 142 | 143 | listen(): Promise { 144 | this.io.logger.info('opening listener...') 145 | return new Promise((res, _rej) => { 146 | // TODO : set listen timeout handling 147 | this.server.listen(this.config.server.port, () => { 148 | this.io.logger.info(`Listening at http://localhost:${this.config.server.port}`) 149 | res() 150 | }) 151 | }) 152 | } 153 | 154 | close(): Promise { 155 | return new Promise((res, _rej) => { 156 | // TODO : set close timeout handling 157 | this.server.close(() => { 158 | this.io.logger.info(`Closing server...`) 159 | res() 160 | }) 161 | }) 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /packages/etl/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import * as D from '@docmaps/sdk' 2 | import * as E from 'fp-ts/lib/Either' 3 | import { pipe } from 'fp-ts/lib/function' 4 | import type { ErrorOrDocmap } from '../types' 5 | 6 | /* ******* 7 | * Utils 8 | * ******* 9 | * 10 | * This package contains utility functions that are good candidates for 11 | * adoption into the core SDK but currently have only known uses in this 12 | * ETL package. 13 | */ 14 | 15 | /** mapLeftToUnknownError - helper function for interoperating between Either types 16 | * 17 | * specifically, io-ts codecs are always of type Either, and that 18 | * validation error is not naturally upcastable to Error where we use Either. 19 | */ 20 | export const mapLeftToUnknownError = (m = 'unknown error in @docmaps/etl') => 21 | E.mapLeft((e: unknown) => { 22 | return new Error(`error: ${m}`, { cause: e }) 23 | }) 24 | 25 | export function nameForAuthor(a: { family: string; name?: string; given?: string }): string { 26 | // FIXME this seems presumptuous 27 | return a.name || (a.given ? `${a.family}, ${a.given}` : a.family) 28 | } 29 | 30 | // NOTE: possibly this wants to be in the core sdk, but because docmaps 31 | // contain info about authorship, i am not so sure --- might require too 32 | // much configuration. 33 | // 34 | // This is slightly sane because while steps have keys like `first-step` 35 | // and `next-step`, these keys do not mean anything outside context of docmap. 36 | // possibly long term this makes a case for rdf-star. 37 | 38 | /** 39 | * stepArrayToDocmap - a helper function that processes a list of steps into a coherent docmap 40 | * 41 | * This function is needed because while a recursive process can produce a list of steps, 42 | * those steps are not inherently doubly-linked the way they need to be in a docmap. 43 | * (i.e., the Steps are each created independent from each other based on the crossref 44 | * data for each DOI, but they need to be connected when they become a Workflow.) 45 | * we additionally insert any step-independent info that is pertinent to the docmap, such as 46 | * the Publisher of the docmap. 47 | * 48 | * This is an awkward moment that breaks some of the functional abstraction (see comments). 49 | */ 50 | export function stepArrayToDocmap( 51 | publisher: D.PublisherT, 52 | inputDoi: string, 53 | [firstStep, ...steps]: D.StepT[], 54 | ): ErrorOrDocmap { 55 | // TODO: extract this logic 56 | const dm_id = `https://docmaps-project.github.io/ex/docmap_for/${inputDoi}` 57 | 58 | const now = new Date() 59 | 60 | let bnodeId = 0 61 | 62 | const dmBody = { 63 | type: 'docmap', 64 | id: dm_id, 65 | publisher: publisher, 66 | created: now, // FIXME does it have to be a string? 67 | updated: now, // FIXME does it have to be a string? 68 | } 69 | 70 | if (!firstStep) { 71 | const dmObject = D.Docmap.decode(dmBody) 72 | 73 | if (E.isLeft(dmObject)) { 74 | return E.left(new Error('unable to parse manuscript step', { cause: dmObject.left })) 75 | } 76 | 77 | return E.right([dmObject.right]) 78 | } 79 | 80 | // this reduction takes advantage of the fact that we have separated the firstStep 81 | // from the ...steps argument, because the first & last step is only singly linked 82 | // (see the last argument to #reduce). 83 | const reduction = steps.reduce>>( 84 | (memo, next) => { 85 | if (E.isLeft(memo)) { 86 | return memo //cascade all errors 87 | } 88 | 89 | const m = memo.right 90 | 91 | const previousId = `_:b${bnodeId}` 92 | bnodeId += 1 93 | const thisId = `_:b${bnodeId}` 94 | 95 | const prev = m[previousId] 96 | if (!prev) { 97 | return E.left( 98 | new Error( 99 | `algorithm error: step memo was missing step for id ${previousId} but was processing step with id ${thisId}`, 100 | ), 101 | ) 102 | } 103 | m[previousId] = { 104 | ...prev, 105 | 'next-step': thisId, 106 | } 107 | m[thisId] = { 108 | ...next, 109 | 'previous-step': previousId, 110 | } 111 | 112 | return E.right(m) 113 | }, 114 | // initial memo: the first step only, whose next-step is inserted during 115 | // the reduction loop and who doesn't need a first-step. since the reduce 116 | // is creating an Either, we begin with an Either that never fails. 117 | E.right({ 118 | '_:b0': firstStep, 119 | }), 120 | ) 121 | 122 | return pipe( 123 | reduction, 124 | E.map((r) => ({ 125 | ...dmBody, 126 | 'first-step': '_:b0', 127 | steps: r, 128 | })), 129 | E.chain((b) => pipe(b, D.Docmap.decode, mapLeftToUnknownError('decoding docmap'))), 130 | // requires to output Array of docmap 131 | E.map((d) => [d]), 132 | ) 133 | } 134 | -------------------------------------------------------------------------------- /packages/http-server/test/integration/httpserver.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import * as D from '@docmaps/sdk' 3 | import { inspect } from 'util' 4 | import { API_VERSION } from '../../src' 5 | import { MakeHttpClient } from '@docmaps/http-client' 6 | import { withNewServer } from './utils' 7 | 8 | import { ErrorBody } from '@docmaps/http-client' 9 | 10 | //FIXME: the close step is by far the longest in server setup, so 11 | // these should not shut down express server between tests. 12 | test.serial('it serves info endpoint', async (t) => { 13 | await withNewServer(async (_s) => { 14 | const client = MakeHttpClient({ 15 | baseUrl: 'http://localhost:33033', 16 | baseHeaders: {}, 17 | }) 18 | 19 | const info = await client.getInfo() 20 | 21 | t.is(info.status, 200) 22 | t.is(info.headers.get('Access-Control-Allow-Origin'), '*') 23 | t.deepEqual(info.body, { 24 | // FIXME: this is technically a lie, because it is not prefixed /docmaps/v1 25 | api_url: 'http://localhost:33033/docmaps/v1/', 26 | api_version: API_VERSION, 27 | ephemeral_document_expiry: { 28 | max_retrievals: 1, 29 | max_seconds: 60, 30 | }, 31 | peers: [], 32 | }) 33 | }, t.log) 34 | }) 35 | 36 | test.serial('it serves /docmap endpoint', async (t) => { 37 | await withNewServer(async (_s) => { 38 | const client = MakeHttpClient({ 39 | baseUrl: 'http://localhost:33033', 40 | baseHeaders: {}, 41 | }) 42 | const testIri = 43 | 'https://data-hub-api.elifesciences.org/enhanced-preprints/docmaps/v1/get-by-doi?preprint_doi=10.1101%2F2022.11.08.515698' 44 | 45 | const resp = await client.getDocmapById({ 46 | params: { id: encodeURI(encodeURIComponent(testIri)) }, 47 | }) 48 | 49 | t.is(resp.status, 200, `failed with this response: ${inspect(resp, { depth: null })}`) 50 | 51 | const dm = resp.body as D.DocmapT 52 | 53 | t.deepEqual(dm.id, testIri) 54 | t.deepEqual(dm.publisher, { 55 | account: { 56 | id: 'https://sciety.org/groups/elife', 57 | service: 'https://sciety.org/', 58 | }, 59 | homepage: 'https://elifesciences.org/', 60 | id: 'https://elifesciences.org/', 61 | logo: 'https://sciety.org/static/groups/elife--b560187e-f2fb-4ff9-a861-a204f3fc0fb0.png', 62 | name: 'eLife', 63 | }) 64 | }, t.log) 65 | }) 66 | 67 | test.serial('it serves /docmap_for/doi endpoint', async (t) => { 68 | await withNewServer(async (_s) => { 69 | const client = MakeHttpClient({ 70 | baseUrl: 'http://localhost:33033', 71 | baseHeaders: {}, 72 | }) 73 | const testDoi1 = '10.1101/2021.03.24.436774' 74 | 75 | const resp = await client.getDocmapForDoi({ 76 | query: { subject: testDoi1 }, 77 | }) 78 | 79 | t.is(resp.status, 200, `failed with this response: ${inspect(resp, { depth: null })}`) 80 | 81 | const dm = resp.body as D.DocmapT 82 | 83 | t.deepEqual(dm.id, 'https://eeb.embo.org/api/v2/docmap/10.1101/2021.03.24.436774') 84 | t.deepEqual(dm.publisher, { 85 | name: 'review commons', 86 | url: 'https://reviewcommons.org/', 87 | }) 88 | 89 | // test handling case where multiples exist 90 | const testDoi2 = '10.1101/2022.11.08.515698' 91 | 92 | const resp2 = await client.getDocmapForDoi({ 93 | query: { subject: testDoi2 }, 94 | }) 95 | 96 | t.is(resp2.status, 200, `failed with this response: ${inspect(resp, { depth: null })}`) 97 | 98 | const dm2 = resp2.body as D.DocmapT 99 | 100 | t.deepEqual( 101 | dm2.id, 102 | 'https://data-hub-api.elifesciences.org/enhanced-preprints/docmaps/v1/get-by-doi?preprint_doi=10.1101%2F2022.11.08.515698', 103 | ) 104 | t.deepEqual(dm2.publisher, { 105 | account: { 106 | id: 'https://sciety.org/groups/elife', 107 | service: 'https://sciety.org/', 108 | }, 109 | homepage: 'https://elifesciences.org/', 110 | id: 'https://elifesciences.org/', 111 | logo: 'https://sciety.org/static/groups/elife--b560187e-f2fb-4ff9-a861-a204f3fc0fb0.png', 112 | name: 'eLife', 113 | }) 114 | }, t.log) 115 | }) 116 | 117 | test.serial('it errors with helpful body in /docmap_for/doi endpoint', async (t) => { 118 | await withNewServer(async (_s) => { 119 | const client = MakeHttpClient({ 120 | baseUrl: 'http://localhost:33033', 121 | baseHeaders: {}, 122 | }) 123 | const testDoi1 = '10.1101/notPresent' 124 | 125 | const resp = await client.getDocmapForDoi({ 126 | query: { subject: testDoi1 }, 127 | }) 128 | 129 | t.is( 130 | resp.status, 131 | 404, 132 | `expected 404 but got ${resp.status} response: ${inspect(resp, { depth: null })}`, 133 | ) 134 | 135 | const error = resp.body as ErrorBody 136 | 137 | t.deepEqual(error, { message: 'zero quads found for query' }) 138 | }, t.log) 139 | }) 140 | -------------------------------------------------------------------------------- /packages/etl/test/unit/processor.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { inspect } from 'util' 3 | import { isRight, isLeft } from 'fp-ts/lib/Either' 4 | import * as TE from 'fp-ts/lib/TaskEither' 5 | import { stepsForIdRecursive } from '../../src/processor' 6 | import { whenThenRight } from './utils' 7 | import * as am from './__fixtures__/abstract' 8 | import type * as D from '@docmaps/sdk' 9 | import { when } from 'ts-mockito' 10 | 11 | const nothing = [] as D.ThingT[] 12 | 13 | test('happy-path scenario: a manuscript with one preprint and no reviews', async (t) => { 14 | const mocks = am.AbstractPluginMocks() 15 | whenThenRight(mocks.pluginT.stepForId, am.MANUSCRIPT_ID, am.mockManuscriptWithPreprintResponse) 16 | whenThenRight(mocks.pluginT.stepForId, am.PREPRINT_ID, am.mockPreprintWithManuscriptResponse) 17 | 18 | const routine = stepsForIdRecursive(mocks.plugin, am.MANUSCRIPT_ID, new Set(), { 19 | inputs: nothing, 20 | }) 21 | 22 | const res = await routine() 23 | if (isLeft(res)) { 24 | t.fail(`Got error instead of steps: ${res.left}`) 25 | return 26 | } 27 | 28 | const steps = res.right.all 29 | t.is(steps.length, 2) 30 | 31 | t.deepEqual(steps[0]?.actions[0]?.inputs, []) 32 | t.deepEqual(steps[0]?.actions[0]?.outputs[0]?.id, am.PREPRINT_ID) 33 | t.deepEqual(steps[1]?.actions[0]?.inputs[0]?.id, am.PREPRINT_ID) 34 | t.deepEqual(steps[1]?.actions[0]?.outputs[0]?.id, am.MANUSCRIPT_ID) 35 | //TODO: can write stronger assertions as we learn what this should look like 36 | }) 37 | 38 | test('happy-path scenario: a manuscript discovered from its preprint', async (t) => { 39 | const mocks = am.AbstractPluginMocks() 40 | whenThenRight(mocks.pluginT.stepForId, am.PREPRINT_ID, am.mockPreprintWithManuscriptResponse) 41 | whenThenRight(mocks.pluginT.stepForId, am.MANUSCRIPT_ID, am.mockManuscriptWithPreprintResponse) 42 | 43 | const routine = stepsForIdRecursive(mocks.plugin, am.PREPRINT_ID, new Set(), { 44 | inputs: nothing, 45 | }) 46 | 47 | const res = await routine() 48 | if (isLeft(res)) { 49 | t.fail(`Got error instead of steps: ${res.left}`) 50 | return 51 | } 52 | 53 | const steps = res.right.all 54 | t.is(steps.length, 2) 55 | 56 | t.deepEqual(steps[0]?.actions[0]?.inputs, []) 57 | t.deepEqual(steps[0]?.actions[0]?.outputs[0]?.id, am.PREPRINT_ID) 58 | t.deepEqual(steps[1]?.actions[0]?.inputs[0]?.id, am.PREPRINT_ID) 59 | t.deepEqual(steps[1]?.actions[0]?.outputs[0]?.id, am.MANUSCRIPT_ID) 60 | //TODO: can write stronger assertions as we learn what this should look like 61 | }) 62 | 63 | test('happy-path scenario: a manuscript with no relations', async (t) => { 64 | const mocks = am.AbstractPluginMocks() 65 | whenThenRight(mocks.pluginT.stepForId, am.MANUSCRIPT_ID, am.mockManuscriptResponse) 66 | 67 | const routine = stepsForIdRecursive(mocks.plugin, am.MANUSCRIPT_ID, new Set(), { 68 | inputs: nothing, 69 | }) 70 | 71 | const res = await routine() 72 | if (isLeft(res)) { 73 | t.fail(`Got error instead of steps: ${res.left}`) 74 | return 75 | } 76 | 77 | const steps = res.right.all 78 | t.is(steps.length, 1) 79 | 80 | t.deepEqual(steps[0]?.actions[0]?.inputs, []) 81 | t.deepEqual(steps[0]?.actions[0]?.outputs[0]?.id, am.MANUSCRIPT_ID) 82 | }) 83 | 84 | test('happy-path scenario: a manuscript with 2 reviews and no preprint', async (t) => { 85 | const mocks = am.AbstractPluginMocks() 86 | whenThenRight(mocks.pluginT.stepForId, am.MANUSCRIPT_ID, am.mockManuscriptWithReviewsResponse) 87 | whenThenRight(mocks.pluginT.actionForReviewId, am.REVIEW_1_ID, am.reviewActions[0]) 88 | whenThenRight(mocks.pluginT.actionForReviewId, am.REVIEW_2_ID, am.reviewActions[1]) 89 | 90 | const routine = stepsForIdRecursive(mocks.plugin, am.MANUSCRIPT_ID, new Set(), { 91 | inputs: nothing, 92 | }) 93 | 94 | const res = await routine() 95 | if (isLeft(res)) { 96 | t.fail(`Got error instead of steps: ${inspect(res.left, { depth: null })}`) 97 | return 98 | } 99 | 100 | const steps = res.right.all 101 | t.is(steps.length, 2) 102 | 103 | t.deepEqual(steps[0]?.actions[0]?.inputs, []) 104 | t.deepEqual(steps[0]?.actions[0]?.outputs[0]?.id, am.MANUSCRIPT_ID) 105 | 106 | t.deepEqual(steps[1]?.actions[0]?.inputs[0]?.id, am.MANUSCRIPT_ID) 107 | t.deepEqual(steps[1]?.actions[0]?.outputs[0]?.id, am.REVIEW_1_ID) 108 | 109 | t.deepEqual(steps[1]?.actions[1]?.outputs[0]?.id, am.REVIEW_2_ID) 110 | }) 111 | 112 | test('error case: if the plugin produces an error ', async (t) => { 113 | const mocks = am.AbstractPluginMocks() 114 | when(mocks.pluginT.stepForId).thenReturn(() => TE.left(new Error('fake error!!'))) 115 | const routine = stepsForIdRecursive(mocks.plugin, am.MANUSCRIPT_ID, new Set(), { 116 | inputs: nothing, 117 | }) 118 | 119 | const res = await routine() 120 | 121 | if (isRight(res)) { 122 | t.fail(`Got docmaps instead of error: ${res.right}`) 123 | return 124 | } 125 | 126 | t.regex(res.left.message, /fake error!!/) 127 | }) 128 | -------------------------------------------------------------------------------- /packages/widget/test/fixtures/anotherDocmapWithOneStep.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | id: 'https://sciety.org/docmaps/v1/articles/10.21203/rs.3.rs-3171736/v1/rapid-reviews-covid-19.docmap.json', 3 | type: 'docmap', 4 | created: '2023-08-25T04:32:28.335Z', 5 | publisher: { 6 | id: 'https://rapidreviewscovid19.mitpress.mit.edu/', 7 | homepage: 'https://rapidreviewscovid19.mitpress.mit.edu/', 8 | logo: 'https://sciety.org/static/groups/rapid-reviews-covid-19--5142a5bc-6b18-42b1-9a8d-7342d7d17e94.png', 9 | name: 'Rapid Reviews Infectious Diseases', 10 | account: { 11 | id: 'https://sciety.org/groups/rapid-reviews-covid-19', 12 | service: 'https://sciety.org/', 13 | }, 14 | }, 15 | 'first-step': '_:b21', 16 | steps: { 17 | '_:b21': { 18 | inputs: [ 19 | { 20 | doi: '10.21203/rs.3.rs-3171736/v1', 21 | url: 'https://doi.org/10.21203/rs.3.rs-3171736/v1', 22 | }, 23 | ], 24 | actions: [ 25 | { 26 | participants: [ 27 | { 28 | actor: { 29 | type: 'person', 30 | name: 'Sachiko Koyama', 31 | }, 32 | role: 'peer-reviewer', 33 | }, 34 | { 35 | actor: { 36 | type: 'person', 37 | name: 'Rafa Khan', 38 | }, 39 | role: 'peer-reviewer', 40 | }, 41 | { 42 | actor: { 43 | type: 'person', 44 | name: 'Richard Doty', 45 | }, 46 | role: 'peer-reviewer', 47 | }, 48 | ], 49 | outputs: [ 50 | { 51 | type: 'review-article', 52 | published: '2023-08-24T06:29:28.000Z', 53 | content: [ 54 | { 55 | type: 'web-page', 56 | url: 'http://dx.doi.org/10.1162/2e3983f5.993b8d46', 57 | }, 58 | { 59 | type: 'web-content', 60 | url: 'https://sciety.org/evaluations/rapidreviews:http://dx.doi.org/10.1162/2e3983f5.993b8d46/content', 61 | }, 62 | { 63 | type: 'web-page', 64 | url: 'https://sciety.org/articles/activity/10.21203/rs.3.rs-3171736/v1#rapidreviews:http://dx.doi.org/10.1162/2e3983f5.993b8d46', 65 | }, 66 | ], 67 | }, 68 | ], 69 | }, 70 | { 71 | participants: [ 72 | { 73 | actor: { 74 | type: 'person', 75 | name: 'Sachiko Koyama', 76 | }, 77 | role: 'peer-reviewer', 78 | }, 79 | ], 80 | outputs: [ 81 | { 82 | type: 'review-article', 83 | published: '2023-08-24T06:30:01.000Z', 84 | content: [ 85 | { 86 | type: 'web-content', 87 | url: 'https://sciety.org/evaluations/rapidreviews:http://dx.doi.org/10.1162/2e3983f5.386498d8/content', 88 | }, 89 | { 90 | type: 'web-page', 91 | url: 'https://sciety.org/articles/activity/10.21203/rs.3.rs-3171736/v1#rapidreviews:http://dx.doi.org/10.1162/2e3983f5.386498d8', 92 | }, 93 | { 94 | type: 'web-page', 95 | url: 'http://dx.doi.org/10.1162/2e3983f5.386498d8', 96 | }, 97 | ], 98 | }, 99 | ], 100 | }, 101 | { 102 | participants: [ 103 | { 104 | actor: { 105 | type: 'person', 106 | name: 'Richard Doty', 107 | }, 108 | role: 'peer-reviewer', 109 | }, 110 | { 111 | actor: { 112 | type: 'person', 113 | name: 'Rafa Khan', 114 | }, 115 | role: 'peer-reviewer', 116 | }, 117 | ], 118 | outputs: [ 119 | { 120 | type: 'review-article', 121 | published: '2023-08-24T06:30:09.000Z', 122 | content: [ 123 | { 124 | type: 'web-page', 125 | url: 'https://sciety.org/articles/activity/10.21203/rs.3.rs-3171736/v1#rapidreviews:http://dx.doi.org/10.1162/2e3983f5.cf4866e5', 126 | }, 127 | { 128 | type: 'web-content', 129 | url: 'https://sciety.org/evaluations/rapidreviews:http://dx.doi.org/10.1162/2e3983f5.cf4866e5/content', 130 | }, 131 | { 132 | type: 'web-page', 133 | url: 'http://dx.doi.org/10.1162/2e3983f5.cf4866e5', 134 | }, 135 | ], 136 | }, 137 | ], 138 | }, 139 | ], 140 | assertions: [], 141 | }, 142 | }, 143 | '@context': 'https://w3id.org/docmaps/context.jsonld', 144 | }; 145 | -------------------------------------------------------------------------------- /packages/widget/test/integration/util.ts: -------------------------------------------------------------------------------- 1 | import { Page, Request, Route } from '@playwright/test'; 2 | import { DocmapsWidget } from '../../src'; 3 | import docmapWithOneStep from '../fixtures/docmapWithOneStep'; 4 | 5 | 6 | const STAGING_SERVER_URL: string = 'https://example.com'; 7 | 8 | export async function renderWidgetWithServerMock(page: Page, doi: string, docmap: any) { 9 | await mockDocmapForEndpoint(page, doi, docmap); 10 | return await renderWidget(page, doi); 11 | } 12 | 13 | export async function renderWidgetWithUnknownDOI(page: Page) { 14 | // We pass in a docmap that we don't use for anything. 15 | // This is really just a hack that lets us use our existing method that mocks out the docmap for endpoint. 16 | await mockDocmapForEndpoint(page, "not-the-doi-we-will-request", docmapWithOneStep); 17 | return await renderWidget(page, "unknown-doi"); 18 | } 19 | 20 | export async function renderWidgetWithDocmapLiteral(page: Page, docmap: any) { 21 | // This approach is inspired by https://github.com/microsoft/playwright/issues/14241#issuecomment-1488829515 22 | await page.goto('/'); 23 | await page.evaluate( 24 | ({ docmap }) => { 25 | const root = document.querySelector('#root'); 26 | if (root) { 27 | root.innerHTML = ``; 28 | } 29 | 30 | customElements.whenDefined('docmaps-widget').then(() => { 31 | const widgetElement = document.getElementById('test-docmap'); 32 | (widgetElement as DocmapsWidget).docmap = docmap; 33 | }); 34 | }, 35 | { docmap }, // This is not a regular closure, so we need to pass in the variables we want to use 36 | ); 37 | await page.waitForSelector('#test-docmap'); 38 | 39 | return page.locator('#test-docmap'); 40 | } 41 | 42 | export async function renderWidget(page: Page, doi: string) { 43 | // This approach is inspired by https://github.com/microsoft/playwright/issues/14241#issuecomment-1488829515 44 | await page.goto('/'); 45 | await page.evaluate( 46 | ({ serverUrl, doi }) => { 47 | const root = document.querySelector('#root'); 48 | if (root) { 49 | root.innerHTML = ``; 50 | } 51 | }, 52 | { serverUrl: STAGING_SERVER_URL, doi }, // This is not a regular closure, so we need to pass in the variables we want to use 53 | ); 54 | await page.waitForSelector('docmaps-widget'); 55 | 56 | return page.locator('docmaps-widget'); 57 | } 58 | 59 | /** 60 | * Mocks out the api server's `/docmap_for/doi?subject=` endpoint to return a specific docmap 61 | */ 62 | export async function mockDocmapForEndpoint(page: Page, doi: string, docmapToReturn: any) { 63 | const urlsToMock = (url: URL): boolean => url.toString().includes(STAGING_SERVER_URL); 64 | 65 | const mockHandler = async (route: Route, request: Request) => { 66 | let response: { body: string; status: number; contentType?: string } = { 67 | status: 400, 68 | body: `MOCK SERVER: No docmap found for doi '${doi}'`, 69 | }; 70 | 71 | if (request.url().includes(doi)) { 72 | response = { 73 | status: 200, 74 | contentType: 'application/json', 75 | body: JSON.stringify(docmapToReturn), 76 | }; 77 | } 78 | 79 | await route.fulfill(response); 80 | }; 81 | 82 | await page.route(urlsToMock, mockHandler); 83 | } 84 | 85 | export const TYPE_UNKNOWN_DETAIL_HEADER_COLOR: string = '#777'; 86 | 87 | // TODO I don't like that this is basically a copy of the giant object in docmaps-widget.ts 88 | // But unfortunately it's not as trivial as you'd expect to import the options from the source code 89 | export const typeShortLabelToOpts: { 90 | [key: string]: { longLabel: string; backgroundColor: string; textColor: string }; 91 | } = { 92 | R: { 93 | longLabel: 'Review', 94 | backgroundColor: '#1E2F48', 95 | textColor: '#D4E5FF', 96 | }, 97 | P: { 98 | longLabel: 'Preprint', 99 | backgroundColor: '#077A12', 100 | textColor: '#CBFFD0', 101 | }, 102 | ES: { 103 | longLabel: 'Evaluation Summary', 104 | backgroundColor: '#936308', 105 | textColor: '#FFF1D8', 106 | }, 107 | RA: { 108 | longLabel: 'Review Article', 109 | backgroundColor: '#099CEE', 110 | textColor: '#E7F6FF', 111 | }, 112 | JA: { 113 | longLabel: 'Journal Article', 114 | backgroundColor: '#880052', 115 | textColor: '#FFE3F4', 116 | }, 117 | ED: { 118 | longLabel: 'Editorial', 119 | backgroundColor: '#2A8781', 120 | textColor: '#E8FFFE', 121 | }, 122 | CO: { 123 | longLabel: 'Comment', 124 | backgroundColor: '#B66248', 125 | textColor: '#FFF0EB', 126 | }, 127 | RE: { 128 | longLabel: 'Reply', 129 | backgroundColor: '#79109E', 130 | textColor: '#F9E9FF', 131 | }, 132 | '': { 133 | longLabel: 'Type unknown', 134 | backgroundColor: '#CDCDCD', 135 | textColor: '#043945', 136 | }, 137 | }; 138 | 139 | export const typeToDetailBackgroundColor = (type: string) => 140 | type === '' ? TYPE_UNKNOWN_DETAIL_HEADER_COLOR : typeShortLabelToOpts[type].backgroundColor; 141 | -------------------------------------------------------------------------------- /examples/docmaps-example-epmc-01.jsonld: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://w3id.org/docmaps/context.jsonld", 3 | "id": "https://sciety.org/docmaps/v1/articles/10.21203/rs.3.rs-3171736/v1/rapid-reviews-covid-19.docmap.json", 4 | "type": "docmap", 5 | "created": "2023-08-25T04:32:28.335Z", 6 | "updated": "2023-08-25T05:01:25.436Z", 7 | "publisher": { 8 | "id": "https://rapidreviewscovid19.mitpress.mit.edu/", 9 | "name": "Rapid Reviews Infectious Diseases", 10 | "logo": "https://sciety.org/static/groups/rapid-reviews-covid-19--5142a5bc-6b18-42b1-9a8d-7342d7d17e94.png", 11 | "homepage": "https://rapidreviewscovid19.mitpress.mit.edu/", 12 | "account": { 13 | "id": "https://sciety.org/groups/rapid-reviews-covid-19", 14 | "service": "https://sciety.org" 15 | } 16 | }, 17 | "first-step": "_:b0", 18 | "steps": { 19 | "_:b0": { 20 | "assertions": [], 21 | "inputs": [ 22 | { 23 | "doi": "10.21203/rs.3.rs-3171736/v1", 24 | "url": "https://doi.org/10.21203/rs.3.rs-3171736/v1" 25 | } 26 | ], 27 | "actions": [ 28 | { 29 | "participants": [ 30 | { 31 | "actor": { 32 | "name": "Sachiko Koyama", 33 | "type": "person" 34 | }, 35 | "role": "peer-reviewer" 36 | }, 37 | { 38 | "actor": { 39 | "name": "Richard Doty", 40 | "type": "person" 41 | }, 42 | "role": "peer-reviewer" 43 | }, 44 | { 45 | "actor": { 46 | "name": "Rafa Khan", 47 | "type": "person" 48 | }, 49 | "role": "peer-reviewer" 50 | } 51 | ], 52 | "outputs": [ 53 | { 54 | "type": "review-article", 55 | "published": "2023-08-24T06:29:28.000Z", 56 | "content": [ 57 | { 58 | "type": "web-page", 59 | "url": "http://dx.doi.org/10.1162/2e3983f5.993b8d46" 60 | }, 61 | { 62 | "type": "web-page", 63 | "url": "https://sciety.org/articles/activity/10.21203/rs.3.rs-3171736/v1#rapidreviews:http://dx.doi.org/10.1162/2e3983f5.993b8d46" 64 | }, 65 | { 66 | "type": "web-content", 67 | "url": "https://sciety.org/evaluations/rapidreviews:http://dx.doi.org/10.1162/2e3983f5.993b8d46/content" 68 | } 69 | ] 70 | } 71 | ] 72 | }, 73 | { 74 | "participants": [ 75 | { 76 | "actor": { 77 | "name": "Richard Doty", 78 | "type": "person" 79 | }, 80 | "role": "peer-reviewer" 81 | }, 82 | { 83 | "actor": { 84 | "name": "Rafa Khan", 85 | "type": "person" 86 | }, 87 | "role": "peer-reviewer" 88 | } 89 | ], 90 | "outputs": [ 91 | { 92 | "type": "review-article", 93 | "published": "2023-08-24T06:30:09.000Z", 94 | "content": [ 95 | { 96 | "type": "web-page", 97 | "url": "http://dx.doi.org/10.1162/2e3983f5.cf4866e5" 98 | }, 99 | { 100 | "type": "web-page", 101 | "url": "https://sciety.org/articles/activity/10.21203/rs.3.rs-3171736/v1#rapidreviews:http://dx.doi.org/10.1162/2e3983f5.cf4866e5" 102 | }, 103 | { 104 | "type": "web-content", 105 | "url": "https://sciety.org/evaluations/rapidreviews:http://dx.doi.org/10.1162/2e3983f5.cf4866e5/content" 106 | } 107 | ] 108 | } 109 | ] 110 | }, 111 | { 112 | "participants": [ 113 | { 114 | "actor": { 115 | "name": "Sachiko Koyama", 116 | "type": "person" 117 | }, 118 | "role": "peer-reviewer" 119 | } 120 | ], 121 | "outputs": [ 122 | { 123 | "type": "review-article", 124 | "published": "2023-08-24T06:30:01.000Z", 125 | "content": [ 126 | { 127 | "type": "web-page", 128 | "url": "http://dx.doi.org/10.1162/2e3983f5.386498d8" 129 | }, 130 | { 131 | "type": "web-page", 132 | "url": "https://sciety.org/articles/activity/10.21203/rs.3.rs-3171736/v1#rapidreviews:http://dx.doi.org/10.1162/2e3983f5.386498d8" 133 | }, 134 | { 135 | "type": "web-content", 136 | "url": "https://sciety.org/evaluations/rapidreviews:http://dx.doi.org/10.1162/2e3983f5.386498d8/content" 137 | } 138 | ] 139 | } 140 | ] 141 | } 142 | ] 143 | } 144 | } 145 | } 146 | --------------------------------------------------------------------------------