From b5f391b0ff88440a736408671c9898fb8a437aa0 Mon Sep 17 00:00:00 2001 From: early evening Date: Wed, 10 May 2023 15:09:04 -0700 Subject: [PATCH 01/29] add basic spa package -- fixed DOI example --- packages/spa/.gitignore | 4 + packages/spa/README.md | 107 +++++++++++++++++++ packages/spa/package.json | 29 +++++ packages/spa/public/favicon.png | Bin 0 -> 3127 bytes packages/spa/public/global.css | 63 +++++++++++ packages/spa/public/index.html | 18 ++++ packages/spa/rollup.config.js | 78 ++++++++++++++ packages/spa/scripts/setupTypeScript.js | 134 ++++++++++++++++++++++++ packages/spa/src/App.svelte | 65 ++++++++++++ packages/spa/src/main.js | 9 ++ packages/spa/src/utils.js | 31 ++++++ 11 files changed, 538 insertions(+) create mode 100644 packages/spa/.gitignore create mode 100644 packages/spa/README.md create mode 100644 packages/spa/package.json create mode 100644 packages/spa/public/favicon.png create mode 100644 packages/spa/public/global.css create mode 100644 packages/spa/public/index.html create mode 100644 packages/spa/rollup.config.js create mode 100644 packages/spa/scripts/setupTypeScript.js create mode 100644 packages/spa/src/App.svelte create mode 100644 packages/spa/src/main.js create mode 100644 packages/spa/src/utils.js diff --git a/packages/spa/.gitignore b/packages/spa/.gitignore new file mode 100644 index 00000000..da93220b --- /dev/null +++ b/packages/spa/.gitignore @@ -0,0 +1,4 @@ +/node_modules/ +/public/build/ + +.DS_Store diff --git a/packages/spa/README.md b/packages/spa/README.md new file mode 100644 index 00000000..d488b3c7 --- /dev/null +++ b/packages/spa/README.md @@ -0,0 +1,107 @@ +# This repo is no longer maintained. Consider using `npm init vite` and selecting the `svelte` option or — if you want a full-fledged app framework — use [SvelteKit](https://kit.svelte.dev), the official application framework for Svelte. + +--- + +# svelte app + +This is a project template for [Svelte](https://svelte.dev) apps. It lives at https://github.com/sveltejs/template. + +To create a new project based on this template using [degit](https://github.com/Rich-Harris/degit): + +```bash +npx degit sveltejs/template svelte-app +cd svelte-app +``` + +*Note that you will need to have [Node.js](https://nodejs.org) installed.* + + +## Get started + +Install the dependencies... + +```bash +cd svelte-app +npm install +``` + +...then start [Rollup](https://rollupjs.org): + +```bash +npm run dev +``` + +Navigate to [localhost:8080](http://localhost:8080). You should see your app running. Edit a component file in `src`, save it, and reload the page to see your changes. + +By default, the server will only respond to requests from localhost. To allow connections from other computers, edit the `sirv` commands in package.json to include the option `--host 0.0.0.0`. + +If you're using [Visual Studio Code](https://code.visualstudio.com/) we recommend installing the official extension [Svelte for VS Code](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). If you are using other editors you may need to install a plugin in order to get syntax highlighting and intellisense. + +## Building and running in production mode + +To create an optimised version of the app: + +```bash +npm run build +``` + +You can run the newly built app with `npm run start`. This uses [sirv](https://github.com/lukeed/sirv), which is included in your package.json's `dependencies` so that the app will work when you deploy to platforms like [Heroku](https://heroku.com). + + +## Single-page app mode + +By default, sirv will only respond to requests that match files in `public`. This is to maximise compatibility with static fileservers, allowing you to deploy your app anywhere. + +If you're building a single-page app (SPA) with multiple routes, sirv needs to be able to respond to requests for *any* path. You can make it so by editing the `"start"` command in package.json: + +```js +"start": "sirv public --single" +``` + +## Using TypeScript + +This template comes with a script to set up a TypeScript development environment, you can run it immediately after cloning the template with: + +```bash +node scripts/setupTypeScript.js +``` + +Or remove the script via: + +```bash +rm scripts/setupTypeScript.js +``` + +If you want to use `baseUrl` or `path` aliases within your `tsconfig`, you need to set up `@rollup/plugin-alias` to tell Rollup to resolve the aliases. For more info, see [this StackOverflow question](https://stackoverflow.com/questions/63427935/setup-tsconfig-path-in-svelte). + +## Deploying to the web + +### With [Vercel](https://vercel.com) + +Install `vercel` if you haven't already: + +```bash +npm install -g vercel +``` + +Then, from within your project folder: + +```bash +cd public +vercel deploy --name my-project +``` + +### With [surge](https://surge.sh/) + +Install `surge` if you haven't already: + +```bash +npm install -g surge +``` + +Then, from within your project folder: + +```bash +npm run build +surge public my-project.surge.sh +``` diff --git a/packages/spa/package.json b/packages/spa/package.json new file mode 100644 index 00000000..b639636f --- /dev/null +++ b/packages/spa/package.json @@ -0,0 +1,29 @@ +{ + "name": "svelte-app", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "rollup -c", + "dev": "rollup -c -w", + "start": "sirv public --no-clear" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^24.0.0", + "@rollup/plugin-node-resolve": "^15.0.0", + "@rollup/plugin-terser": "^0.4.0", + "rollup": "^3.15.0", + "rollup-plugin-css-only": "^4.3.0", + "rollup-plugin-livereload": "^2.0.0", + "rollup-plugin-svelte": "^7.1.2", + "svelte": "^3.55.0" + }, + "dependencies": { + "@source-data/render-rev": "github:source-data/render-rev", + "@spider-ui/global-event-registry": ">= 0.2.3", + "docmaps-sdk": "^0.5.0", + "fp-ts": "^2.13.1", + "sirv-cli": "^2.0.0", + "upgraded-element": ">= 0.4.3" + } +} diff --git a/packages/spa/public/favicon.png b/packages/spa/public/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..7e6f5eb5a2f1f1c882d265cf479de25caa925645 GIT binary patch literal 3127 zcmV-749N3|P)i z7)}s4L53SJCkR}iVi00SFk;`MXX*#X*kkwKs@nFGS}c;=?XFjU|G$3t^5sjIVS2G+ zw)WGF83CpoGXhLGW(1gW%uV|X7>1P6VhCX=Ux)Lb!*DZ%@I3!{Gsf7d?gtIQ%nQiK z3%(LUSkBji;C5Rfgd6$VsF@H`Pk@xtY6t<>FNR-pD}=C~$?)9pdm3XZ36N5PNWYjb z$xd$yNQR9N!dfj-Vd@BwQo^FIIWPPmT&sZyQ$v81(sCBV=PGy{0wltEjB%~h157*t zvbe_!{=I_783x!0t1-r#-d{Y?ae$Q4N_Nd^Ui^@y(%)Gjou6y<3^XJdu{rmUf-Me?)zZ>9OR&6U5H*cK; z$gUlB{g0O4gN0sLSO|Of?hU(l?;h(jA3uH!Z{EBKuV23ouU@^Y6#%v+QG;>e*E}%?wlu-NT4DG zs)z)7WbLr)vGAu(ohrKc^em@OpO&f~6_>E61n_e0_V3@{U3^O;j{`^mNCJUj_>;7v zsMs6Hu3g7+@v+lSo;=yTYFqq}jZmQ-BK8K{C4kqi_i*jBaQE(Au0607V-zKeT;EPg zX(`vrn=L+e74+-Tqeok@_`tDa$G9I|$nTU5H*2V8@y()n*zqM?J1G!-1aX;CfDC9B zTnJ#j_%*n8Qb1)re*Bno7g0RG{Eb;IK14irJYJp$5Z6ac9~b_P?+5t~95~SRG$g?1 znFJ7p$xV&GZ18m~79TGRdfsc-BcX$9yXTR*n)mPD@1~O(_?cT$ZvFPucRmGlq&se0 zKrcUf^k}4hM*biEJOWKzz!qQe;CB_ZtSOO9Owg#lZAc=s65^rb{fZe(TYu_rk!wKkEf}RIt=#Om( zR8mN`DM<^xj~59euMMspBolVN zAPTr8sSDI104orIAdmL$uOXn*6hga1G+0WD0E?UtabxC#VC~vf3|10|phW;yQ3CY8 z2CM=)ErF;xq-YJ5G|um}>*1#E+O_Mu|Nr#qQ&G1P-NMq@f?@*XUcSbV?tX=)ilM-Q zBZP|!Bpv0V;#ojKcpc7$=eqO;#Uy~#?^kNI{vSZfLx&DEt~LTmaKWXcx=joubklI<*Aw z>LtMaQ7DR<1I2LkWvwyu#Rwn~;ezT}_g(@5l3h?W%-a86Y-t#O1PubP+z<%?V5D(U zy57A6{h+{?kOZp7&WKZR+=sznMJ}+Dnpo=C_0%R_x_t~J5T?E_{+))l5v1%52>)d-`iiZyx|5!%M2Fb2dU zW3~MwwpEH9Rhue+k$UIOoo($Ds!NbOyMR36fRHu;*15(YcA7siIZk#%JWz>P!qX1?IUojG&nKR>^gArBt2 zit(ETyZ=@V&7mv_Fi4bABcnwP+jzQuHcfU&BrAV91u-rFvEi7y-KnWsvHH=d2 zgAk(GKm_S8RcTJ>2N3~&Hbwp{Z3NF_Xeh}g4Eke)V&dY{W(3&b1j9t4yK_aYJisZZ{1rcU5- z;eD>K;ndPq&B-8yA_S0F!4ThA&{1{x)H<#?k9a#6Pc6L?V^s0``ynL&D;p(!Nmx`Y zFkHex{4p!Ggm^@DlehW}iHHVi}~u=$&N? z(NEBLQ#UxxAkdW>X9LnqUr#t4Lu0=9L8&o>JsqTtT5|%gb3QA~hr0pED71+iFFr)dZ=Q=E6ng{NE{Z~0)C?deO#?Aj zSDQ$z#TeC2T^|=}6GBo-&$;E{HL3!q3Z-szuf)O=G#zDjin4SSP%o%6+2IT#sLjQa ziyxFFz~LMjWY+_a5H!U6%a<=b7QVP^ z*90a62;bVq{?@)P6^DWd^Yilq4|YTV2Nw!Yu;a1lPI-sxR)rf@Fe5DhDP7FH zZZ%4S*1C30P;|O+jB!1;m|rXT90Sm5*RBbQN`PKu+hDD*S^yE(CdtSfg=z>u$cIj> z + + + + + + Docmaps Demonstration + + + + + + + + + + + diff --git a/packages/spa/rollup.config.js b/packages/spa/rollup.config.js new file mode 100644 index 00000000..d1d73065 --- /dev/null +++ b/packages/spa/rollup.config.js @@ -0,0 +1,78 @@ +import { spawn } from 'child_process'; +import svelte from 'rollup-plugin-svelte'; +import commonjs from '@rollup/plugin-commonjs'; +import terser from '@rollup/plugin-terser'; +import resolve from '@rollup/plugin-node-resolve'; +import livereload from 'rollup-plugin-livereload'; +import css from 'rollup-plugin-css-only'; + +const production = !process.env.ROLLUP_WATCH; + +function serve() { + let server; + + function toExit() { + if (server) server.kill(0); + } + + return { + writeBundle() { + if (server) return; + server = spawn('npm', ['run', 'start', '--', '--dev'], { + stdio: ['ignore', 'inherit', 'inherit'], + shell: true + }); + + process.on('SIGTERM', toExit); + process.on('exit', toExit); + } + }; +} + +export default { + input: 'src/main.js', + output: { + sourcemap: true, + format: 'iife', + name: 'app', + file: 'public/build/bundle.js' + }, + plugins: [ + svelte({ + compilerOptions: { + // enable run-time checks when not in production + dev: !production + } + }), + // we'll extract any component CSS out into + // a separate file - better for performance + css({ output: 'bundle.css' }), + + // If you have external dependencies installed from + // npm, you'll most likely need these plugins. In + // some cases you'll need additional configuration - + // consult the documentation for details: + // https://github.com/rollup/plugins/tree/master/packages/commonjs + resolve({ + browser: true, + dedupe: ['svelte'], + exportConditions: ['svelte'] + }), + commonjs(), + + // In dev mode, call `npm run start` once + // the bundle has been generated + !production && serve(), + + // Watch the `public` directory and refresh the + // browser on changes when not in production + !production && livereload('public'), + + // If we're building for production (npm run build + // instead of npm run dev), minify + production && terser() + ], + watch: { + clearScreen: false + } +}; diff --git a/packages/spa/scripts/setupTypeScript.js b/packages/spa/scripts/setupTypeScript.js new file mode 100644 index 00000000..4385f655 --- /dev/null +++ b/packages/spa/scripts/setupTypeScript.js @@ -0,0 +1,134 @@ +// @ts-check + +/** This script modifies the project to support TS code in .svelte files like: + + + + As well as validating the code for CI. + */ + +/** To work on this script: + rm -rf test-template template && git clone sveltejs/template test-template && node scripts/setupTypeScript.js test-template +*/ + +import fs from "fs" +import path from "path" +import { argv } from "process" +import url from 'url'; + +const __filename = url.fileURLToPath(import.meta.url); +const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); +const projectRoot = argv[2] || path.join(__dirname, "..") + +// Add deps to pkg.json +const packageJSON = JSON.parse(fs.readFileSync(path.join(projectRoot, "package.json"), "utf8")) +packageJSON.devDependencies = Object.assign(packageJSON.devDependencies, { + "svelte-check": "^3.0.0", + "svelte-preprocess": "^5.0.0", + "@rollup/plugin-typescript": "^11.0.0", + "typescript": "^4.9.0", + "tslib": "^2.5.0", + "@tsconfig/svelte": "^3.0.0" +}) + +// Add script for checking +packageJSON.scripts = Object.assign(packageJSON.scripts, { + "check": "svelte-check" +}) + +// Write the package JSON +fs.writeFileSync(path.join(projectRoot, "package.json"), JSON.stringify(packageJSON, null, " ")) + +// mv src/main.js to main.ts - note, we need to edit rollup.config.js for this too +const beforeMainJSPath = path.join(projectRoot, "src", "main.js") +const afterMainTSPath = path.join(projectRoot, "src", "main.ts") +fs.renameSync(beforeMainJSPath, afterMainTSPath) + +// Switch the app.svelte file to use TS +const appSveltePath = path.join(projectRoot, "src", "App.svelte") +let appFile = fs.readFileSync(appSveltePath, "utf8") +appFile = appFile.replace(" + +
+

Demo: crossref-to-docmap

+ + + +
+
+ + diff --git a/packages/spa/src/main.js b/packages/spa/src/main.js new file mode 100644 index 00000000..10bf9990 --- /dev/null +++ b/packages/spa/src/main.js @@ -0,0 +1,9 @@ +import App from './App.svelte'; + +const app = new App({ + target: document.body, + props: { + } +}); + +export default app; diff --git a/packages/spa/src/utils.js b/packages/spa/src/utils.js new file mode 100644 index 00000000..8862601b --- /dev/null +++ b/packages/spa/src/utils.js @@ -0,0 +1,31 @@ +import { fetchPublicationByDoi, CrossrefClient } from 'ts-etl' +import * as D from 'docmaps-sdk' +import { either } from 'fp-ts/lib/Either' + +export async function fetchData() { + // Create a CrossrefClient instance + const client = new CrossrefClient(); + + // Create a DocmapPublisherT instance (replace with appropriate values) + const publisher = new D.DocmapPublisherT(/*...publisher values...*/); + + const result: ErrorOrDocmap = await fetchPublicationByDoi( + client, + publisher, + inputUrl + ); + + either.fold( + (error) => { + // Handle the error case (e.g., display an error message) + console.error('Error:', error); + }, + (docmap: D.DocmapT[]) => { + // Handle the success case + const resultArea = document.getElementById('resultArea'); + const renderRev = document.createElement('render-rev'); + renderRev.setAttribute('source', JSON.stringify(docmap)); + resultArea.appendChild(renderRev); + } + )(result); +} From faa1ad1ebe61a60017ee04c06ff8a9858f527ed4 Mon Sep 17 00:00:00 2001 From: early evening Date: Wed, 10 May 2023 15:30:58 -0700 Subject: [PATCH 02/29] function encapsulation --- packages/spa/src/App.svelte | 19 +++++----- packages/spa/src/utils.js | 73 ++++++++++++++++++++++++------------- 2 files changed, 56 insertions(+), 36 deletions(-) diff --git a/packages/spa/src/App.svelte b/packages/spa/src/App.svelte index abbe10b1..ced8faab 100644 --- a/packages/spa/src/App.svelte +++ b/packages/spa/src/App.svelte @@ -1,23 +1,18 @@ @@ -28,7 +25,9 @@ /> -
+
+ +
diff --git a/packages/spa/src/JsonBox.svelte b/packages/spa/src/JsonBox.svelte index e543cf03..7c7361ab 100644 --- a/packages/spa/src/JsonBox.svelte +++ b/packages/spa/src/JsonBox.svelte @@ -22,3 +22,9 @@
+ From 3df20efc85966fbf466aba0184bdef953ac96127 Mon Sep 17 00:00:00 2001 From: early evening Date: Thu, 1 Jun 2023 15:48:40 -0700 Subject: [PATCH 15/29] release to github pages as part of release train --- .github/workflows/gh-pages.yaml | 5 +---- .github/workflows/release.yaml | 6 +++++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/gh-pages.yaml b/.github/workflows/gh-pages.yaml index cf479c34..4ac8eb40 100644 --- a/.github/workflows/gh-pages.yaml +++ b/.github/workflows/gh-pages.yaml @@ -2,12 +2,9 @@ name: Deploy static content to Pages on: - # Runs on pushes targeting the default branch - push: - branches: ["main", "ships/chore/add-playground"] - # Allows you to run this workflow manually from the Actions tab workflow_dispatch: + workflow_call: # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages permissions: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index c593563d..e5d529bd 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -70,4 +70,8 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - run: npx multi-semantic-release + # must ignore spa because we need spa in the workspace file for development, but spa is not released. + run: npx multi-semantic-release --ignore-packages=packages/spa + + release-github-pages-docs: + uses: ./.github/workflows/gh-pages.yaml From f33105a93f767ea4b73a1e7c46be1f9ded7d71f7 Mon Sep 17 00:00:00 2001 From: early evening Date: Thu, 1 Jun 2023 16:12:31 -0700 Subject: [PATCH 16/29] update readme --- packages/spa/README.md | 112 ++++++++++++++++------------------------- 1 file changed, 42 insertions(+), 70 deletions(-) diff --git a/packages/spa/README.md b/packages/spa/README.md index d488b3c7..044735e5 100644 --- a/packages/spa/README.md +++ b/packages/spa/README.md @@ -1,107 +1,79 @@ -# This repo is no longer maintained. Consider using `npm init vite` and selecting the `svelte` option or — if you want a full-fledged app framework — use [SvelteKit](https://kit.svelte.dev), the official application framework for Svelte. +Docmap Visual Playground ---- +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. -# svelte app +## Demo -This is a project template for [Svelte](https://svelte.dev) apps. It lives at https://github.com/sveltejs/template. +You can try the live demo hosted on GitHub Pages [here](https://docmaps-project.github.io/docmaps). -To create a new project based on this template using [degit](https://github.com/Rich-Harris/degit): +## Getting Started for Development -```bash -npx degit sveltejs/template svelte-app -cd svelte-app -``` +### Prerequisites -*Note that you will need to have [Node.js](https://nodejs.org) installed.* +- Node.js (>= 12.x) +- pnpm package manager +### Installation -## Get started - -Install the dependencies... +1. Clone the repository: ```bash -cd svelte-app -npm install +git clone https://github.com/docmaps-project/docmaps +cd docmaps/packages/spa ``` -...then start [Rollup](https://rollupjs.org): +2. Install dependencies: ```bash -npm run dev +pnpm install ``` -Navigate to [localhost:8080](http://localhost:8080). You should see your app running. Edit a component file in `src`, save it, and reload the page to see your changes. - -By default, the server will only respond to requests from localhost. To allow connections from other computers, edit the `sirv` commands in package.json to include the option `--host 0.0.0.0`. +### Running the tests -If you're using [Visual Studio Code](https://code.visualstudio.com/) we recommend installing the official extension [Svelte for VS Code](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). If you are using other editors you may need to install a plugin in order to get syntax highlighting and intellisense. - -## Building and running in production mode - -To create an optimised version of the app: +To run tests: ```bash -npm run build +pnpm test ``` -You can run the newly built app with `npm run start`. This uses [sirv](https://github.com/lukeed/sirv), which is included in your package.json's `dependencies` so that the app will work when you deploy to platforms like [Heroku](https://heroku.com). - +Note that you need an installation of Chrome, and must set +the `CHROME_PATH` variable. If you use Chromium, it might be something like +`/usr/local/bin/chromium`. -## Single-page app mode +### Running the App -By default, sirv will only respond to requests that match files in `public`. This is to maximise compatibility with static fileservers, allowing you to deploy your app anywhere. - -If you're building a single-page app (SPA) with multiple routes, sirv needs to be able to respond to requests for *any* path. You can make it so by editing the `"start"` command in package.json: - -```js -"start": "sirv public --single" -``` - -## Using TypeScript - -This template comes with a script to set up a TypeScript development environment, you can run it immediately after cloning the template with: +To start the development server: ```bash -node scripts/setupTypeScript.js +pnpm run dev ``` -Or remove the script via: +Then navigate to `http://localhost:8080` in your browser. -```bash -rm scripts/setupTypeScript.js -``` - -If you want to use `baseUrl` or `path` aliases within your `tsconfig`, you need to set up `@rollup/plugin-alias` to tell Rollup to resolve the aliases. For more info, see [this StackOverflow question](https://stackoverflow.com/questions/63427935/setup-tsconfig-path-in-svelte). - -## Deploying to the web +### Deploying to GH Pages -### With [Vercel](https://vercel.com) +The Github Pages is deployed from the `gh-pages` workflow in repository root. This workflow is called +by the `release` workflow and is only triggered on merge to main. It first builds/bundles this package, +then deploys the bundle. Note that because the repo name disagrees with the package name, the `index.html` +is modified to use relative paths for all bundled resources. -Install `vercel` if you haven't already: - -```bash -npm install -g vercel -``` +## Built With -Then, from within your project folder: +- [Svelte](https://svelte.dev/) - The web framework used +- [render-rev](github.com/source-data/render-rev) - Display component built by EMBO +- [pnpm](https://pnpm.io/) - The package manager -```bash -cd public -vercel deploy --name my-project -``` +## Contributing -### With [surge](https://surge.sh/) +See the main repository contributing guidelines. -Install `surge` if you haven't already: +## License -```bash -npm install -g surge -``` +See main repository license. -Then, from within your project folder: +## Acknowledgments -```bash -npm run build -surge public my-project.surge.sh -``` +- [render-rev](github.com/source-data/render-rev) +- [Svelte](https://svelte.dev/) +- [pnpm](https://pnpm.io/) +- [crossref](https://crossref.org/) From 7b688910a8ff5affe6402604f1ccd51560298bea Mon Sep 17 00:00:00 2001 From: early evening Date: Tue, 6 Jun 2023 12:32:08 -0700 Subject: [PATCH 17/29] fix copy on spa --- packages/spa/src/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/spa/src/utils.js b/packages/spa/src/utils.js index 5cff5468..76cc09ac 100644 --- a/packages/spa/src/utils.js +++ b/packages/spa/src/utils.js @@ -11,7 +11,7 @@ export async function configureForDoiString(str, handleJson, handleError) { client: CrossrefClient, }, publisher: { - name: 'Discovered on api.crossref.org', + name: 'Inferred from Crossref', url: 'https://github.com/docmaps-project/docmaps/tree/main/packages/ts-etl', }, } From 627672e08178db1ee1830bde0e597b03ebb989e6 Mon Sep 17 00:00:00 2001 From: early evening Date: Tue, 6 Jun 2023 12:49:49 -0700 Subject: [PATCH 18/29] fix spa error handling --- packages/spa/src/App.svelte | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/spa/src/App.svelte b/packages/spa/src/App.svelte index 847f31ad..1b4665f1 100644 --- a/packages/spa/src/App.svelte +++ b/packages/spa/src/App.svelte @@ -28,7 +28,6 @@ function handleError(error) { renderRevElement.configure({ - ...config, docmaps: error, // hacky solution }); } From 46e9393f2a7e4fd72f52553f3514422c45882035 Mon Sep 17 00:00:00 2001 From: early evening Date: Tue, 6 Jun 2023 12:59:44 -0700 Subject: [PATCH 19/29] More better copy --- packages/spa/src/App.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/spa/src/App.svelte b/packages/spa/src/App.svelte index 1b4665f1..b310d2d0 100644 --- a/packages/spa/src/App.svelte +++ b/packages/spa/src/App.svelte @@ -14,7 +14,7 @@ function handleData(data) { let config = { display: { - publisherName: name => name || "Unknown" + publisherName: name => name || "Preprint posted on Crossref" } } From 36fa7a65b90c385d4d5ec766e6e1779820c1593b Mon Sep 17 00:00:00 2001 From: early evening Date: Tue, 6 Jun 2023 14:27:00 -0700 Subject: [PATCH 20/29] extract crossref tests from itemcmd tests --- packages/ts-etl/test/unit/command.test.ts | 132 +--------------- .../ts-etl/test/unit/crossref/api.test.ts | 149 ++++++++++++++++++ 2 files changed, 152 insertions(+), 129 deletions(-) create mode 100644 packages/ts-etl/test/unit/crossref/api.test.ts diff --git a/packages/ts-etl/test/unit/command.test.ts b/packages/ts-etl/test/unit/command.test.ts index ef5e40ba..40f55b4a 100644 --- a/packages/ts-etl/test/unit/command.test.ts +++ b/packages/ts-etl/test/unit/command.test.ts @@ -4,8 +4,9 @@ import { ItemCmd } from '../../src/command' import { whenThenResolve } from './utils' import * as cm from './__fixtures__/crossref' -// TODO: decouple commands test from crossref test - +// This test is redundant with a test in the crossref case. Possibly there +// is no need for a test suite at this level as integration can handle it; +// however this asserts that the Cmd still passes through. test('ItemCmd: crossref: happy-path scenario: a manuscript with one preprint and no reviews', async (t) => { const mocks = cm.CrossrefClientMocks() whenThenResolve( @@ -46,130 +47,3 @@ test('ItemCmd: crossref: happy-path scenario: a manuscript with one preprint and t.deepEqual(dm.steps?.['_:b1']?.actions[0]?.outputs[0]?.doi, cm.MANUSCRIPT_DOI) //TODO: can write stronger assertions as we learn what this should look like }) - -test('ItemCmd: crossref: happy-path scenario: publisher is included', async (t) => { - const mocks = cm.CrossrefClientMocks() - whenThenResolve( - mocks.worksT.getWorks, - { doi: cm.MANUSCRIPT_DOI }, - cm.mockCrossrefManuscriptResponse, - ) - - const res = await ItemCmd([cm.MANUSCRIPT_DOI], { - source: { - preset: 'crossref-api', - client: mocks.crs, - }, - publisher: { - id: 'my_pub_id', - name: 'my_name', - }, - }) - - if (isLeft(res)) { - t.fail(`Got error instead of docmaps: ${res.left}`) - return - } - - t.is(res.right.length, 1) - const dm = res.right[0] - - // necessary because Typescript doesn't narrow down type of dm just because - // test failure guarantees we can't get here - if (!dm) { - t.fail('impossibly, we couldnt find the first docmap in a list of one') - return //necessary - } - - t.deepEqual(dm.type, 'docmap') - t.deepEqual(dm.publisher, { - id: 'my_pub_id', - name: 'my_name', - }) -}) - -test('ItemCmd: crossref: happy-path scenario: a manuscript with no relations', async (t) => { - const mocks = cm.CrossrefClientMocks() - whenThenResolve( - mocks.worksT.getWorks, - { doi: cm.MANUSCRIPT_DOI }, - cm.mockCrossrefManuscriptResponse, - ) - - const res = await ItemCmd([cm.MANUSCRIPT_DOI], { - source: { - preset: 'crossref-api', - client: mocks.crs, - }, - publisher: {}, - }) - - if (isLeft(res)) { - t.fail(`Got error instead of docmaps: ${res.left}`) - return - } - - t.is(res.right.length, 1) - const dm = res.right[0] - - // necessary because Typescript doesn't narrow down type of dm just because - // test failure guarantees we can't get here - if (!dm) { - t.fail('impossibly, we couldnt find the first docmap in a list of one') - return //necessary - } - - t.deepEqual(dm.type, 'docmap') - t.deepEqual(dm.steps?.['_:b0']?.inputs.length, 0) - t.deepEqual(dm.steps?.['_:b0']?.actions[0]?.outputs[0]?.doi, cm.MANUSCRIPT_DOI) -}) - -test('ItemCmd: crossref: happy-path scenario: a manuscript with 2 reviews and no preprint', async (t) => { - const mocks = cm.CrossrefClientMocks() - whenThenResolve( - mocks.worksT.getWorks, - { doi: cm.MANUSCRIPT_DOI }, - cm.mockCrossrefManuscriptWithReviewsResponse, - ) - // TODO - this implies each Review is fetched on its own - whenThenResolve( - mocks.worksT.getWorks, - { doi: cm.REVIEW_1_DOI }, - cm.mockCrossrefReviewsResponses[0], - ) - whenThenResolve( - mocks.worksT.getWorks, - { doi: cm.REVIEW_2_DOI }, - cm.mockCrossrefReviewsResponses[1], - ) - - const res = await ItemCmd([cm.MANUSCRIPT_DOI], { - source: { - preset: 'crossref-api', - client: mocks.crs, - }, - publisher: {}, - }) - - if (isLeft(res)) { - t.fail(`Got error instead of docmaps: ${res.left}`) - return - } - - t.is(res.right.length, 1) - const dm = res.right[0] - - // necessary because Typescript doesn't narrow down type of dm just because - // test failure guarantees we can't get here - if (!dm) { - t.fail('impossibly, we couldnt find the first docmap in a list of one') - return //necessary - } - - t.deepEqual(dm.type, 'docmap') - t.deepEqual(dm.steps?.['_:b0']?.inputs.length, 0) - t.deepEqual(dm.steps?.['_:b0']?.actions[0]?.outputs[0]?.doi, cm.MANUSCRIPT_DOI) - t.deepEqual(dm.steps?.['_:b1']?.inputs[0]?.doi, cm.MANUSCRIPT_DOI) - t.deepEqual(dm.steps?.['_:b1']?.actions[0]?.outputs[0]?.doi, cm.REVIEW_1_DOI) - t.deepEqual(dm.steps?.['_:b1']?.actions[1]?.outputs[0]?.doi, cm.REVIEW_2_DOI) -}) diff --git a/packages/ts-etl/test/unit/crossref/api.test.ts b/packages/ts-etl/test/unit/crossref/api.test.ts new file mode 100644 index 00000000..ddc2f24a --- /dev/null +++ b/packages/ts-etl/test/unit/crossref/api.test.ts @@ -0,0 +1,149 @@ +import test from 'ava' +import { isLeft } from 'fp-ts/lib/Either' +import { fetchPublicationByDoi } from '../../../src/plugins/crossref' +import { whenThenResolve } from '../utils' +import * as cm from '../__fixtures__/crossref' + +test('ItemCmd: crossref: happy-path scenario: a manuscript with one preprint and no reviews', async (t) => { + const mocks = cm.CrossrefClientMocks() + whenThenResolve( + mocks.worksT.getWorks, + { doi: cm.MANUSCRIPT_DOI }, + cm.mockCrossrefManuscriptWithPreprintResponse, + ) + whenThenResolve(mocks.worksT.getWorks, { doi: cm.PREPRINT_DOI }, cm.mockCrossrefPreprintResponse) + + const res = await fetchPublicationByDoi(mocks.crs, {}, cm.MANUSCRIPT_DOI) + + if (isLeft(res)) { + t.fail(`Got error instead of docmaps: ${res.left}`) + return + } + + t.is(res.right.length, 1) + const dm = res.right[0] + + // necessary because Typescript doesn't narrow down type of dm just because + // test failure guarantees we can't get here + if (!dm) { + t.fail('impossibly, we couldnt find the first docmap in a list of one') + return //necessary + } + + t.deepEqual(dm.type, 'docmap') + t.is(dm.steps ? Object.keys(dm.steps).length : 0, 2) + t.is(dm.steps?.['_:b0']?.inputs.length, 0) + t.deepEqual(dm.steps?.['_:b0']?.actions[0]?.outputs[0]?.doi, cm.PREPRINT_DOI) + t.deepEqual(dm.steps?.['_:b1']?.inputs[0]?.doi, cm.PREPRINT_DOI) + t.deepEqual(dm.steps?.['_:b1']?.actions[0]?.outputs[0]?.doi, cm.MANUSCRIPT_DOI) + //TODO: can write stronger assertions as we learn what this should look like +}) +test('fetchPublicationByDoi: happy-path scenario: publisher is included', async (t) => { + const mocks = cm.CrossrefClientMocks() + whenThenResolve( + mocks.worksT.getWorks, + { doi: cm.MANUSCRIPT_DOI }, + cm.mockCrossrefManuscriptResponse, + ) + + const publisher = { + id: 'my_pub_id', + name: 'my_name', + } + const res = await fetchPublicationByDoi(mocks.crs, publisher, cm.MANUSCRIPT_DOI) + + if (isLeft(res)) { + t.fail(`Got error instead of docmaps: ${res.left}`) + return + } + + t.is(res.right.length, 1) + const dm = res.right[0] + + // necessary because Typescript doesn't narrow down type of dm just because + // test failure guarantees we can't get here + if (!dm) { + t.fail('impossibly, we couldnt find the first docmap in a list of one') + return //necessary + } + + t.deepEqual(dm.type, 'docmap') + t.deepEqual(dm.publisher, { + id: 'my_pub_id', + name: 'my_name', + }) +}) + +test('fetchPublicationByDoi: happy-path scenario: a manuscript with no relations', async (t) => { + const mocks = cm.CrossrefClientMocks() + whenThenResolve( + mocks.worksT.getWorks, + { doi: cm.MANUSCRIPT_DOI }, + cm.mockCrossrefManuscriptResponse, + ) + + const res = await fetchPublicationByDoi(mocks.crs, {}, cm.MANUSCRIPT_DOI) + + if (isLeft(res)) { + t.fail(`Got error instead of docmaps: ${res.left}`) + return + } + + t.is(res.right.length, 1) + const dm = res.right[0] + + // necessary because Typescript doesn't narrow down type of dm just because + // test failure guarantees we can't get here + if (!dm) { + t.fail('impossibly, we couldnt find the first docmap in a list of one') + return //necessary + } + + t.deepEqual(dm.type, 'docmap') + t.deepEqual(dm.steps?.['_:b0']?.inputs.length, 0) + t.deepEqual(dm.steps?.['_:b0']?.actions[0]?.outputs[0]?.doi, cm.MANUSCRIPT_DOI) +}) + +test('fetchPublicationByDoi: happy-path scenario: a manuscript with 2 reviews and no preprint', async (t) => { + const mocks = cm.CrossrefClientMocks() + whenThenResolve( + mocks.worksT.getWorks, + { doi: cm.MANUSCRIPT_DOI }, + cm.mockCrossrefManuscriptWithReviewsResponse, + ) + // TODO - this implies each Review is fetched on its own + whenThenResolve( + mocks.worksT.getWorks, + { doi: cm.REVIEW_1_DOI }, + cm.mockCrossrefReviewsResponses[0], + ) + whenThenResolve( + mocks.worksT.getWorks, + { doi: cm.REVIEW_2_DOI }, + cm.mockCrossrefReviewsResponses[1], + ) + + const res = await fetchPublicationByDoi(mocks.crs, {}, cm.MANUSCRIPT_DOI) + + if (isLeft(res)) { + t.fail(`Got error instead of docmaps: ${res.left}`) + return + } + + t.is(res.right.length, 1) + const dm = res.right[0] + + // necessary because Typescript doesn't narrow down type of dm just because + // test failure guarantees we can't get here + if (!dm) { + t.fail('impossibly, we couldnt find the first docmap in a list of one') + return //necessary + } + + t.deepEqual(dm.type, 'docmap') + t.deepEqual(dm.steps?.['_:b0']?.inputs.length, 0) + t.deepEqual(dm.steps?.['_:b0']?.actions[0]?.outputs[0]?.doi, cm.MANUSCRIPT_DOI) + t.deepEqual(dm.steps?.['_:b1']?.inputs[0]?.doi, cm.MANUSCRIPT_DOI) + t.deepEqual(dm.steps?.['_:b1']?.actions[0]?.outputs[0]?.doi, cm.REVIEW_1_DOI) + t.deepEqual(dm.steps?.['_:b1']?.actions[1]?.outputs[0]?.doi, cm.REVIEW_2_DOI) +}) From e872a1d8c8a567c76d9fa8530f3f8c56efa67610 Mon Sep 17 00:00:00 2001 From: early evening Date: Tue, 6 Jun 2023 15:55:13 -0700 Subject: [PATCH 21/29] allow finding manuscript by preprint and set groundwork for graph-walk algorithm instead --- packages/ts-etl/src/plugins/crossref/api.ts | 84 ++++++++++++++++--- .../ts-etl/test/unit/crossref/api.test.ts | 36 +++++++- 2 files changed, 108 insertions(+), 12 deletions(-) diff --git a/packages/ts-etl/src/plugins/crossref/api.ts b/packages/ts-etl/src/plugins/crossref/api.ts index 916f78b3..23c3c3a3 100644 --- a/packages/ts-etl/src/plugins/crossref/api.ts +++ b/packages/ts-etl/src/plugins/crossref/api.ts @@ -24,19 +24,24 @@ export const Client = CreateCrossrefClient({}) type RecursiveStepDataChain = { head: D.StepT all: D.StepT[] + visitedIds: Set } function stepsForDoiRecursive( client: CrossrefClient, inputDoi: string, - status: string, + visitedIds: Set, + annotations: { + status: string // the acquired status for the root step created by this recursive call + inputs: D.ThingT[] + }, ): TE.TaskEither { const service = client.works const program = pipe( TE.Do, TE.bind('w', () => TE.tryCatch( - () => service.getWorks({ doi: inputDoi }), // FIXME throw/reject if not status OK ? (what is client behavior if not 200?) + () => service.getWorks({ doi: inputDoi }), (reason: unknown) => new Error(`failed to fetch crossref body for DOI ${inputDoi}`, { cause: reason }), ), @@ -47,11 +52,11 @@ function stepsForDoiRecursive( w.message, decodeActionForWork, E.map((action) => ({ - inputs: [], // TODO: confirm we always override this as needed + inputs: annotations.inputs, actions: [action], assertions: [ { - status: status, //TODO : choose this key carefully + status: annotations.status, //TODO : choose this key carefully item: w.message.DOI, }, ], @@ -63,8 +68,9 @@ function stepsForDoiRecursive( ), ), E.map((s) => ({ - all: [s], head: s, + all: [s], + visitedIds: visitedIds.add(inputDoi), })), TE.fromEither, ), @@ -79,17 +85,23 @@ function stepsForDoiRecursive( return pipe( preprints, A.filter((wre) => { - return wre['id-type'].toLowerCase() == 'doi' + return ( + wre['id-type'].toLowerCase() == 'doi' && + !initialChain.visitedIds.has(wre['id'].toLowerCase()) + ) }), // get unique IDs A.map((wre) => wre.id), A.uniq(eqString), TE.traverseArray((wreId) => { - return stepsForDoiRecursive(client, wreId, 'catalogued') // TODO: status for any preprint + return stepsForDoiRecursive(client, wreId, initialChain.visitedIds, { + status: 'catalogued', // TODO: this is our provisionally selected status for any preprint + inputs: [], + }) }), TE.map((meta) => { // this is array of DataChain. - // the output `head` should have all the `head` outputs as inputs. + // the output `head` should have all the in-array `head` outputs as inputs. // the output `all` should have concatenation of alls, plus the new Head. const newStep = { @@ -97,18 +109,22 @@ function stepsForDoiRecursive( inputs: meta.reduce( (memo, c) => memo.concat(c.head.actions.reduce((m, a) => m.concat(a.outputs), [])), - [], + initialChain.head.inputs, ), } return { head: newStep, all: meta.reduce((m, c) => m.concat(c.all), []).concat([newStep]), + visitedIds: meta.reduce>( + (m, c) => new Set(...m, ...c.visitedIds), + initialChain.visitedIds, + ), } }), ) }), - TE.bind('completeChain', ({ w, prefixChain }) => { + TE.bind('postfixChain', ({ w, prefixChain }) => { const reviews = w.message.relation?.['has-review'] if (!reviews) { return TE.right(prefixChain) @@ -140,9 +156,55 @@ function stepsForDoiRecursive( TE.map((reviewStep) => ({ head: prefixChain.head, all: prefixChain.all.concat([reviewStep]), + // TODO: consider whether we want to include the review DOIs in the visitedIds + // - sometimes the Review is marked `is-review-of` both the preprint and manuscript + visitedIds: prefixChain.visitedIds, })), ) }), + TE.bind('completeChain', ({ w, postfixChain }) => { + const manuscripts = w.message.relation?.['is-preprint-of'] + if (!manuscripts) { + return TE.right(postfixChain) + } + + // 2. if this is a preprint of another manuscript recurse + return pipe( + manuscripts, + A.filter((wre) => { + return ( + wre['id-type'].toLowerCase() == 'doi' && + !postfixChain.visitedIds.has(wre['id'].toLowerCase()) + ) + }), + // get unique IDs + A.map((wre) => wre.id), + A.uniq(eqString), + TE.traverseArray((wreId) => { + return stepsForDoiRecursive(client, wreId, postfixChain.visitedIds, { + status: 'published', // TODO: this is for manuscript, but doesn't handle situations with deep recursion + // all the outputs of all the actions of the head + inputs: postfixChain.head.actions.reduce((m, a) => m.concat(a.outputs), []), + }) + }), + TE.map((meta) => { + // this is array of DataChain. + // in this case, we are not manipulating the current head, instead + // we told the new heads that they have inputs. + // the output `all` should still have concatenation of alls. + + return { + head: postfixChain.head, + all: postfixChain.all.concat(meta.reduce((m, c) => m.concat(c.all), [])), + visitedIds: meta.reduce>( + (m, c) => new Set(...m, ...c.visitedIds), + postfixChain.visitedIds, + ), + } + }), + ) + }), + // we return a complete chain, but the head is dropped when recursion is finished and we just consider `all`. TE.map(({ completeChain }) => completeChain), ) @@ -155,7 +217,7 @@ export async function fetchPublicationByDoi( inputDoi: string, ): Promise { const resultTask = pipe( - stepsForDoiRecursive(client, inputDoi, 'published'), + stepsForDoiRecursive(client, inputDoi, new Set(), { status: 'published', inputs: [] }), TE.chain((steps) => { return pipe(stepArrayToDocmap(publisher, inputDoi, steps.all), TE.fromEither) }), diff --git a/packages/ts-etl/test/unit/crossref/api.test.ts b/packages/ts-etl/test/unit/crossref/api.test.ts index ddc2f24a..e3e4c0fb 100644 --- a/packages/ts-etl/test/unit/crossref/api.test.ts +++ b/packages/ts-etl/test/unit/crossref/api.test.ts @@ -4,7 +4,7 @@ import { fetchPublicationByDoi } from '../../../src/plugins/crossref' import { whenThenResolve } from '../utils' import * as cm from '../__fixtures__/crossref' -test('ItemCmd: crossref: happy-path scenario: a manuscript with one preprint and no reviews', async (t) => { +test('fetchPublicationByDoi: happy-path scenario: a manuscript with one preprint and no reviews', async (t) => { const mocks = cm.CrossrefClientMocks() whenThenResolve( mocks.worksT.getWorks, @@ -38,6 +38,40 @@ test('ItemCmd: crossref: happy-path scenario: a manuscript with one preprint and t.deepEqual(dm.steps?.['_:b1']?.actions[0]?.outputs[0]?.doi, cm.MANUSCRIPT_DOI) //TODO: can write stronger assertions as we learn what this should look like }) +test('fetchPublicationByDoi: happy-path scenario: a manuscript discovered from its preprint', async (t) => { + const mocks = cm.CrossrefClientMocks() + whenThenResolve( + mocks.worksT.getWorks, + { doi: cm.MANUSCRIPT_DOI }, + cm.mockCrossrefManuscriptWithPreprintResponse, + ) + whenThenResolve(mocks.worksT.getWorks, { doi: cm.PREPRINT_DOI }, cm.mockCrossrefPreprintResponse) + + const res = await fetchPublicationByDoi(mocks.crs, {}, cm.PREPRINT_DOI) + + if (isLeft(res)) { + t.fail(`Got error instead of docmaps: ${res.left}`) + return + } + + t.is(res.right.length, 1) + const dm = res.right[0] + + // necessary because Typescript doesn't narrow down type of dm just because + // test failure guarantees we can't get here + if (!dm) { + t.fail('impossibly, we couldnt find the first docmap in a list of one') + return //necessary + } + + t.deepEqual(dm.type, 'docmap') + t.is(dm.steps ? Object.keys(dm.steps).length : 0, 2) + t.is(dm.steps?.['_:b0']?.inputs.length, 0) + t.deepEqual(dm.steps?.['_:b0']?.actions[0]?.outputs[0]?.doi, cm.PREPRINT_DOI) + t.deepEqual(dm.steps?.['_:b1']?.inputs[0]?.doi, cm.PREPRINT_DOI) + t.deepEqual(dm.steps?.['_:b1']?.actions[0]?.outputs[0]?.doi, cm.MANUSCRIPT_DOI) + //TODO: can write stronger assertions as we learn what this should look like +}) test('fetchPublicationByDoi: happy-path scenario: publisher is included', async (t) => { const mocks = cm.CrossrefClientMocks() whenThenResolve( From 5f5471185ec064a89af2f918e0a95ce87ae38db1 Mon Sep 17 00:00:00 2001 From: early evening Date: Tue, 6 Jun 2023 15:57:53 -0700 Subject: [PATCH 22/29] resume deploying spa from branch for now --- .github/workflows/gh-pages.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/gh-pages.yaml b/.github/workflows/gh-pages.yaml index 4ac8eb40..410c2d72 100644 --- a/.github/workflows/gh-pages.yaml +++ b/.github/workflows/gh-pages.yaml @@ -6,6 +6,10 @@ on: workflow_dispatch: workflow_call: + # FIXME: remove this before merge ideally + push: + branches: ["ships/chore/add-playground"] + # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages permissions: contents: read From afa8caa311b662871f5e791266b111f4720e0f63 Mon Sep 17 00:00:00 2001 From: early evening Date: Tue, 6 Jun 2023 16:16:07 -0700 Subject: [PATCH 23/29] use crossref type to error early for reviews --- packages/ts-etl/src/plugins/crossref/api.ts | 13 ++++++++++-- .../ts-etl/test/unit/__fixtures__/crossref.ts | 8 ++++---- .../ts-etl/test/unit/crossref/api.test.ts | 20 ++++++++++++++++++- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/packages/ts-etl/src/plugins/crossref/api.ts b/packages/ts-etl/src/plugins/crossref/api.ts index 23c3c3a3..c83d35e8 100644 --- a/packages/ts-etl/src/plugins/crossref/api.ts +++ b/packages/ts-etl/src/plugins/crossref/api.ts @@ -39,13 +39,22 @@ function stepsForDoiRecursive( const service = client.works const program = pipe( TE.Do, - TE.bind('w', () => + TE.bind('w', () => pipe( TE.tryCatch( () => service.getWorks({ doi: inputDoi }), (reason: unknown) => new Error(`failed to fetch crossref body for DOI ${inputDoi}`, { cause: reason }), ), - ), + TE.chainEitherK((w) => { + switch (w.message.type) { + case 'posted-content': + case 'journal-article': + return E.right(w) + default: + return E.left(new Error(`requested root docmap for crossref entity of type '${w.message.type}'`)) + } + }), + )), // 1. get step for this TE.bind('initialChain', ({ w }) => pipe( diff --git a/packages/ts-etl/test/unit/__fixtures__/crossref.ts b/packages/ts-etl/test/unit/__fixtures__/crossref.ts index 7f9bf8f5..ee97f058 100644 --- a/packages/ts-etl/test/unit/__fixtures__/crossref.ts +++ b/packages/ts-etl/test/unit/__fixtures__/crossref.ts @@ -67,7 +67,7 @@ const PreprintItemBody = { }, ], }, - type: 'preprint', + type: 'posted-content', URL: `https://doi.org/${PREPRINT_DOI}`, // unimportant values for this test DOI: PREPRINT_DOI, @@ -164,7 +164,7 @@ export const mockCrossrefReviewsResponses: WorkMessage[] = [REVIEW_1_DOI, REVIEW ...GENERIC_WORK_DATA, URL: `https://doi.org/${doi}`, DOI: doi, - type: 'review', + type: 'peer-review', relation: { 'is-review-of': [ { @@ -194,7 +194,7 @@ export const mockCrossrefReviewsResponse: WorksMessage = { ...GENERIC_WORK_DATA, DOI: REVIEW_1_DOI, URL: `http://dx.doi.org/{REVIEW_1_DOI}`, - type: 'review', + type: 'peer-review', relation: { 'is-review-of': [ { @@ -209,7 +209,7 @@ export const mockCrossrefReviewsResponse: WorksMessage = { ...GENERIC_WORK_DATA, DOI: REVIEW_2_DOI, URL: `http://dx.doi.org/{REVIEW_2_DOI}`, - type: 'review', + type: 'peer-review', relation: { 'is-review-of': [ { diff --git a/packages/ts-etl/test/unit/crossref/api.test.ts b/packages/ts-etl/test/unit/crossref/api.test.ts index e3e4c0fb..b0f36c25 100644 --- a/packages/ts-etl/test/unit/crossref/api.test.ts +++ b/packages/ts-etl/test/unit/crossref/api.test.ts @@ -1,5 +1,5 @@ import test from 'ava' -import { isLeft } from 'fp-ts/lib/Either' +import { isLeft, isRight } from 'fp-ts/lib/Either' import { fetchPublicationByDoi } from '../../../src/plugins/crossref' import { whenThenResolve } from '../utils' import * as cm from '../__fixtures__/crossref' @@ -181,3 +181,21 @@ test('fetchPublicationByDoi: happy-path scenario: a manuscript with 2 reviews an t.deepEqual(dm.steps?.['_:b1']?.actions[0]?.outputs[0]?.doi, cm.REVIEW_1_DOI) t.deepEqual(dm.steps?.['_:b1']?.actions[1]?.outputs[0]?.doi, cm.REVIEW_2_DOI) }) + +test('fetchPublicationByDoi: error case: looking up a crossref work of wrong type', async (t) => { + const mocks = cm.CrossrefClientMocks() + whenThenResolve( + mocks.worksT.getWorks, + { doi: cm.REVIEW_1_DOI }, + cm.mockCrossrefReviewsResponses[0], + ) + + const res = await fetchPublicationByDoi(mocks.crs, {}, cm.REVIEW_1_DOI) + + if (isRight(res)) { + t.fail(`Got docmaps instead of error: ${res.right}`) + return + } + + t.regex(res.left.message, /requested root docmap for crossref entity of type 'peer-review'/) +}) From 671f6ee208cfd3798451da1781bb6ee84dda11fb Mon Sep 17 00:00:00 2001 From: early evening Date: Tue, 6 Jun 2023 16:27:24 -0700 Subject: [PATCH 24/29] error on crossref request for review --- packages/ts-etl/src/plugins/crossref/api.ts | 40 ++++++++++++--------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/packages/ts-etl/src/plugins/crossref/api.ts b/packages/ts-etl/src/plugins/crossref/api.ts index c83d35e8..c469e4c3 100644 --- a/packages/ts-etl/src/plugins/crossref/api.ts +++ b/packages/ts-etl/src/plugins/crossref/api.ts @@ -32,31 +32,39 @@ function stepsForDoiRecursive( inputDoi: string, visitedIds: Set, annotations: { - status: string // the acquired status for the root step created by this recursive call inputs: D.ThingT[] }, ): TE.TaskEither { const service = client.works const program = pipe( TE.Do, - TE.bind('w', () => pipe( + TE.bind('w', () => TE.tryCatch( () => service.getWorks({ doi: inputDoi }), (reason: unknown) => new Error(`failed to fetch crossref body for DOI ${inputDoi}`, { cause: reason }), ), - TE.chainEitherK((w) => { - switch (w.message.type) { - case 'posted-content': - case 'journal-article': - return E.right(w) - default: - return E.left(new Error(`requested root docmap for crossref entity of type '${w.message.type}'`)) - } - }), - )), + ), + TE.bind('status', ({ w }) => + pipe( + w, + (w) => { + switch (w.message.type) { + case 'posted-content': + return E.right('catalogued') + case 'journal-article': + return E.right('published') + default: + return E.left( + new Error(`requested root docmap for crossref entity of type '${w.message.type}'`), + ) + } + }, + TE.fromEither, + ), + ), // 1. get step for this - TE.bind('initialChain', ({ w }) => + TE.bind('initialChain', ({ w, status }) => pipe( w.message, decodeActionForWork, @@ -65,7 +73,7 @@ function stepsForDoiRecursive( actions: [action], assertions: [ { - status: annotations.status, //TODO : choose this key carefully + status: status, //TODO : choose this key carefully item: w.message.DOI, }, ], @@ -104,7 +112,6 @@ function stepsForDoiRecursive( A.uniq(eqString), TE.traverseArray((wreId) => { return stepsForDoiRecursive(client, wreId, initialChain.visitedIds, { - status: 'catalogued', // TODO: this is our provisionally selected status for any preprint inputs: [], }) }), @@ -191,7 +198,6 @@ function stepsForDoiRecursive( A.uniq(eqString), TE.traverseArray((wreId) => { return stepsForDoiRecursive(client, wreId, postfixChain.visitedIds, { - status: 'published', // TODO: this is for manuscript, but doesn't handle situations with deep recursion // all the outputs of all the actions of the head inputs: postfixChain.head.actions.reduce((m, a) => m.concat(a.outputs), []), }) @@ -226,7 +232,7 @@ export async function fetchPublicationByDoi( inputDoi: string, ): Promise { const resultTask = pipe( - stepsForDoiRecursive(client, inputDoi, new Set(), { status: 'published', inputs: [] }), + stepsForDoiRecursive(client, inputDoi, new Set(), { inputs: [] }), TE.chain((steps) => { return pipe(stepArrayToDocmap(publisher, inputDoi, steps.all), TE.fromEither) }), From f02a54c7626cd8fc48aaa799597365e2cd96140b Mon Sep 17 00:00:00 2001 From: early evening Date: Tue, 6 Jun 2023 16:40:19 -0700 Subject: [PATCH 25/29] add error display to spa --- packages/spa/src/App.svelte | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/spa/src/App.svelte b/packages/spa/src/App.svelte index b310d2d0..cb010c31 100644 --- a/packages/spa/src/App.svelte +++ b/packages/spa/src/App.svelte @@ -30,6 +30,8 @@ renderRevElement.configure({ docmaps: error, // hacky solution }); + + json=JSON.stringify(error, ['message', 'cause']); } async function fetchData() { From 5a1df327a825e1c1a72bd4e00445b3f49c7db21f Mon Sep 17 00:00:00 2001 From: early evening Date: Wed, 7 Jun 2023 09:33:23 -0700 Subject: [PATCH 26/29] remove setup script from template --- packages/spa/scripts/setupTypeScript.js | 134 ------------------------ 1 file changed, 134 deletions(-) delete mode 100644 packages/spa/scripts/setupTypeScript.js diff --git a/packages/spa/scripts/setupTypeScript.js b/packages/spa/scripts/setupTypeScript.js deleted file mode 100644 index 4385f655..00000000 --- a/packages/spa/scripts/setupTypeScript.js +++ /dev/null @@ -1,134 +0,0 @@ -// @ts-check - -/** This script modifies the project to support TS code in .svelte files like: - - - - As well as validating the code for CI. - */ - -/** To work on this script: - rm -rf test-template template && git clone sveltejs/template test-template && node scripts/setupTypeScript.js test-template -*/ - -import fs from "fs" -import path from "path" -import { argv } from "process" -import url from 'url'; - -const __filename = url.fileURLToPath(import.meta.url); -const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); -const projectRoot = argv[2] || path.join(__dirname, "..") - -// Add deps to pkg.json -const packageJSON = JSON.parse(fs.readFileSync(path.join(projectRoot, "package.json"), "utf8")) -packageJSON.devDependencies = Object.assign(packageJSON.devDependencies, { - "svelte-check": "^3.0.0", - "svelte-preprocess": "^5.0.0", - "@rollup/plugin-typescript": "^11.0.0", - "typescript": "^4.9.0", - "tslib": "^2.5.0", - "@tsconfig/svelte": "^3.0.0" -}) - -// Add script for checking -packageJSON.scripts = Object.assign(packageJSON.scripts, { - "check": "svelte-check" -}) - -// Write the package JSON -fs.writeFileSync(path.join(projectRoot, "package.json"), JSON.stringify(packageJSON, null, " ")) - -// mv src/main.js to main.ts - note, we need to edit rollup.config.js for this too -const beforeMainJSPath = path.join(projectRoot, "src", "main.js") -const afterMainTSPath = path.join(projectRoot, "src", "main.ts") -fs.renameSync(beforeMainJSPath, afterMainTSPath) - -// Switch the app.svelte file to use TS -const appSveltePath = path.join(projectRoot, "src", "App.svelte") -let appFile = fs.readFileSync(appSveltePath, "utf8") -appFile = appFile.replace("