From bc6c85d1d0ca8754d506eb5d68c12a4b962000f7 Mon Sep 17 00:00:00 2001 From: Eric McDaniel Date: Thu, 19 Dec 2024 17:24:15 -0500 Subject: [PATCH] Datacite Action (#833) * scaffold datacite action * basic deposit * fix broken deposits * fix arcadia seed * added a few tests * dry * test tweaks * format * lockfile * fix eslint * support doi suffix * fix typeerror * small fixes and some error messages * remove log * suffix test * publish doi instead drafting * add lastModifiedBy to updatePub * fix type error in test * add datacite related environment variable definitions to terraform * declare existence of temp datacite secrets * fix tf issues * use datacite test api for now * Update core/actions/datacite/action.ts Co-authored-by: Thomas F. K. Jorna * pr feedback * fix bad merge * new values * fix tests --------- Co-authored-by: Eric McDaniel Co-authored-by: Thomas F. K. Jorna --- .github/ISSUE_TEMPLATE/bug-issue.md | 4 +- .github/pull_request_template.md | 1 + DEVELOPMENT.md | 21 +- README.md | 37 +- core/.env.development | 4 + core/actions/_lib/resolvePubfields.ts | 1 + core/actions/api/index.ts | 2 + core/actions/datacite/action.ts | 92 ++ core/actions/datacite/run.test.ts | 275 ++++ core/actions/datacite/run.ts | 266 ++++ core/actions/datacite/types.ts | 1316 +++++++++++++++++ core/actions/runs.ts | 1 + core/app/c/[communitySlug]/types/page.tsx | 1 + .../ActionUI/ActionConfigFormWrapper.tsx | 5 +- core/lib/env/env.mjs | 3 + core/lib/serverActions.ts | 2 +- core/prisma/exampleCommunitySeeds/arcadia.ts | 23 +- .../migration.sql | 2 + core/prisma/schema/schema.dbml | 1 + core/prisma/schema/schema.prisma | 1 + docker-compose.dev.yml | 264 ++-- infrastructure/maskfile.md | 8 +- infrastructure/nginx/README.md | 1 + infrastructure/terraform/README.md | 14 +- .../terraform/environments/blake/main.tf | 2 + .../environments/cloudflare/README.md | 7 +- .../environments/global_aws/README.md | 1 - .../terraform/environments/stevie/main.tf | 2 + .../terraform/modules/core-services/README.md | 12 +- .../terraform/modules/core-services/main.tf | 8 + .../modules/core-services/outputs.tf | 2 + .../terraform/modules/deployment/main.tf | 4 +- .../terraform/modules/deployment/variables.tf | 5 + integrations/evaluations-proxy/package.json | 8 +- integrations/submissions-proxy/package.json | 8 +- packages/db/src/public/Action.ts | 1 + packages/ui/package.json | 2 +- packages/ui/src/auto-form/fields/date.tsx | 47 +- packages/ui/src/calendar.tsx | 4 - pnpm-lock.yaml | 27 +- 40 files changed, 2268 insertions(+), 217 deletions(-) create mode 100644 core/actions/datacite/action.ts create mode 100644 core/actions/datacite/run.test.ts create mode 100644 core/actions/datacite/run.ts create mode 100644 core/actions/datacite/types.ts create mode 100644 core/prisma/migrations/20241203193207_add_datacite_action/migration.sql diff --git a/.github/ISSUE_TEMPLATE/bug-issue.md b/.github/ISSUE_TEMPLATE/bug-issue.md index ea526b7fd..10ec8651e 100644 --- a/.github/ISSUE_TEMPLATE/bug-issue.md +++ b/.github/ISSUE_TEMPLATE/bug-issue.md @@ -1,9 +1,9 @@ --- name: Bug Report about: Find something broken or odd? Report it here. -title: '' +title: "" labels: bug -assignees: '' +assignees: "" --- ## Test Plan diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 8d581dfe9..dc0e6cac9 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -4,20 +4,21 @@ Docker-compose provides a means to run all the code components as containers. It has these advantages: -- more realistically emulating the setting where this code is run in production. -- less contamination of environment, so spurious failures (or successes) can be avoided. -- easy to boot from nothing without system dependencies except Docker +- more realistically emulating the setting where this code is run in production. +- less contamination of environment, so spurious failures (or successes) can be avoided. +- easy to boot from nothing without system dependencies except Docker With disadvantages: -- doesn't support hot-reloading -- slightly slower iteration due to `docker build` between runs +- doesn't support hot-reloading +- slightly slower iteration due to `docker build` between runs With these properties, this is useful for end-to-end tests and locally verifying that the code works when removed from some of the fast-iteration features of `next.js`, such as JIT compilation. Because a `docker build` is needed to build the containers to run, hot-reloading is not available in this environment; so faster iteration with `pnpm dev` is recommended until you are ready to run battery of tests or need to verify behavior in an isolated environment. A slightly modified version of `.env.local` is required, where we remove the DATABASE_URL (this is built out of parts in docker envs): + ``` grep -v DATABASE_URL \ <./core/.env.local \ @@ -27,13 +28,13 @@ grep -v DATABASE_URL \ This cluster will address the local supabase and postgres just like when you are running with `pnpm dev`, so no need to take extra steps for migrations (though the same ones are needed). To run the full cluster as local docker-compose, first initialize supabase as you would for development; then do: + ``` docker compose -f docker-compose.dev.yml up ``` you can now address on `localhost:3000` as before. note that `pnpm dev` uses the same ports and cannot be running at the same time. - ## Prettier At the moment, the repo simply uses prettier before adding any additional complexity with ESLint configs. Just auto-format (either on save, or on commit), and let the .prettierrc hold the small subset of decisions. @@ -58,7 +59,6 @@ We currently have a race condition where dev will sometimes fail because we can' `core` depends on `ui` which depends on `utils`. `utils` often takes longer to build than it does for `ui` to start building, which causes an error to be thrown because `utils` d.ts file has been cleared out during its build and hasn't been replaced yet. This generates an error, but is quick to resolve, so doesn't break actual dev work from beginning. It does make the console output messier though. - ## Building and deploying for AWS environments All change management to Knowledge Futures' production environment is done through github actions. @@ -69,6 +69,7 @@ including all details for a container. We don't want to tie code releases to ter but the service "declaration" relies on this Task Definition to exist. Therefore based on community patterns we have seen, the flow is roughly this: + 1. The infrastructure code in terraform declares a "template" Task Definition. 2. Terraform is told not to change the "service" based on changes to the Task Definition. 3. Any changes to the template will be picked up by the next deploy, which is done outside of Terraform. @@ -116,10 +117,11 @@ act \ **AWS CLI access in `act`:** When you setup `act` locally for the first time, you can choose whether to do a Small, Medium, or Large install. -The Large install is *very large*, but the medium install does not include the AWS CLI. +The Large install is _very large_, but the medium install does not include the AWS CLI. If you choose to work with the medium install, you will need to customize afterward by editing your `~/.actrc` file to use an image that includes the AWS CLI, for example your .actrc might look like: + ``` -P ubuntu-latest=eveships/act-plusplus:latest -P ubuntu-22.04=catthehacker/ubuntu:full-22.04 @@ -140,4 +142,5 @@ AWS_SECRET_ACCESS_KEY... Images tagged with a SHA alone should be idempotently built, but `-dirty` can be changed/overwritten. **TODO:** -- [ ] allow deploying without a rebuild, so that a rollback is convenient + +- [ ] allow deploying without a rebuild, so that a rollback is convenient diff --git a/README.md b/README.md index c46dcfe70..43205f97b 100644 --- a/README.md +++ b/README.md @@ -9,20 +9,22 @@ As noted in [our recent announcement](https://www.knowledgefutures.org/updates/p Learn More: [Documentation](https://help.knowledgefutures.org) | [PubPub Platform](https://knowledgefutures.org/pubpub) | [Knowledge Futures](https://www.knowledgefutures.org/) | [Newsletter](https://pubpub.us5.list-manage.com/subscribe?u=9b9b78707f3dd62d0d47ec03d&id=be26e45660) | [Roadmap](https://github.com/orgs/pubpub/projects/46/views/1) -**PubPub Platform is currently an alpha release, which means the code is subject to frequent, breaking changes. We do not yet recommend running PubPub Platform for production projects.** +**PubPub Platform is currently an alpha release, which means the code is subject to frequent, breaking changes. We do not yet recommend running PubPub Platform for production projects.** ## Community Guidelines and Code of Conduct + Knowledge Futures intends to foster an open and welcoming environment that aligns with our [core values of ACCESS](https://notes.knowledgefutures.org/pub/cqih29xa#our-values-access) (accessibility, collaboration, curiosity, equity, and systemic outlike). As such, we require that all employees and members of our open-source community adhere to our [Code of Conduct](https://github.com/knowledgefutures/general/blob/master/CODE_OF_CONDUCT.md) in all interactions in this and other Knowledge Futures repositories, including issues, discussions, comments, pull requests, commit messages, code, and all other messages. [![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) ## Repository Structure + This repo is built as a monorepo that holds first-party components of PubPub. There are four primary sections: ``` root ├── core/ -├── infrastructure/ +├── infrastructure/ ├── integrations/ (deprecated) ├── jobs/ ├── packages/ @@ -38,6 +40,7 @@ root To avoid inconsistencies and difficult-to-track errors, we specify a particular version of node in `/.nvmrc` (currently `v20.17.0`). We recommend using [nvm](https://github.com/nvm-sh/nvm) to ensure you're using the same version. ## Local Installation + This package runs the version of node specified in `.nvmrc` (currently `v20.17.0`) and uses pnpm for package management. All following commands are run from the root of this package. To get started, clone the repository and install the version of node specified in `.nvmrc` (we recommend using [nvm](https://github.com/nvm-sh/nvm). @@ -56,6 +59,7 @@ pnpm build Depending on which app or package you are doing work on, you may need to create a .env.local file. See each package's individual README.md file for further details. ## Development + To run all packages in the monorepo workspace, simply run: ``` @@ -80,47 +84,56 @@ pnpm --filter core migrate-dev ``` ## Self-Hosting and Deployment + Guidance for self-hosting and deployment is coming soon. In the meantime, you can read about how we currently deploy the app in [DEVELOPMENT.md](https://github.com/pubpub/platform/blob/main/DEVELOPMENT.md). With small modifications, most development teams can use the included terraform configuration to run the app on commodity cloud infrastructure of their choice. ## Bugs, Feature Requests, Help, and Feedback + We use the [Discussion Forum](https://github.com/pubpub/platform/discussions) for feature requests, ideas, and general feedback, and [GitHub Issues](https://github.com/pubpub/platform/issues/) for day-to-day development. Thus, if you're unsure where to post your feedback, start with a discussion. We can always transfer it to an issue later if needed. ### Bugs + If you have a specific bug to report, feel free to add a [new bug issue](https://github.com/pubpub/platform/issues/new?assignees=&labels=bug&projects=&template=bug-issue.md&title=) to the PubPub Platform Repo. If you submit a bug, we ask that you use the available template and fill it out to the best of your ability, including information about your browser and operating system, detailed, written step-by-step instructions to reproduce the bug and screenshots or a screen recording when relevant. Having all of this information up-front helps us solve any issues faster. Please search the [issue list](https://github.com/pubpub/platform/issues) first to make sure your bug hasn't already been reported. If it has, add your feedback to the preexisting issue as a comment. ### Feature Requests, Feedback, and Help + If you have a feature request, idea, general feedback, or need help with PubPub, we'd love you to post a discussion on the [Discussion Forum](https://github.com/pubpub/platform/discussions). As with bug reports, make sure to search the forum first to see if the community has already discussed your idea or solved your issue. If we have, feel free to join in on that ongoing discussion. Remember to be polite and courteous. All activity on this repository is governed by the [Knowledge Futures Code of Conduct](https://github.com/knowledgefutures/general/blob/master/CODE_OF_CONDUCT.md). ## Contributing + In the coming weeks, we'll be developing more thorough contribution guides, particularly for contributors interested in: -- Extending PubPub Platform with new Actions and Rules -- Extending the PubPub Platform API -- Contributing to self-hosting scripts and guides on common cloud hosting -- Contributing documentation for developers or users + +- Extending PubPub Platform with new Actions and Rules +- Extending the PubPub Platform API +- Contributing to self-hosting scripts and guides on common cloud hosting +- Contributing documentation for developers or users For now, you can browse the [issue list](https://github.com/pubpub/platform/issues) and comment on any issues you may want -to take on. We'll be in touch shortly +to take on. We'll be in touch shortly ### Pull Requests + Our preferred practice is for contributors to create a branch using the format `initials/descriptive-name` and submit it against main. Request names should be prefixed with one of the following categories: -- fix: for commits focused on specific bug fixes -- feature: for commits that introduce a new feature -- update: for commits that improve an existing feature -- dev: for commits that focus solely on documentation, refactoring code, or developer experience updates +- fix: for commits focused on specific bug fixes +- feature: for commits that introduce a new feature +- update: for commits that improve an existing feature +- dev: for commits that focus solely on documentation, refactoring code, or developer experience updates Request descriptions should use to our Pull Request template, including a clear rationale for the PR, listing any issues resolved, and describing the test plan for the request, including both tests you wrote and step-by-step descriptions of any manual QA that may be needed. Finally, we request that any complex code, new terminology, potentially decisions you made, or any areas you'd like feedback on be commented on inline in GitHub's files changed interface. ## User-Facing Documentation -User-facing documentation is a work in progress, and can be found at https://help.knowledgefutures.org. + +User-facing documentation is a work in progress, and can be found at https://help.knowledgefutures.org. ## Supporting Services + Thank you to these groups for providing their tools for free to PubPub's open source mission. [![Browserstack-logo@2x](https://user-images.githubusercontent.com/1000455/64237395-318a4c80-cef4-11e9-8b78-98ed3ec58ce3.png)](https://www.browserstack.com/) diff --git a/core/.env.development b/core/.env.development index 4316607d3..caa875ff5 100644 --- a/core/.env.development +++ b/core/.env.development @@ -16,3 +16,7 @@ HONEYCOMB_API_KEY="xxx" ARTILLERY_CLOUD_API_KEY="xxx" KYSELY_DEBUG="true" + +DATACITE_REPOSITORY_ID="" +DATACITE_PASSWORD="" +DATACITE_API_URL="https://api.test.datacite.org" \ No newline at end of file diff --git a/core/actions/_lib/resolvePubfields.ts b/core/actions/_lib/resolvePubfields.ts index f653ddfce..9ab2615b2 100644 --- a/core/actions/_lib/resolvePubfields.ts +++ b/core/actions/_lib/resolvePubfields.ts @@ -58,5 +58,6 @@ export const resolveWithPubfields = >( return { ...rest, ...pv, + pubFields, }; }; diff --git a/core/actions/api/index.ts b/core/actions/api/index.ts index 835ed8144..7fc3600ff 100644 --- a/core/actions/api/index.ts +++ b/core/actions/api/index.ts @@ -5,6 +5,7 @@ import type * as z from "zod"; import type { Event } from "db/public"; import { pubEnteredStage, pubInStageForDuration, pubLeftStage } from "../_lib/rules"; +import * as datacite from "../datacite/action"; import * as email from "../email/action"; import * as googleDriveImport from "../googleDriveImport/action"; import * as http from "../http/action"; @@ -21,6 +22,7 @@ export const actions = { [http.action.name]: http.action, [move.action.name]: move.action, [googleDriveImport.action.name]: googleDriveImport.action, + [datacite.action.name]: datacite.action, } as const; export const getActionByName = (name: N) => { diff --git a/core/actions/datacite/action.ts b/core/actions/datacite/action.ts new file mode 100644 index 000000000..c5f58d17c --- /dev/null +++ b/core/actions/datacite/action.ts @@ -0,0 +1,92 @@ +import * as z from "zod"; + +import { Action } from "db/public"; +import { Globe } from "ui/icon"; + +import { defineAction } from "../types"; + +export const action = defineAction({ + name: Action.datacite, + config: { + schema: z.object({ + doi: z.string().optional(), + doiPrefix: z.string().optional(), + doiSuffix: z.string().optional(), + title: z.string().optional(), + url: z.string(), + publisher: z.string(), + publicationDate: z.date(), + creator: z.string(), + creatorName: z.string(), + }), + fieldConfig: { + doi: { + allowedSchemas: true, + }, + doiSuffix: { + allowedSchemas: true, + }, + title: { + allowedSchemas: true, + }, + url: { + allowedSchemas: true, + }, + publisher: { + allowedSchemas: true, + }, + publicationDate: { + allowedSchemas: true, + }, + creator: { + allowedSchemas: true, + }, + creatorName: { + allowedSchemas: true, + }, + }, + }, + params: { + schema: z.object({ + doi: z.string().optional(), + doiPrefix: z.string().optional(), + doiSuffix: z.string().optional(), + title: z.string().optional(), + url: z.string(), + publisher: z.string(), + publicationDate: z.date(), + creator: z.string(), + creatorName: z.string(), + }), + fieldConfig: { + doi: { + allowedSchemas: true, + }, + doiSuffix: { + allowedSchemas: true, + }, + title: { + allowedSchemas: true, + }, + url: { + allowedSchemas: true, + }, + publisher: { + allowedSchemas: true, + }, + publicationDate: { + allowedSchemas: true, + }, + creator: { + allowedSchemas: true, + }, + creatorName: { + allowedSchemas: true, + }, + }, + }, + description: "Deposit a pub to DataCite", + icon: Globe, + experimental: true, + superAdminOnly: true, +}); diff --git a/core/actions/datacite/run.test.ts b/core/actions/datacite/run.test.ts new file mode 100644 index 000000000..b2ff103cb --- /dev/null +++ b/core/actions/datacite/run.test.ts @@ -0,0 +1,275 @@ +import { afterEach } from "node:test"; +import { isNull } from "util"; + +import { describe, expect, it, vitest } from "vitest"; + +import type { + ActionRunsId, + CommunitiesId, + PubFieldsId, + PubsId, + PubTypesId, + PubValuesId, + StagesId, +} from "db/public"; +import { CoreSchemaType } from "db/public"; + +import type { ActionPub, RunProps } from "../types"; +import type { action } from "./action"; +import type { ClientExceptionOptions } from "~/lib/serverActions"; +import { updatePub } from "~/lib/server"; +import { didSucceed } from "~/lib/serverActions"; +import { run } from "./run"; + +vitest.mock("~/lib/env/env.mjs", () => { + return { + env: { + DATACITE_API_URL: "https://api.test.datacite.org", + DATACITE_REPOSITORY_ID: "id", + DATACITE_REPOSITORY_PASSWORD: "password", + }, + }; +}); + +vitest.mock("~/lib/server", () => { + return { + getPubsWithRelatedValuesAndChildren: () => { + return { ...pub, values: [] }; + }, + updatePub: vitest.fn(() => { + return {}; + }), + }; +}); + +type Fetch = typeof global.fetch; + +// TODO: use vitest.stubGlobal with a little wrapper that lets us pass an array +// of responses +let _fetch = global.fetch; +const mockFetch = (...fns: Fetch[]) => { + const mock = vitest.fn((url, init) => { + const next = fns.shift(); + if (next === undefined) { + throw new Error("mocked fetch called too many times"); + } + return next(url, init); + }); + global.fetch = mock; + return mock; +}; + +const unmockFetch = () => { + global.fetch = _fetch; +}; + +// { +// "pubpub:doi": undefined, +// "pubpub:url": "https://www.pubpub.org", +// "pubpub:publication-date": new Date("01-01-2024").toString(), +// }, + +const pub = { + id: "" as PubsId, + values: [ + { + id: "" as PubValuesId, + fieldId: "" as PubFieldsId, + fieldName: "", + fieldSlug: "pubpub:doi", + value: undefined, + createdAt: new Date(), + updatedAt: new Date(), + schemaName: CoreSchemaType.String, + relatedPubId: null, + }, + { + id: "" as PubValuesId, + fieldId: "" as PubFieldsId, + fieldName: "", + fieldSlug: "pubpub:url", + value: "https://www.pubpub.org", + createdAt: new Date(), + updatedAt: new Date(), + schemaName: CoreSchemaType.URL, + relatedPubId: null, + }, + { + id: "" as PubValuesId, + fieldId: "" as PubFieldsId, + fieldName: "", + fieldSlug: "pubpub:publication-date", + value: new Date("01-01-2024").toString(), + createdAt: new Date(), + updatedAt: new Date(), + schemaName: CoreSchemaType.DateTime, + relatedPubId: null, + }, + ], + children: [], + communityId: "" as CommunitiesId, + createdAt: new Date(), + updatedAt: new Date(), + title: "A Preprint", + pubType: { + id: "" as PubTypesId, + communityId: "" as CommunitiesId, + name: "Preprint", + description: "", + createdAt: new Date(), + updatedAt: new Date(), + fields: [], + }, + pubTypeId: "" as PubTypesId, + stageId: null, + parentId: null, +} as ActionPub; + +const RUN_OPTIONS: RunProps = { + actionRunId: "" as ActionRunsId, + stageId: "" as StagesId, + communityId: "" as CommunitiesId, + config: { + url: "placeholder", + publisher: "Knowledge Futures", + publicationDate: new Date(), + creator: "placeholder", + creatorName: "placeholder", + doiPrefix: undefined, + doi: undefined, + doiSuffix: undefined, + pubFields: { + url: ["pubpub:url"], + creator: ["pubpub:author"], + creatorName: ["pubpub:name"], + publicationDate: ["pubpub:publication-date"], + doi: ["pubpub:doi"], + doiSuffix: ["pubpub:doi-suffix"], + }, + }, + configFieldOverrides: new Set(), + pub, + args: {} as any, + argsFieldOverrides: new Set(), + lastModifiedBy: "system|0", +}; + +const makeStubDatacitePayload = (doi?: string) => { + return { + data: { + attributes: { + doi, + }, + }, + }; +}; + +const makeStubDataciteResponse = (doi?: string) => + new Response(JSON.stringify(makeStubDatacitePayload(doi))); + +describe("DataCite action", () => { + afterEach(() => { + unmockFetch(); + }); + + it("creates a deposit if the pub does not have a DOI and a DOI prefix is configured", async () => { + const doi = "10.100/a-preprint"; + const fetch = mockFetch(async () => makeStubDataciteResponse(doi)); + await run({ ...RUN_OPTIONS, config: { ...RUN_OPTIONS.config, doiPrefix: "10.100" } }); + + expect(fetch).toHaveBeenCalledOnce(); + expect(fetch.mock.lastCall![1]!.method).toBe("POST"); + expect(updatePub).toHaveBeenCalledWith( + expect.objectContaining({ + pubValues: { + "pubpub:doi": doi, + }, + }) + ); + }); + + it("creates a deposit if the pub has a DOI not recognized by DataCite", async () => { + const doi = "10.100/a-preprint"; + const fetch = mockFetch( + async () => new Response(undefined, { status: 404 }), + async () => makeStubDataciteResponse(doi) + ); + await run({ ...RUN_OPTIONS, config: { ...RUN_OPTIONS.config, doi } }); + + expect(fetch.mock.lastCall![1]!.method).toBe("POST"); + }); + + it("updates a deposit if the pub has a DOI recognized by DataCite", async () => { + const doi = "10.100/a-preprint"; + const fetch = mockFetch( + async () => new Response(), + async () => makeStubDataciteResponse(doi) + ); + await run({ ...RUN_OPTIONS, config: { ...RUN_OPTIONS.config, doi } }); + + expect(fetch.mock.lastCall![1]!.method).toBe("PUT"); + }); + + it("reports an error if the pub has neither a DOI nor a DOI suffix and no DOI prefix is configured", async () => { + const result = await run(RUN_OPTIONS); + + expect(didSucceed(result)).toBe(false); + }); + + it("reports an error when the DOI fails to persist", async () => { + const error = new Error(); + vitest.mocked(updatePub).mockImplementationOnce(() => { + throw error; + }); + mockFetch(async () => makeStubDataciteResponse()); + const result = await run({ + ...RUN_OPTIONS, + config: { ...RUN_OPTIONS.config, doiPrefix: "10.100" }, + }); + + expect(didSucceed(result)).toBe(false); + expect((result as ClientExceptionOptions).cause).toBe(error); + }); + + it("uses the DOI suffix field if provided", async () => { + const doiPrefix = "10.100"; + const doiSuffix = "100"; + const doi = `${doiPrefix}/${doiSuffix}`; + + const fetch = mockFetch( + async () => new Response("{}"), + async () => makeStubDataciteResponse(doi) + ); + + await run({ + ...RUN_OPTIONS, + config: { + ...RUN_OPTIONS.config, + doiPrefix, + doiSuffix, + }, + }); + + const call = fetch.mock.lastCall![1]!; + const body = JSON.parse(call.body as string); + expect(body.data.attributes.doi).toBe(doi); + expect(call.method).toBe("POST"); + }); + + it.todo("transforms related contributor pubs into DataCite creators"); + + // unsure about these two one: + it.todo("transforms child pubs with DOIs and related pubs into relatedIdentifiers"); + it.todo("transforms child pubs without DOIs into relatedItems"); + + it.todo("deposits explicit and pub-provided metadata fields", () => { + // Title + // Publisher + // Publication year + // "Created" date + // "Updated" date + // Resource type -- probably hardcoded "Preprint" for now + // URL + // And more! (License etc.) + }); +}); diff --git a/core/actions/datacite/run.ts b/core/actions/datacite/run.ts new file mode 100644 index 000000000..8e4061348 --- /dev/null +++ b/core/actions/datacite/run.ts @@ -0,0 +1,266 @@ +"use server"; + +import * as z from "zod"; + +import type { ProcessedPub } from "contracts"; +import type { PubsId } from "db/public"; +import { assert, AssertionError, expect } from "utils"; + +import type { ActionPub, ActionPubType } from "../types"; +import type { action } from "./action"; +import type { components } from "./types"; +import { env } from "~/lib/env/env.mjs"; +import { getPubsWithRelatedValuesAndChildren, updatePub } from "~/lib/server"; +import { isClientExceptionOptions } from "~/lib/serverActions"; +import { defineRun } from "../types"; + +type ConfigSchema = z.infer<(typeof action)["config"]["schema"]>; +type Config = ConfigSchema & { pubFields: { [K in keyof ConfigSchema]?: string[] } }; +type Payload = components["schemas"]["Doi"]; + +type RelatedPubs = Awaited< + ReturnType> +>[number]["values"]; + +const encodeDataciteCredentials = (username: string, password: string) => + Buffer.from(`${username}:${password}`).toString("base64"); + +const makeDataciteCreatorFromAuthorPub = (pub: ProcessedPub, creatorNameFieldSlug: string) => { + const name = pub.values.find((value) => value.fieldSlug === creatorNameFieldSlug)?.value; + assert(typeof name === "string"); + return { + name, + // TODO: author/creator affiliations + affiliation: [], + nameIdentifiers: [], + }; +}; + +const deriveCreatorsFromRelatedPubs = ( + relatedPubs: RelatedPubs, + creatorFieldSlug: string, + creatorNameFieldSlug: string +) => + relatedPubs + .filter((v) => v.fieldSlug === creatorFieldSlug) + .map((v) => v.relatedPub!) + .map((pub) => makeDataciteCreatorFromAuthorPub(pub, creatorNameFieldSlug)); + +const makeDatacitePayload = async (pub: ActionPub, config: Config): Promise => { + const titleFieldSlug = config.pubFields.title?.[0]; + const urlFieldSlug = expect( + config.pubFields.url?.[0], + "The DataCite action is missing a URL field override." + ); + const creatorFieldSlug = expect( + config.pubFields.creator?.[0], + "The DataCite action is missing a creator field override." + ); + const creatorNameFieldSlug = expect( + config.pubFields.creatorName?.[0], + "The DataCite action is missing a creator name field override." + ); + const publicationDateFieldSlug = expect( + config.pubFields.publicationDate?.[0], + "The DataCite action is missing a publication date field override." + ); + + const { values } = await getPubsWithRelatedValuesAndChildren({ + pubId: pub.id as PubsId, + communityId: pub.communityId, + }); + + const relatedPubs = values.filter((v) => v.relatedPub != null); + + const creators = deriveCreatorsFromRelatedPubs( + relatedPubs, + creatorFieldSlug, + creatorNameFieldSlug + ); + + let title: string; + + if (titleFieldSlug !== undefined) { + title = expect( + pub.values.find((v) => v.fieldSlug === titleFieldSlug)?.value as string | undefined, + "The pub has no corresponding value for the configured title field." + ); + } else { + title = expect(pub.title, "The pub has no title."); + } + + const url = pub.values.find((v) => v.fieldSlug === urlFieldSlug)?.value; + assert( + typeof url === "string", + "The pub is missing a value corresponding to the configured URL field override." + ); + + const publicationDate = pub.values.find((v) => v.fieldSlug === publicationDateFieldSlug)?.value; + assert( + typeof publicationDate === "string", + "The pub is missing a value corresponding to the configured publication date field override." + ); + + const publicationYear = new Date(publicationDate).getFullYear(); + + let doi = config.doi; + + if (!doi) { + assert( + config.doiPrefix !== undefined, + "The DataCite action must be configured with a DOI prefix to form a complete DOI." + ); + + // If a prefix and suffix exist, join the parts to make a DOI. If the + // pub does not have a suffix, DataCite will auto-generate a DOI using + // the prefix. + if (config.doiSuffix) { + doi = `${config.doiPrefix}/${config.doiSuffix}`; + } + } + + return { + data: { + type: "dois", + attributes: { + event: "publish", + doi, + prefix: config.doiPrefix, + titles: [{ title }], + creators, + publisher: config.publisher, + publicationYear, + dates: [ + { + date: pub.createdAt.toString(), + dateType: "Created", + }, + { + date: pub.updatedAt.toString(), + dateType: "Updated", + }, + ], + types: { + resourceTypeGeneral: "Preprint", + }, + url, + }, + }, + }; +}; + +const makeRequestHeaders = () => { + return { + Accept: "application/vnd.api+json", + Authorization: + "Basic " + + encodeDataciteCredentials( + String(env.DATACITE_REPOSITORY_ID), + String(env.DATACITE_PASSWORD) + ), + "Content-Type": "application/json", + }; +}; + +const checkDoi = async (doi: string) => { + const response = await fetch(`${env.DATACITE_API_URL}/dois/${doi}`, { + method: "GET", + headers: makeRequestHeaders(), + }); + + return response.ok; +}; + +const createPubDeposit = async (payload: Payload) => { + const response = await fetch(`${env.DATACITE_API_URL}/dois`, { + method: "POST", + headers: makeRequestHeaders(), + body: JSON.stringify(payload), + }); + + if (!response.ok) { + return { + title: "Failed to create DOI", + error: "An error occurred while depositing the pub to DataCite.", + }; + } + + return response.json(); +}; + +const updatePubDeposit = async (payload: Payload) => { + const doi = expect(payload?.data?.attributes?.doi); + const response = await fetch(`${env.DATACITE_API_URL}/dois/${doi}`, { + method: "PUT", + headers: makeRequestHeaders(), + body: JSON.stringify(payload), + }); + + if (!response.ok) { + return { + title: "Failed to update DOI", + error: "An error occurred while depositing the pub to DataCite.", + }; + } + + return response.json(); +}; + +export const run = defineRun(async ({ pub, config, args, lastModifiedBy }) => { + const depositConfig = { ...config, ...args }; + + let payload: Payload; + try { + payload = await makeDatacitePayload(pub, depositConfig); + } catch (error) { + if (error instanceof AssertionError) { + return { + title: "Failed to create DataCite deposit", + error: error.message, + cause: undefined, + }; + } + throw error; + } + + const depositResult = + // If the pub already has a DOI, and DataCite recognizes it, + depositConfig.doi && (await checkDoi(depositConfig.doi)) + ? // Update the pub metadata in DataCite + await updatePubDeposit(payload) + : // Otherwise, deposit the pub to DataCite + await createPubDeposit(payload); + + if (isClientExceptionOptions(depositResult)) { + return depositResult; + } + + // If the pub does not have a DOI, persist the newly generated DOI from + // DataCite + if (!depositConfig.doi) { + const doiFieldSlug = expect(config.pubFields.doi?.[0]); + + try { + await updatePub({ + pubId: pub.id, + communityId: pub.communityId, + pubValues: { + [doiFieldSlug]: depositResult.data.attributes.doi, + }, + continueOnValidationError: false, + lastModifiedBy, + }); + } catch (error) { + return { + title: "Failed to save DOI", + error: "The pub was deposited to DataCite, but we were unable to update the pub's DOI in PubPub", + cause: error, + }; + } + } + + return { + data: {}, + success: true, + }; +}); diff --git a/core/actions/datacite/types.ts b/core/actions/datacite/types.ts new file mode 100644 index 000000000..10caeb5d8 --- /dev/null +++ b/core/actions/datacite/types.ts @@ -0,0 +1,1316 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/activities": { + /** Get a JSON API result of activities. */ + get: { + parameters: { + query?: { + /** @description Find activity by an id. */ + id?: string; + /** @description Find activities by array of activity ids */ + ids?: string[]; + /** @description Search the index by keyword or query string syntax. */ + query?: string; + /** @description Pagination - page number */ + "page[number]"?: number; + /** @description Pagination - page size */ + "page[size]"?: number; + /** @description Pagination - page cursor (used instead of page[number]) */ + "page[cursor]"?: string; + }; + }; + responses: { + /** @description A JSON API result of activities. */ + 200: { + content: { + "application/vnd.api+json": components["schemas"]["Activity"]; + }; + }; + }; + }; + }; + "/activities/{id}": { + /** Get a JSON API result of a specific activity. */ + get: { + parameters: { + path: { + /** @description Activity ID */ + id: string; + }; + }; + responses: { + /** @description A JSON object. */ + 200: { + content: { + "application/vnd.api+json": components["schemas"]["Activity"]; + }; + }; + }; + }; + }; + "/client-prefixes": { + /** Return a list of client-prefixes. */ + get: { + parameters: { + query?: { + query?: string; + year?: number; + "client-id"?: string; + "prefix-id"?: string; + "page[number]"?: number; + sort?: "name" | "-name" | "created" | "-created"; + }; + }; + responses: { + /** @description A JSON array of client-prefixes. */ + 200: { + content: { + "application/vnd.api+json": components["schemas"]["ClientPrefix"]; + }; + }; + }; + }; + }; + "/clients": { + /** Return a list of clients (repositories). */ + get: { + parameters: { + query?: { + query?: string; + /** @description The year the client was created. */ + year?: number; + "provider-id"?: string; + software?: + | "ckan" + | "dataverse" + | "dspace" + | "eprints" + | "fedora" + | "invenio" + | "islandora" + | "nesstar" + | "open journal systems (ojs)" + | "opus" + | "samvera" + | "pubman" + | "mycore" + | "other" + | "unknown"; + "client-type"?: "repository" | "periodical"; + "repository-type"?: + | "disciplinary" + | "governmental" + | "institutional" + | "multidisciplinary" + | "project-related" + | "other"; + certificate?: + | "CLARIN" + | "CoreTrustSeal" + | "DIN 31644" + | "DINI" + | "DSA" + | "RatSWD" + | "WDS"; + "page[number]"?: number; + "page[size]"?: number; + include?: "provider" | "repository"; + sort?: "relevance" | "name" | "-name" | "created" | "-created"; + }; + }; + responses: { + /** @description A JSON array of clients. */ + 200: { + content: { + "application/vnd.api+json": components["schemas"]["Client"]; + }; + }; + }; + }; + }; + "/clients/totals": { + /** Return clients DOI production statistics. */ + get: { + parameters: { + query?: { + "provider-id"?: string; + state?: "findable" | "registered" | "draft"; + }; + }; + responses: { + /** @description A JSON array of clients stats. */ + 200: { + content: never; + }; + }; + }; + }; + "/clients/{id}": { + /** Return a client. */ + get: { + parameters: { + path: { + /** @description Client ID */ + id: string; + }; + }; + responses: { + /** @description A JSON object. */ + 200: { + content: { + "application/vnd.api+json": components["schemas"]["Client"]; + }; + }; + }; + }; + }; + "/dois": { + /** Return a list of dois. */ + get: { + parameters: { + query?: { + query?: string; + created?: number; + registered?: number; + published?: number; + "provider-id"?: string; + "client-id"?: string; + "consortium-id"?: string; + prefix?: string; + certificate?: + | "CLARIN" + | "CoreTrustSeal" + | "DIN 31644" + | "DINI" + | "DSA" + | "RatSWD" + | "WDS"; + "person-id"?: string; + "affiliation-id"?: string; + "resource-type-id"?: + | "audiovisual" + | "book" + | "book-chapter" + | "collection" + | "computational-notebook" + | "conference-paper" + | "conference-proceeding" + | "data-paper" + | "dataset" + | "dissertation" + | "event" + | "image" + | "interactive-resource" + | "journal" + | "journal-article" + | "model" + | "output-management-plan" + | "peer-review" + | "physical-object" + | "preprint" + | "report" + | "service" + | "software" + | "sound" + | "standard" + | "text" + | "workflow" + | "other"; + subject?: string; + "field-of-science"?: string; + license?: string; + "schema-version"?: string; + state?: "findable" | "registered" | "draft"; + /** @description Set affiliation=true to see additional affiliation information such as the affiliation identifier that was added in Schema 4.3. */ + affiliation?: boolean; + "link-check-status"?: 200 | 400 | 401 | 403 | 404 | 410 | 429 | 500 | 502 | 503; + /** @description Retreive a random sample of DOIs. When true, the page[number] parameter is ignored. */ + random?: boolean; + "sample-size"?: number; + "sample-group"?: "client" | "provider" | "resource-type"; + "page[number]"?: number; + "page[size]"?: number; + "page[cursor]"?: string; + include?: "client" | "media"; + sort?: + | "relevance" + | "name" + | "-name" + | "created" + | "-created" + | "updated" + | "-updated"; + }; + }; + responses: { + /** @description A JSON array of dois. */ + 200: { + content: { + "application/vnd.api+json": components["schemas"]["Doi"]; + }; + }; + }; + }; + /** Add a new doi. */ + post: { + requestBody: { + content: { + "application/vnd.api+json": components["schemas"]["Doi"]; + }; + }; + responses: { + /** @description Created */ + 201: { + content: never; + }; + }; + }; + }; + "/dois/{id}": { + /** Return a doi. */ + get: { + parameters: { + path: { + /** @description DOI */ + id: string; + }; + }; + responses: { + /** @description A JSON object. */ + 200: { + content: { + "application/vnd.api+json": components["schemas"]["Doi"]; + }; + }; + }; + }; + /** Update a doi. */ + put: { + parameters: { + path: { + /** @description DOI */ + id: string; + }; + }; + requestBody: { + content: { + "application/vnd.api+json": components["schemas"]["Doi"]; + }; + }; + responses: { + /** @description OK */ + 200: { + content: never; + }; + }; + }; + /** Delete a doi (for DOIs in draft state only). */ + delete: { + parameters: { + path: { + /** @description DOI */ + id: string; + }; + }; + responses: { + /** @description No content */ + 204: { + content: never; + }; + }; + }; + }; + "/dois/{id}/activities": { + /** Return activity for a specific DOI. */ + get: { + parameters: { + path: { + /** @description DOI */ + id: string; + }; + }; + responses: { + /** @description A JSON object. */ + 200: { + content: { + "application/vnd.api+json": components["schemas"]["Activity"]; + }; + }; + }; + }; + }; + "/events": { + /** Return a list of events. */ + get: { + parameters: { + query?: { + query?: string; + "subj-id"?: string; + "obj-id"?: string; + doi?: string; + orcid?: string; + prefix?: string; + subtype?: string; + "citation-type"?: string; + "source-id"?: string; + "registrant-id"?: string; + "relation-type-id"?: string; + issn?: string; + "publication-year"?: string; + "year-month"?: string; + "page[number]"?: number; + "page[size]"?: number; + "page[cursor]"?: string; + include?: "subj" | "obj"; + sort?: "relevance" | "name" | "-name" | "created" | "-created"; + }; + }; + responses: { + /** @description A JSON array of events. */ + 200: { + content: { + "application/vnd.api+json": components["schemas"]["Event"]; + }; + }; + }; + }; + }; + "/events/{id}": { + /** Return an event. */ + get: { + parameters: { + path: { + /** @description Event */ + id: string; + }; + }; + responses: { + /** @description A JSON array of events. */ + 200: { + content: { + "application/vnd.api+json": components["schemas"]["Event"]; + }; + }; + }; + }; + }; + "/heartbeat": { + /** Return the current status of the REST API. */ + get: { + responses: { + /** @description REST API is operating normally. */ + 200: { + content: { + "text/plain": string; + }; + }; + /** @description REST API is not working properly. */ + 500: { + content: { + "text/plain": string; + }; + }; + }; + }; + }; + "/prefixes": { + /** Return a list of prefixes. */ + get: { + parameters: { + query?: { + year?: number; + state?: "with-repository" | "without-repository" | "unassigned"; + }; + }; + responses: { + /** @description A JSON array of prefixes. */ + 200: { + content: { + "application/vnd.api+json": components["schemas"]["Prefix"]; + }; + }; + }; + }; + }; + "/prefixes/totals": { + /** Return prefixes DOI production statistics. */ + get: { + parameters: { + query?: { + "client-id"?: string; + /** @description Must be authenticated to view registered and draft DOIs. */ + state?: "findable" | "registered" | "draft"; + }; + }; + responses: { + /** @description A JSON array of prefixes stats. */ + 200: { + content: never; + }; + }; + }; + }; + "/prefixes/{id}": { + /** Return a prefix. */ + get: { + parameters: { + path: { + /** @description Prefix */ + id: string; + }; + }; + responses: { + /** @description Return a prefix. */ + 200: { + content: { + "application/vnd.api+json": components["schemas"]["Prefix"]; + }; + }; + }; + }; + }; + "/provider-prefixes": { + /** Return a list of provider-prefixes. */ + get: { + parameters: { + query?: { + query?: string; + year?: number; + "consortium-id"?: string; + "provider-id"?: string; + "prefix-id"?: string; + "page[number]"?: number; + sort?: "name" | "-name" | "created" | "-created"; + }; + }; + responses: { + /** @description A JSON array of provider-prefixes. */ + 200: { + content: { + "application/vnd.api+json": components["schemas"]["ProviderPrefix"]; + }; + }; + }; + }; + }; + "/providers": { + /** Return a list of providers (including members and consortium organizations). */ + get: { + parameters: { + query?: { + query?: string; + /** @description The year the provider was created. */ + year?: number; + "consortium-id"?: string; + region?: "amer" | "apac" | "emea"; + "member-type"?: + | "consortium_organization" + | "direct_member" + | "governmentAgency" + | "consortium" + | "member_only" + | "developer"; + "organization-type"?: + | "academicInstitution" + | "governmentAgency" + | "nationalInstitution" + | "publisher" + | "professionalSociety" + | "researchInstitution" + | "serviceProvider" + | "internationalOrganization" + | "other"; + "focus-area"?: + | "naturalSciences" + | "engineeringAndTechnology" + | "medicalAndHealthSciences" + | "agriculturalSciences" + | "socialSciences" + | "humanities" + | "general"; + "has-required-contacts"?: boolean; + "page[number]"?: number; + "page[size]"?: number; + sort?: "relevance" | "name" | "-name" | "created" | "-created"; + }; + }; + responses: { + /** @description A JSON array of prefixes. */ + 200: { + content: { + "application/vnd.api+json": components["schemas"]["Provider"]; + }; + }; + }; + }; + }; + "/providers/totals": { + /** Return providers DOI production statistics. */ + get: { + parameters: { + query?: { + state?: "findable" | "registered" | "draft"; + }; + }; + responses: { + /** @description A JSON array of providers stats. */ + 200: { + content: never; + }; + }; + }; + }; + "/providers/{id}": { + /** Return a provider. */ + get: { + parameters: { + path: { + /** @description Provider ID */ + id: string; + }; + }; + responses: { + /** @description A JSON object. */ + 200: { + content: { + "application/vnd.api+json": components["schemas"]["Provider"]; + }; + }; + }; + }; + }; + "/reports": { + /** A JSON array of reports. */ + get: { + parameters: { + query?: { + /** @description Name of the Platform the usage is being requested for. This can be omitted if the service provides usage for only one platform. */ + platform?: string; + /** @description The long name of the report. */ + "report-name"?: string; + /** @description The report ID or code or shortname. Typically this will be the same code provided in the Report parameter of the request. */ + "report-id"?: string; + /** @description The release or version of the report. */ + release?: string; + /** @description Time the report was prepared. Format as defined by date-time - RFC3339 */ + created?: string; + /** @description Name of the organization producing the report. */ + "created-by"?: string; + "page[number]"?: number; + "page[size]"?: number; + }; + }; + responses: { + /** @description A JSON array of reports. */ + 200: { + content: { + "application/vnd.api+json": components["schemas"]["Report"]; + }; + }; + }; + }; + /** Add a new report. */ + post: { + requestBody: { + content: { + "application/vnd.api+json": components["schemas"]["Report"]; + }; + }; + responses: { + /** @description Created */ + 201: { + content: never; + }; + }; + }; + }; + "/reports/{id}": { + /** Return a report. */ + get: { + parameters: { + path: { + /** @description Report */ + id: string; + }; + }; + responses: { + /** @description A JSON object. */ + 200: { + content: { + "application/vnd.api+json": components["schemas"]["Report"]; + }; + }; + }; + }; + /** Update a report. */ + put: { + parameters: { + path: { + /** @description Report */ + id: string; + }; + }; + requestBody: { + content: { + "application/vnd.api+json": components["schemas"]["Report"]; + }; + }; + responses: { + /** @description OK */ + 200: { + content: never; + }; + }; + }; + /** Delete a report. */ + delete: { + parameters: { + path: { + /** @description Report */ + id: string; + }; + }; + responses: { + /** @description No content */ + 204: { + content: never; + }; + }; + }; + }; +} + +export type webhooks = Record; + +export interface components { + schemas: { + /** + * @example { + * "id": "tib.pangaea", + * "attributes": { + * "name": "Pangaea" + * } + * } + */ + Client: { + data?: { + id?: string; + type?: string; + attributes?: { + name?: string; + symbol?: string; + contactName?: string; + contactEmail?: string; + description?: string; + domains?: string; + url?: string; + created?: string; + updated?: string; + }; + }; + }; + ClientPrefix: { + data?: { + id?: string; + type?: string; + attributes?: { + created?: string; + updated?: string; + }; + }; + }; + /** @description Represents an activity for an event within DataCite systems. */ + Activity: { + data?: { + /** @example 0000-0000-0000-0000 */ + id?: string; + type?: string; + attributes?: { + "prov:wasGeneratedBy"?: string; + "prov:generatedAtTime"?: string; + "prov:wasDerivedFrom"?: string; + "prov:wasAttributedTo"?: string; + action?: string; + version?: number; + changes?: Record; + }; + }; + }; + /** + * @description Represents a DOI and provides access to metadata attributes, further schema specific information can be found at https://schema.datacite.org + * @example { + * "data": { + * "type": "dois", + * "attributes": { + * "doi": "10.5438/0014", + * "prefix": "10.5438", + * "suffix": "0014", + * "identifiers": [ + * { + * "identifier": "https://doi.org/10.5438/0014", + * "identifierType": "DOI" + * } + * ], + * "creators": [ + * { + * "name": "DataCite Metadata Working Group" + * } + * ], + * "titles": [ + * { + * "title": "DataCite Metadata Schema Documentation for the Publication and Citation of Research Data v4.1" + * } + * ], + * "publisher": "DataCite", + * "publicationYear": 2017, + * "types": { + * "resourceTypeGeneral": "Text" + * }, + * "url": "https://schema.datacite.org/meta/kernel-4.1/" + * } + * } + * } + */ + Doi: { + data?: { + id?: string; + /** @enum {string} */ + type: "dois"; + attributes?: { + doi?: string; + prefix?: string; + suffix?: string; + /** + * @description Can be set to trigger a DOI state change. + * @enum {string} + */ + event?: "publish" | "register" | "hide"; + identifiers?: { + identifier?: string; + identifierType?: string; + }[]; + creators?: { + /** @enum {string} */ + nameType?: "Personal" | "Organizational"; + nameIdentifiers?: { + nameIdentifier?: string; + nameIdentifierScheme?: string; + schemeUri?: string; + }[]; + name: string; + givenName?: string; + familyName?: string; + affiliation?: { + affiliationIdentifier?: string; + affiliationIdentifierScheme?: string; + name?: string; + schemeUri?: string; + }[]; + }[]; + titles?: { + title: string; + /** @enum {string} */ + titleType?: "AlternativeTitle" | "Subtitle" | "TranslatedTitle" | "Other"; + lang?: string; + }[]; + publisher: string; + container?: { + readonly type?: string; + readonly identifier?: string; + readonly identifierType?: string; + readonly title?: string; + readonly volume?: string; + readonly issue?: string; + readonly firstPage?: string; + readonly lastPage?: string; + }; + publicationYear: number; + subjects?: { + subject?: string; + subjectScheme?: string; + schemeUri?: string; + valueUri?: string; + lang?: string; + }[]; + contributors?: { + /** @enum {string} */ + nameType?: "Personal" | "Organizational"; + nameIdentifiers?: { + nameIdentifier?: string; + nameIdentifierScheme?: string; + schemeUri?: string; + }[]; + name?: string; + givenName?: string; + familyName?: string; + affiliation?: { + affiliationIdentifier?: string; + affiliationIdentifierScheme?: string; + name?: string; + schemeUri?: string; + }[]; + /** @enum {string} */ + contributorType?: + | "ContactPerson" + | "DataCollector" + | "DataCurator" + | "DataManager" + | "Distributor" + | "Editor" + | "HostingInstitution" + | "Producer" + | "ProjectLeader" + | "ProjectManager" + | "ProjectMember" + | "RegistrationAgency" + | "RegistrationAuthority" + | "RelatedPerson" + | "Researcher" + | "ResearchGroup" + | "RightsHolder" + | "Sponsor" + | "Supervisor" + | "WorkPackageLeader" + | "Other"; + }[]; + dates?: { + date?: string; + /** @enum {string} */ + dateType?: + | "Accepted" + | "Available" + | "Copyrighted" + | "Collected" + | "Created" + | "Issued" + | "Submitted" + | "Updated" + | "Valid" + | "Withdrawn" + | "Other"; + }[]; + language?: string; + types?: { + /** @enum {string} */ + resourceTypeGeneral: + | "Audiovisual" + | "Book" + | "BookChapter" + | "Collection" + | "ComputationalNotebook" + | "ConferencePaper" + | "ConferenceProceeding" + | "DataPaper" + | "Dataset" + | "Dissertation" + | "Event" + | "Image" + | "InteractiveResource" + | "JournalArticle" + | "Model" + | "OutputManagementPlan" + | "PeerReview" + | "PhysicalObject" + | "Preprint" + | "Report" + | "Service" + | "Software" + | "Sound" + | "Standard" + | "Text" + | "Workflow" + | "Other"; + resourceType?: string; + schemaOrg?: string; + bibtex?: string; + citeproc?: string; + ris?: string; + }; + relatedIdentifiers?: { + relatedIdentifier?: string; + /** @enum {string} */ + relatedIdentifierType?: + | "ARK" + | "arXiv" + | "bibcode" + | "DOI" + | "EAN13" + | "EISSN" + | "Handle" + | "IGSN" + | "ISBN" + | "ISSN" + | "ISTC" + | "LISSN" + | "LSID" + | "PMID" + | "PURL" + | "UPC" + | "URL" + | "URN" + | "w3id"; + /** @enum {string} */ + relationType?: + | "IsCitedBy" + | "Cites" + | "IsSupplementTo" + | "IsSupplementedBy" + | "IsContinuedBy" + | "Continues" + | "IsDescribedBy" + | "Describes" + | "HasMetadata" + | "IsMetadataFor" + | "HasVersion" + | "IsVersionOf" + | "IsNewVersionOf" + | "IsPreviousVersionOf" + | "IsPartOf" + | "HasPart" + | "IsPublishedIn" + | "IsReferencedBy" + | "References" + | "IsDocumentedBy" + | "Documents" + | "IsCompiledBy" + | "Compiles" + | "IsVariantFormOf" + | "IsOriginalFormOf" + | "IsIdenticalTo" + | "IsReviewedBy" + | "Reviews" + | "IsDerivedFrom" + | "IsSourceOf" + | "IsRequiredBy" + | "Requires" + | "IsObsoletedBy" + | "Obsoletes"; + resourceTypeGeneral?: string; + }[]; + sizes?: string[]; + formats?: string[]; + version?: string; + rightsList?: { + rights?: string; + rightsUri?: string; + lang?: string; + }[]; + descriptions?: { + description?: string; + /** @enum {string} */ + descriptionType?: + | "Abstract" + | "Methods" + | "SeriesInformation" + | "TableOfContents" + | "TechnicalInfo" + | "Other"; + lang?: string; + }[]; + geoLocations?: { + geoLocationPoint?: Record; + geoLocationBox?: Record; + geoLocationPlace?: string; + }[]; + fundingReferences?: { + funderName?: string; + funderIdentifier?: string; + /** @enum {string} */ + funderIdentifierType?: + | "Crossref Funder ID" + | "GRID" + | "ISNI" + | "ROR" + | "Other"; + awardNumber?: string; + awardUri?: string; + awardTitle?: string; + }[]; + relatedItems?: { + /** @enum {string} */ + relatedItemType: + | "Audiovisual" + | "Book" + | "BookChapter" + | "Collection" + | "ComputationalNotebook" + | "ConferencePaper" + | "ConferenceProceeding" + | "DataPaper" + | "Dataset" + | "Dissertation" + | "Event" + | "Image" + | "InteractiveResource" + | "JournalArticle" + | "Model" + | "OutputManagementPlan" + | "PeerReview" + | "PhysicalObject" + | "Preprint" + | "Report" + | "Service" + | "Software" + | "Sound" + | "Standard" + | "Text" + | "Workflow" + | "Other"; + /** @enum {string} */ + relationType: + | "IsCitedBy" + | "Cites" + | "IsSupplementTo" + | "IsSupplementedBy" + | "IsContinuedBy" + | "Continues" + | "IsDescribedBy" + | "Describes" + | "HasMetadata" + | "IsMetadataFor" + | "HasVersion" + | "IsVersionOf" + | "IsNewVersionOf" + | "IsPreviousVersionOf" + | "IsPartOf" + | "HasPart" + | "IsPublishedIn" + | "IsReferencedBy" + | "References" + | "IsDocumentedBy" + | "Documents" + | "IsCompiledBy" + | "Compiles" + | "IsVariantFormOf" + | "IsOriginalFormOf" + | "IsIdenticalTo" + | "IsReviewedBy" + | "Reviews" + | "IsDerivedFrom" + | "IsSourceOf" + | "IsRequiredBy" + | "Requires" + | "IsObsoletedBy" + | "Obsoletes"; + relatedItemIdentifier?: { + relatedItemIdentifier?: string; + /** @enum {string} */ + relatedItemIdentifierType?: + | "ARK" + | "arXiv" + | "bibcode" + | "DOI" + | "EAN13" + | "EISSN" + | "Handle" + | "IGSN" + | "ISBN" + | "ISSN" + | "ISTC" + | "LISSN" + | "LSID" + | "PMID" + | "PURL" + | "UPC" + | "URL" + | "URN" + | "w3id"; + relatedMetadataScheme?: string; + schemeURI?: string; + schemeType?: string; + }; + creators?: { + name: string; + givenName?: string; + familyName?: string; + /** @enum {string} */ + nameType?: "Personal" | "Organizational"; + }[]; + titles?: { + title: string; + /** @enum {string} */ + titleType?: + | "AlternativeTitle" + | "Subtitle" + | "TranslatedTitle" + | "Other"; + }[]; + volume?: string; + issue?: string; + number?: string; + /** @enum {string} */ + numberType?: "Article" | "Chapter" | "Report" | "Other"; + firstPage?: string; + lastPage?: string; + publisher?: string; + publicationYear?: string; + edition?: string; + contributors?: { + name: string; + givenName?: string; + familyName?: string; + /** @enum {string} */ + nameType?: "Personal" | "Organizational"; + /** @enum {string} */ + contributorType: + | "ContactPerson" + | "DataCollector" + | "DataCurator" + | "DataManager" + | "Distributor" + | "Editor" + | "HostingInstitution" + | "Producer" + | "ProjectLeader" + | "ProjectManager" + | "ProjectMember" + | "RegistrationAgency" + | "RegistrationAuthority" + | "RelatedPerson" + | "Researcher" + | "ResearchGroup" + | "RightsHolder" + | "Sponsor" + | "Supervisor" + | "WorkPackageLeader" + | "Other"; + }[]; + }[]; + url?: string; + contentUrl?: string[]; + metadataVersion?: number; + schemaVersion?: string; + source?: string; + isActive?: boolean; + state?: string; + reason?: string; + /** @description Data describing the landing page, used by link checking. */ + landingPage?: { + checked?: string; + url?: string; + contentType?: string; + error?: string; + redirectCount?: number; + redirectUrls?: string[]; + downloadLatency?: number; + hasSchemaOrg?: boolean; + schemaOrgid?: string; + dcIdentifier?: string; + citationDoi?: string; + bodyhasPid?: boolean; + }; + created?: string; + registered?: string; + updated?: string; + }; + }; + }; + Event: { + data?: { + id?: string; + type?: string; + attributes?: { + subjId?: string; + objId?: string; + /** @enum {string} */ + messageAction?: "create" | "delete"; + relationTypeId?: string; + sourceToken?: string; + sourceId?: string; + total?: number; + license?: string; + occuredAt?: string; + timestamp?: string; + subj?: Record; + obj?: Record; + }; + }; + }; + Prefix: { + data?: { + prefix?: string; + }; + }; + ProviderPrefix: { + data?: { + id?: string; + attributes?: { + created?: string; + updated?: string; + }; + }; + }; + /** + * @example { + * "id": "bl", + * "attributes": { + * "name": "British Library", + * "symbol": "BL" + * } + * } + */ + Provider: { + data?: { + id?: string; + name?: string; + symbol?: string; + }; + }; + /** @description Describes the formatting needs for the COUNTER Dataset Report. Response may include the Report_Header (optional), Report_Datasets (usage stats). */ + Report: { + data?: { + /** @example 0000-0000-0000-0000 */ + id?: string; + /** + * @description The long name of the report. + * @example Dataset Report + */ + "report-name"?: string; + /** + * @description The report ID or code or shortname. Typically this will be the same code provided in the Report parameter of the request. + * @example DSR + */ + "report-id"?: string; + /** + * @description The release or version of the report. + * @example RD1 + */ + release?: string; + /** + * Format: dateTime + * @description Time the report was prepared. Format as defined by date-time - RFC3339 + * @example 2016-09-08T22:47:31Z + */ + created?: string; + /** + * @description Name of the organization producing the report. + * @example DataONE + */ + "created-by"?: string; + /** @description Zero or more report filters used for this report. Typically reflect filters provided on the Request. Filters limit the data to be reported on. */ + "report-filters"?: string; + /** @description Zero or more additional attributes applied to the report. Attributes inform the level of detail in the report. */ + "report-attributes"?: string; + /** @description Time the report was prepared. */ + "reporting-period"?: string; + /** @description Defines the output for the Report_Datasets being returned in a Dataset Report. Collection of datasets from the report. */ + "report-datasets"?: string; + }; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} + +export type $defs = Record; + +export type external = Record; + +export type operations = Record; diff --git a/core/actions/runs.ts b/core/actions/runs.ts index 300d35500..299146e7c 100644 --- a/core/actions/runs.ts +++ b/core/actions/runs.ts @@ -5,3 +5,4 @@ export { run as move } from "./move/run"; export { run as pushToV6 } from "./pushToV6/run"; export { run as http } from "./http/run"; export { run as googleDriveImport } from "./googleDriveImport/run"; +export { run as datacite } from "./datacite/run"; diff --git a/core/app/c/[communitySlug]/types/page.tsx b/core/app/c/[communitySlug]/types/page.tsx index 045df8448..de45a72b9 100644 --- a/core/app/c/[communitySlug]/types/page.tsx +++ b/core/app/c/[communitySlug]/types/page.tsx @@ -57,6 +57,7 @@ export default async function Page({ if (!types || !fields) { return null; } + return (
diff --git a/core/app/components/ActionUI/ActionConfigFormWrapper.tsx b/core/app/components/ActionUI/ActionConfigFormWrapper.tsx index 91edd2e27..d1061c914 100644 --- a/core/app/components/ActionUI/ActionConfigFormWrapper.tsx +++ b/core/app/components/ActionUI/ActionConfigFormWrapper.tsx @@ -27,7 +27,10 @@ export const ActionConfigFormWrapper = async ({ }) => { const { tokens = {} } = getActionByName(actionInstance.action); - const fieldPromise = getPubFields({ communityId: stage.communityId }).executeTakeFirstOrThrow(); + const fieldPromise = getPubFields({ + communityId: stage.communityId, + includeRelations: true, + }).executeTakeFirstOrThrow(); const resolvedFieldConfigPromise = resolveFieldConfig(actionInstance.action, "config", { stageId: stage.id, diff --git a/core/lib/env/env.mjs b/core/lib/env/env.mjs index eb0238d50..afdb1ec70 100644 --- a/core/lib/env/env.mjs +++ b/core/lib/env/env.mjs @@ -31,6 +31,9 @@ export const env = createEnv({ INBUCKET_URL: z.string().url().optional(), CI: z.string().or(z.boolean()).optional(), GCLOUD_KEY_FILE: z.string(), + DATACITE_API_URL: z.string().optional(), + DATACITE_REPOSITORY_ID: z.string().optional(), + DATACITE_PASSWORD: z.string().optional(), }, client: {}, experimental__runtimeEnv: { diff --git a/core/lib/serverActions.ts b/core/lib/serverActions.ts index 988b572f1..b33ce0564 100644 --- a/core/lib/serverActions.ts +++ b/core/lib/serverActions.ts @@ -58,4 +58,4 @@ export function useServerAction(action: (...args: T) => } export const didSucceed = (result: T): result is Exclude => - !isClientException(result); + typeof result !== "object" || (result !== null && !("error" in result)); diff --git a/core/prisma/exampleCommunitySeeds/arcadia.ts b/core/prisma/exampleCommunitySeeds/arcadia.ts index d714375b8..d4fdcb50b 100644 --- a/core/prisma/exampleCommunitySeeds/arcadia.ts +++ b/core/prisma/exampleCommunitySeeds/arcadia.ts @@ -21,7 +21,7 @@ export const seedArcadia = (communityId?: CommunitiesId) => { Abstract: faker.lorem.paragraphs(2), License: "CC-BY 4.0", PubContent: "Some content", - DOI: "https://doi.org/10.57844/arcadia-14b2-6f27", + URL: "https://www.pubpub.org", "Inline Citation Style": "Author Year", "Citation Style": "APA 7", }, @@ -100,7 +100,7 @@ export const seedArcadia = (communityId?: CommunitiesId) => { pubType: "ExternalBook", values: { Title: "A Great Book", - DOI: "https://doi.org/10.57844/arcadia-ad7f-7a6d", + DOI: "10.82234/arcadia-ad7f-7a6d", Year: "2022", }, }, @@ -112,7 +112,7 @@ export const seedArcadia = (communityId?: CommunitiesId) => { pubType: "ExternalJournalArticle", values: { Title: "A Great Journal Article", - DOI: "https://doi.org/10.57844/arcadia-ad7f-7a6d", + DOI: "10.82234/arcadia-ad7f-7a6d", Year: "2022", }, }, @@ -152,7 +152,9 @@ export const seedArcadia = (communityId?: CommunitiesId) => { Abstract: { schemaName: CoreSchemaType.String }, License: { schemaName: CoreSchemaType.String }, PubContent: { schemaName: CoreSchemaType.String }, - DOI: { schemaName: CoreSchemaType.URL }, + DOI: { schemaName: CoreSchemaType.String }, + "DOI Suffix": { schemaName: CoreSchemaType.String }, + URL: { schemaName: CoreSchemaType.URL }, "PDF Download Displayname": { schemaName: CoreSchemaType.String }, PDF: { schemaName: CoreSchemaType.FileUpload }, "Pub Image": { schemaName: CoreSchemaType.FileUpload }, @@ -313,6 +315,8 @@ export const seedArcadia = (communityId?: CommunitiesId) => { "Last Edited": { isTitle: false }, "Publication Date": { isTitle: false }, DOI: { isTitle: false }, + "DOI Suffix": { isTitle: false }, + URL: { isTitle: false }, PubContent: { isTitle: false }, License: { isTitle: false }, Description: { isTitle: false }, @@ -501,7 +505,7 @@ export const seedArcadia = (communityId?: CommunitiesId) => { stage: "Journals", values: { Title: "Arcadia Research", - DOI: "https://doi.org/10.57844/arcadia-ad7f-7a6d", + DOI: "10.82234/arcadia-ad7f-7a6d", ISSN: "2998-4084", Slug: "arcadia-research", }, @@ -516,7 +520,7 @@ export const seedArcadia = (communityId?: CommunitiesId) => { values: { Title: "Issue 1", ISSN: "2998-4084", - DOI: "https://doi.org/10.57844/arcadia-ad7f-7a6d", + DOI: "10.82234/arcadia-ad7f-7a6d", Description: "A cool description", }, relatedPubs: { @@ -539,7 +543,8 @@ export const seedArcadia = (communityId?: CommunitiesId) => { Abstract: `

The development of AAV capsids for therapeutic gene delivery has exploded in popularity over the past few years. However, humans aren’t the first or only species using viral capsids for gene delivery — wasps evolved this tactic over 100 million years ago. Parasitoid wasps that lay eggs inside arthropod hosts have co-opted ancient viruses for gene delivery to manipulate multiple aspects of the host’s biology, thereby increasing the probability of survival of the wasp larvae [1][2]

`, License: "CC-BY 4.0", PubContent: "Some content", - DOI: "https://doi.org/10.57844/arcadia-14b2-6f27", + DOI: "10.82234/arcadia-14b2-6f27", + URL: "https://www.pubpub.org", "Inline Citation Style": "Author Year", "Citation Style": "APA 7", @@ -654,7 +659,7 @@ export const seedArcadia = (communityId?: CommunitiesId) => { "ExternalBook", values: { Title: "A Great Book", - DOI: "https://doi.org/10.57844/arcadia-ad7f-7a6d", + DOI: "10.82234/arcadia-ad7f-7a6d", Year: "2022", }, }, @@ -667,7 +672,7 @@ export const seedArcadia = (communityId?: CommunitiesId) => { "ExternalJournalArticle", values: { Title: "A Great Journal Article", - DOI: "https://doi.org/10.57844/arcadia-ad7f-7a6d", + DOI: "10.82234/arcadia-ad7f-7a6d", Year: "2022", }, }, diff --git a/core/prisma/migrations/20241203193207_add_datacite_action/migration.sql b/core/prisma/migrations/20241203193207_add_datacite_action/migration.sql new file mode 100644 index 000000000..81cd85b01 --- /dev/null +++ b/core/prisma/migrations/20241203193207_add_datacite_action/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "Action" ADD VALUE 'datacite'; diff --git a/core/prisma/schema/schema.dbml b/core/prisma/schema/schema.dbml index c33a940a8..072fbc68d 100644 --- a/core/prisma/schema/schema.dbml +++ b/core/prisma/schema/schema.dbml @@ -532,6 +532,7 @@ Enum Action { http move googleDriveImport + datacite } Enum ActionRunStatus { diff --git a/core/prisma/schema/schema.prisma b/core/prisma/schema/schema.prisma index 5570ed9fe..9473e0ba4 100644 --- a/core/prisma/schema/schema.prisma +++ b/core/prisma/schema/schema.prisma @@ -458,6 +458,7 @@ enum Action { http move googleDriveImport + datacite } model ActionRun { diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 7a5c1708b..dd2792406 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -1,145 +1,145 @@ services: - core: - build: - context: . - args: - - PACKAGE=core - container_name: core - env_file: ./.env.docker-compose - environment: - #s non secret - - ASSETS_BUCKET_NAME=assets.byron.pubpub.org - - MAILGUN_SMTP_HOST=smtp.mailgun.org - - MAILGUN_SMTP_PORT=465 - - MAILGUN_SMTP_USERNAME=omitted - - OTEL_SERVICE_NAME=core.core - - PGDATABASE=postgres - - PGHOST=host.docker.internal - - PGPORT=54322 - - PGUSER=postgres - - PGPASSWORD=postgres - - PUBPUB_URL=http://localhost:8080 - networks: - - app-network - ports: - - "30000:3000" + core: + build: + context: . + args: + - PACKAGE=core + container_name: core + env_file: ./.env.docker-compose + environment: + #s non secret + - ASSETS_BUCKET_NAME=assets.byron.pubpub.org + - MAILGUN_SMTP_HOST=smtp.mailgun.org + - MAILGUN_SMTP_PORT=465 + - MAILGUN_SMTP_USERNAME=omitted + - OTEL_SERVICE_NAME=core.core + - PGDATABASE=postgres + - PGHOST=host.docker.internal + - PGPORT=54322 + - PGUSER=postgres + - PGPASSWORD=postgres + - PUBPUB_URL=http://localhost:8080 + networks: + - app-network + ports: + - "30000:3000" - core-nginx: - build: ./infrastructure/nginx - container_name: core-nginx - environment: - - NGINX_LISTEN_PORT=8080 - - NGINX_PREFIX=/ - - NGINX_UPSTREAM_HOST=core - - NGINX_UPSTREAM_PORT=3000 - - OTEL_SERVICE_NAME=core.nginx - depends_on: - - core - networks: - - app-network - ports: - - "3000:8080" + core-nginx: + build: ./infrastructure/nginx + container_name: core-nginx + environment: + - NGINX_LISTEN_PORT=8080 + - NGINX_PREFIX=/ + - NGINX_UPSTREAM_HOST=core + - NGINX_UPSTREAM_PORT=3000 + - OTEL_SERVICE_NAME=core.nginx + depends_on: + - core + networks: + - app-network + ports: + - "3000:8080" - jobs: - build: - context: . - args: - - PACKAGE=jobs - container_name: jobs - env_file: ./.env.docker-compose - environment: - - OTEL_SERVICE_NAME=jobs.jobs - - PGDATABASE=postgres - - PGHOST=host.docker.internal - - PGPORT=54322 - - PGUSER=postgres - - PGPASSWORD=postgres - - PUBPUB_URL=http://localhost:8080 - networks: - - app-network + jobs: + build: + context: . + args: + - PACKAGE=jobs + container_name: jobs + env_file: ./.env.docker-compose + environment: + - OTEL_SERVICE_NAME=jobs.jobs + - PGDATABASE=postgres + - PGHOST=host.docker.internal + - PGPORT=54322 + - PGUSER=postgres + - PGPASSWORD=postgres + - PUBPUB_URL=http://localhost:8080 + networks: + - app-network - # jobs-nginx: - # No Nginx for jobs, because it does not take requests + # jobs-nginx: + # No Nginx for jobs, because it does not take requests - integration-evaluations: - build: - context: . - args: - - PACKAGE=integration-evaluations - container_name: integration-evaluations - env_file: ./.env.docker-compose - environment: - - SENTRY_AUTH_TOKEN=omitted + integration-evaluations: + build: + context: . + args: + - PACKAGE=integration-evaluations + container_name: integration-evaluations + env_file: ./.env.docker-compose + environment: + - SENTRY_AUTH_TOKEN=omitted - - OTEL_SERVICE_NAME=integration-evaluations.integration-evaluations - - PUBPUB_URL=http://localhost:8080 - depends_on: - - core - networks: - - app-network - ports: - - "30001:3000" + - OTEL_SERVICE_NAME=integration-evaluations.integration-evaluations + - PUBPUB_URL=http://localhost:8080 + depends_on: + - core + networks: + - app-network + ports: + - "30001:3000" - integration-evaluations-nginx: - build: ./infrastructure/nginx - container_name: integration-evaluations-nginx - environment: - - NGINX_LISTEN_PORT=8080 - - NGINX_PREFIX=/ - - NGINX_UPSTREAM_HOST=integration-evaluations - - NGINX_UPSTREAM_PORT=3000 - - OTEL_SERVICE_NAME=integration-evaluations.nginx - depends_on: - - integration-evaluations - networks: - - app-network - ports: - - "3001:8080" + integration-evaluations-nginx: + build: ./infrastructure/nginx + container_name: integration-evaluations-nginx + environment: + - NGINX_LISTEN_PORT=8080 + - NGINX_PREFIX=/ + - NGINX_UPSTREAM_HOST=integration-evaluations + - NGINX_UPSTREAM_PORT=3000 + - OTEL_SERVICE_NAME=integration-evaluations.nginx + depends_on: + - integration-evaluations + networks: + - app-network + ports: + - "3001:8080" - integration-submissions: - build: - context: . - args: - - PACKAGE=integration-submissions - container_name: integration-submissions - env_file: ./.env.docker-compose - environment: - - SENTRY_AUTH_TOKEN=omitted + integration-submissions: + build: + context: . + args: + - PACKAGE=integration-submissions + container_name: integration-submissions + env_file: ./.env.docker-compose + environment: + - SENTRY_AUTH_TOKEN=omitted - - OTEL_SERVICE_NAME=integration-submissions.integration-submissions - - PUBPUB_URL=http://localhost:8080 - depends_on: - - core - networks: - - app-network - ports: - - "30002:3000" + - OTEL_SERVICE_NAME=integration-submissions.integration-submissions + - PUBPUB_URL=http://localhost:8080 + depends_on: + - core + networks: + - app-network + ports: + - "30002:3000" - integration-submissions-nginx: - build: ./infrastructure/nginx - container_name: integration-submissions-nginx - environment: - - NGINX_LISTEN_PORT=8080 - - NGINX_PREFIX=/ - - NGINX_UPSTREAM_HOST=integration-submissions - - NGINX_UPSTREAM_PORT=3000 - - OTEL_SERVICE_NAME=integration-submissions.nginx - depends_on: - - integration-submissions - networks: - - app-network - ports: - - "3002:8080" + integration-submissions-nginx: + build: ./infrastructure/nginx + container_name: integration-submissions-nginx + environment: + - NGINX_LISTEN_PORT=8080 + - NGINX_PREFIX=/ + - NGINX_UPSTREAM_HOST=integration-submissions + - NGINX_UPSTREAM_PORT=3000 + - OTEL_SERVICE_NAME=integration-submissions.nginx + depends_on: + - integration-submissions + networks: + - app-network + ports: + - "3002:8080" - db: - extends: - file: docker-compose.base.yml - service: db - - inbucket: - extends: - file: docker-compose.base.yml - service: inbucket + db: + extends: + file: docker-compose.base.yml + service: db + + inbucket: + extends: + file: docker-compose.base.yml + service: inbucket networks: - app-network: + app-network: diff --git a/infrastructure/maskfile.md b/infrastructure/maskfile.md index 534cc6254..f408a0144 100644 --- a/infrastructure/maskfile.md +++ b/infrastructure/maskfile.md @@ -1,7 +1,7 @@ # Infrastructure operations for pubpub v7 This "Maskfile" is the code AND documentation for common operations -workflows in this `infrastructure` directory. The commands declared +workflows in this `infrastructure` directory. The commands declared here are automatically available as CLI commands when running [`mask`](https://github.com/jacobdeichert/mask) in this directory. @@ -10,7 +10,7 @@ To get started, install important command line tools: `brew bundle` Then you can call `mask --help` to see these commands in the -familiar command line help format. You can also modify the +familiar command line help format. You can also modify the invocations here when the required script changes, or copy & paste the command parts as needed. @@ -153,7 +153,7 @@ is not assumed all developers have access to this. To run these commands, set ( cd .. if [[ -z $image_tag_override ]]; then echo "Deploying HEAD ($(git rev-parse --dirty HEAD)) ... ensure this tag has been pushed!" - else + else echo "Deploying override ($image_tag_override) ... ensure this tag has been pushed!" fi @@ -203,7 +203,7 @@ is not assumed all developers have access to this. To run these commands, set ( cd .. if [[ -z $image_tag_override ]]; then echo "Deploying HEAD ($(git rev-parse --dirty HEAD)) ... ensure this tag has been pushed!" - else + else echo "Deploying override ($image_tag_override) ... ensure this tag has been pushed!" fi diff --git a/infrastructure/nginx/README.md b/infrastructure/nginx/README.md index 650f83e5a..5c92baab0 100644 --- a/infrastructure/nginx/README.md +++ b/infrastructure/nginx/README.md @@ -5,6 +5,7 @@ all traffic to another host (typically 127.0.0.1:). In ECS, all co in the same task are hosted on the same network interface and therefore have the same IP. The specific reason this container is needed in Pubpub-v7 is that: + 1. we have one DNS name that serves the whole application, which is backed by multiple ECS containers. 2. these containers are routed to based on path prefixes. 3. the container code, served by Next.js, is not itself aware of these path prefixes, so expect requests at `/`. diff --git a/infrastructure/terraform/README.md b/infrastructure/terraform/README.md index 4eee94aab..c0f1bea94 100644 --- a/infrastructure/terraform/README.md +++ b/infrastructure/terraform/README.md @@ -24,7 +24,7 @@ Declarative code changes are still managed imperatively with `terraform apply`, which can be made partially or fully automatic. In general, production changes are applied manually after we are satisfied with -preproduction, which may or may not be automatic. Developers should expect a flow like: +preproduction, which may or may not be automatic. Developers should expect a flow like: 1. make a change to a shared module code 2. make matching change to configuration in ALL environment directories, so they can be reviewed together @@ -36,17 +36,17 @@ Now there is no drift between code, staging, and production - we are converged. ## Rollbacks Generally, rollbacks are done in emergencies and are done first in prod. (If done first in staging, this is -really no different a process than a roll-forward). Rollbacks are the only situation in which we should expect +really no different a process than a roll-forward). Rollbacks are the only situation in which we should expect to deploy production from an off-main branch. Changes may be infrastructure or code. In the infrastructure workflow: -1. make changes to terraform code that seems to fix the issue and apply it to production -2. if it resolves the issue, figure out how it needs to be applied to pre-prod for consistency and open a PR -3. when this PR is merged, it deploys to pre-prod and we are converged. +1. make changes to terraform code that seems to fix the issue and apply it to production +2. if it resolves the issue, figure out how it needs to be applied to pre-prod for consistency and open a PR +3. when this PR is merged, it deploys to pre-prod and we are converged. In general, code rollbacks can be done without a re-build, by deploying an old SHA, but it is preferable if there is time, to do a revert & roll-forward flow, because some operations (primarily database migrations) operate on assumptions of monotonic time. Additionally -this flow makes it easier for rollbacks to include reverts of specific changes in the middle of the commit history +this flow makes it easier for rollbacks to include reverts of specific changes in the middle of the commit history without reverting everything more recent. ## Adding/updating variables and configuration @@ -63,7 +63,7 @@ common case will be to add an environment variable to a container so will use th 1. modify `modules/deployment/variables.tf` to add the variable declaration. (This step is not needed if your new env var can be computed based on changes to the upstream infrastructure, such as a database URL.) 1. modify each invocation in `environments/*/main.tf` to add this new variable. -Proceed as above. Note that changes to task definitions (which include container configs) are not actually applied until you then trigger a new `deploy` using `act`/`mask` or the Github console. +Proceed as above. Note that changes to task definitions (which include container configs) are not actually applied until you then trigger a new `deploy` using `act`/`mask` or the Github console. ## Adding secrets diff --git a/infrastructure/terraform/environments/blake/main.tf b/infrastructure/terraform/environments/blake/main.tf index fab03bc99..1a538ba26 100644 --- a/infrastructure/terraform/environments/blake/main.tf +++ b/infrastructure/terraform/environments/blake/main.tf @@ -55,6 +55,7 @@ locals { NEXT_PUBLIC_SUPABASE_PUBLIC_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRzbGVxanV2enVveWNwZW90ZHdzIiwicm9sZSI6ImFub24iLCJpYXQiOjE2ODIzNTE0MjEsImV4cCI6MTk5NzkyNzQyMX0.3HHC0f7zlFXP77N0U8cS3blr7n6hhjqdYI6_ciQJams" ASSETS_BUCKET_NAME = "assets.blake.pubpub.org" HOSTNAME = "0.0.0.0" + DATACITE_API_URL = "https://api.test.datacite.org" } @@ -80,4 +81,5 @@ module "deployment" { NEXT_PUBLIC_SUPABASE_PUBLIC_KEY = local.NEXT_PUBLIC_SUPABASE_PUBLIC_KEY ASSETS_BUCKET_NAME = local.ASSETS_BUCKET_NAME HOSTNAME = local.HOSTNAME + DATACITE_API_URL = local.DATACITE_API_URL } diff --git a/infrastructure/terraform/environments/cloudflare/README.md b/infrastructure/terraform/environments/cloudflare/README.md index be8b5aa62..08f6b9777 100644 --- a/infrastructure/terraform/environments/cloudflare/README.md +++ b/infrastructure/terraform/environments/cloudflare/README.md @@ -4,18 +4,17 @@ This module should generally be created by an admin, and assumees the following permissions which are sensitive: **Cloudflare read-write token** set at `CLOUDFLARE_API_TOKEN`. In general, this -secret can be used for very nefarious things and should be extra sensitively protected. +secret can be used for very nefarious things and should be extra sensitively protected. **AWS read-write permissions**: in `~/.aws/credentials`. see `../maskfile.md` for more info. -## Relationship to AWS environments +## Relationship to AWS environments AWS environments assume existence of the Route53 zone and DNS NS records that refer authority to that zone. If you are not using Cloudflare this module is not needed for those environments, -but in general to create a new env it is expected to augment this module with NS records referring +but in general to create a new env it is expected to augment this module with NS records referring to this route53 configuration for domains subordinate to that new AWS env. Therefore updates to this module, which should happen very infrequently, should be applied before you attempt to create the new AWS-ECS environment, otherwise that will fail due to the AWS Certificate Manager being unsuccessful in validating your ownership of the DNS. - diff --git a/infrastructure/terraform/environments/global_aws/README.md b/infrastructure/terraform/environments/global_aws/README.md index 91f70aad9..7b15bfdb8 100644 --- a/infrastructure/terraform/environments/global_aws/README.md +++ b/infrastructure/terraform/environments/global_aws/README.md @@ -15,4 +15,3 @@ and not applied or updated by a machine user. 1. destroy local copies of the state file This bucket name can now be in your s3.tfbackend files everywhere. - diff --git a/infrastructure/terraform/environments/stevie/main.tf b/infrastructure/terraform/environments/stevie/main.tf index 8640b77ed..1333220b7 100644 --- a/infrastructure/terraform/environments/stevie/main.tf +++ b/infrastructure/terraform/environments/stevie/main.tf @@ -55,6 +55,7 @@ locals { NEXT_PUBLIC_SUPABASE_PUBLIC_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRzbGVxanV2enVveWNwZW90ZHdzIiwicm9sZSI6ImFub24iLCJpYXQiOjE2ODIzNTE0MjEsImV4cCI6MTk5NzkyNzQyMX0.3HHC0f7zlFXP77N0U8cS3blr7n6hhjqdYI6_ciQJams" ASSETS_BUCKET_NAME = "assets.app.pubpub.org" HOSTNAME = "0.0.0.0" + DATACITE_API_URL = "https://api.test.datacite.org" } @@ -80,4 +81,5 @@ module "deployment" { NEXT_PUBLIC_SUPABASE_PUBLIC_KEY = local.NEXT_PUBLIC_SUPABASE_PUBLIC_KEY ASSETS_BUCKET_NAME = local.ASSETS_BUCKET_NAME HOSTNAME = local.HOSTNAME + DATACITE_API_URL = local.DATACITE_API_URL } diff --git a/infrastructure/terraform/modules/core-services/README.md b/infrastructure/terraform/modules/core-services/README.md index 1620c6aa0..e72b591e5 100644 --- a/infrastructure/terraform/modules/core-services/README.md +++ b/infrastructure/terraform/modules/core-services/README.md @@ -1,6 +1,8 @@ # Setup + In a `main.tf` file for a workspace that needs a cluster, you can use this module like: + ``` module "cluster" { source = "../path/to/this/directory" @@ -14,6 +16,7 @@ module "cluster" { ``` then + ``` terraform init terraform apply @@ -22,6 +25,7 @@ terraform apply You will see these resources under `module.cluster.xyz`. ## Managing the ECS Task Definition + Working with ECS task definitions in Terraform is kind of awkward. @@ -57,14 +61,16 @@ of managing ECS with Terraform can be found in [this Terraform issue.](https://github.com/hashicorp/terraform-provider-aws/issues/632) ## Rotating the RDS Password + The RDS password is retrieved from AWS Secrets Manager but that password is managed manually, and rotating it requires downtime. To rotate it, you'll need to perform the following steps: -- Update the value of the Secrets Manager entry through the AWS console -- Update the value in the RDS instance through the AWS console. (At this point, the core container will stop being able to access the database.) -- Recreate the core container's service with `aws update-service cluster $CLUSTER_NAME --service $SERVICE_NAME --force-new-deployment` + +- Update the value of the Secrets Manager entry through the AWS console +- Update the value in the RDS instance through the AWS console. (At this point, the core container will stop being able to access the database.) +- Recreate the core container's service with `aws update-service cluster $CLUSTER_NAME --service $SERVICE_NAME --force-new-deployment` In the future the RDS should probably [manage its own password](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-secrets-manager.html) which will probably require changing the service's code diff --git a/infrastructure/terraform/modules/core-services/main.tf b/infrastructure/terraform/modules/core-services/main.tf index 3fdc8be31..a04b5005a 100644 --- a/infrastructure/terraform/modules/core-services/main.tf +++ b/infrastructure/terraform/modules/core-services/main.tf @@ -34,6 +34,14 @@ resource "aws_secretsmanager_secret" "gcloud_key_file" { name = "gcloud-key-file-${var.cluster_info.name}-${var.cluster_info.environment}" } +resource "aws_secretsmanager_secret" "datacite_repository_id" { + name = "datacite-repository-id-${var.cluster_info.name}-${var.cluster_info.environment}" +} + +resource "aws_secretsmanager_secret" "datacite_password" { + name = "datacite-password-${var.cluster_info.name}-${var.cluster_info.environment}" +} + # generate password and make it accessible through aws secrets manager resource "random_password" "rds_db_password" { length = 16 diff --git a/infrastructure/terraform/modules/core-services/outputs.tf b/infrastructure/terraform/modules/core-services/outputs.tf index e80e85d07..843dfea7d 100644 --- a/infrastructure/terraform/modules/core-services/outputs.tf +++ b/infrastructure/terraform/modules/core-services/outputs.tf @@ -16,6 +16,8 @@ output "secrets" { supabase_webhooks_api_key = aws_secretsmanager_secret.supabase_webhooks_api_key.id mailgun_smtp_password = aws_secretsmanager_secret.mailgun_smtp_password.id gcloud_key_file = aws_secretsmanager_secret.gcloud_key_file.id + datacite_repository_id = aws_secretsmanager_secret.datacite_repository_id.id + datacite_password = aws_secretsmanager_secret.datacite_password.id } } diff --git a/infrastructure/terraform/modules/deployment/main.tf b/infrastructure/terraform/modules/deployment/main.tf index 230ade1d0..26c048168 100644 --- a/infrastructure/terraform/modules/deployment/main.tf +++ b/infrastructure/terraform/modules/deployment/main.tf @@ -99,6 +99,7 @@ module "service_core" { { name = "SUPABASE_URL", value = var.NEXT_PUBLIC_SUPABASE_URL }, { name = "SUPABASE_PUBLIC_KEY", value = var.NEXT_PUBLIC_SUPABASE_PUBLIC_KEY }, { name = "HOSTNAME", value = var.HOSTNAME }, + { name = "DATACITE_API_URL", value = var.DATACITE_API_URL }, ] secrets = [ @@ -106,13 +107,14 @@ module "service_core" { { name = "API_KEY", valueFrom = module.core_dependency_services.secrets.api_key }, { name = "JWT_SECRET", valueFrom = module.core_dependency_services.secrets.jwt_secret }, { name = "ASSETS_UPLOAD_SECRET_KEY", valueFrom = module.core_dependency_services.secrets.asset_uploader_secret_key }, - { name = "SENTRY_AUTH_TOKEN", valueFrom = module.core_dependency_services.secrets.sentry_auth_token }, { name = "SUPABASE_WEBHOOKS_API_KEY", valueFrom = module.core_dependency_services.secrets.supabase_webhooks_api_key }, { name = "SUPABASE_SERVICE_ROLE_KEY", valueFrom = module.core_dependency_services.secrets.supabase_service_role_key }, { name = "HONEYCOMB_API_KEY", valueFrom = module.core_dependency_services.secrets.honeycomb_api_key }, { name = "MAILGUN_SMTP_PASSWORD", valueFrom = module.core_dependency_services.secrets.mailgun_smtp_password }, { name = "GCLOUD_KEY_FILE", valueFrom = module.core_dependency_services.secrets.gcloud_key_file }, + { name = "DATACITE_REPOSITORY_ID", valueFrom = module.core_dependency_services.secrets.datacite_repository_id }, + { name = "DATACITE_PASSWORD", valueFrom = module.core_dependency_services.secrets.datacite_password }, ] } } diff --git a/infrastructure/terraform/modules/deployment/variables.tf b/infrastructure/terraform/modules/deployment/variables.tf index 68b2f5e50..2075ead74 100644 --- a/infrastructure/terraform/modules/deployment/variables.tf +++ b/infrastructure/terraform/modules/deployment/variables.tf @@ -71,3 +71,8 @@ variable "ASSETS_BUCKET_NAME" { description = "Name of the S3 bucket to store assets" type = string } + +variable "DATACITE_API_URL" { + description = "DataCite API URL used by the DataCite action to deposit pubs and allocate DOIs" + type = string +} diff --git a/integrations/evaluations-proxy/package.json b/integrations/evaluations-proxy/package.json index 62febb7a7..d84e57499 100644 --- a/integrations/evaluations-proxy/package.json +++ b/integrations/evaluations-proxy/package.json @@ -1,5 +1,5 @@ { - "name": "evaluations-proxy", - "version": "0.0.0", - "private": true -} \ No newline at end of file + "name": "evaluations-proxy", + "version": "0.0.0", + "private": true +} diff --git a/integrations/submissions-proxy/package.json b/integrations/submissions-proxy/package.json index e286fc74c..f8dda3685 100644 --- a/integrations/submissions-proxy/package.json +++ b/integrations/submissions-proxy/package.json @@ -1,5 +1,5 @@ { - "name": "submissions-proxy", - "version": "0.0.0", - "private": true -} \ No newline at end of file + "name": "submissions-proxy", + "version": "0.0.0", + "private": true +} diff --git a/packages/db/src/public/Action.ts b/packages/db/src/public/Action.ts index 88392305b..abf120f27 100644 --- a/packages/db/src/public/Action.ts +++ b/packages/db/src/public/Action.ts @@ -12,6 +12,7 @@ export enum Action { http = "http", move = "move", googleDriveImport = "googleDriveImport", + datacite = "datacite", } /** Zod schema for Action */ diff --git a/packages/ui/package.json b/packages/ui/package.json index eff136f06..c6f7dc352 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -345,7 +345,7 @@ "db": "workspace:*", "lexical": "^0.15.0", "lucide-react": "^0.357.0", - "react-day-picker": "^8.10.0", + "react-day-picker": "^9.4.2", "schemas": "workspace:*", "tailwind-merge": "catalog:", "tailwindcss-animate": "^1.0.6", diff --git a/packages/ui/src/auto-form/fields/date.tsx b/packages/ui/src/auto-form/fields/date.tsx index 8bae8d31a..f0093084d 100644 --- a/packages/ui/src/auto-form/fields/date.tsx +++ b/packages/ui/src/auto-form/fields/date.tsx @@ -3,6 +3,12 @@ import * as React from "react"; import type { AutoFormInputComponentProps } from "../types"; import { DatePicker } from "../../date-picker"; import { FormControl, FormItem, FormMessage } from "../../form"; +import { + PubFieldSelect, + PubFieldSelectProvider, + PubFieldSelectToggleButton, + PubFieldSelectWrapper, +} from "../../pubFields/pubFieldSelect"; import AutoFormDescription from "../common/description"; import AutoFormLabel from "../common/label"; import AutoFormTooltip from "../common/tooltip"; @@ -14,17 +20,38 @@ export default function AutoFormDate({ field, fieldConfigItem, fieldProps, + zodItem, }: AutoFormInputComponentProps) { - return ( - - - {description && } - - - - + const { showLabel = true } = fieldProps; - - + return ( + +
+ + {showLabel && ( + <> + + + + + {description && } + + )} + {description && } + + + + + + + + + +
+
); } diff --git a/packages/ui/src/calendar.tsx b/packages/ui/src/calendar.tsx index 8c3f4ea32..26d1ae2d9 100644 --- a/packages/ui/src/calendar.tsx +++ b/packages/ui/src/calendar.tsx @@ -53,10 +53,6 @@ function Calendar({ className, classNames, showOutsideDays = true, ...props }: C day_hidden: "invisible", ...classNames, }} - components={{ - IconLeft: ({ ...props }) => , - IconRight: ({ ...props }) => , - }} {...props} /> ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a447a0d48..186897423 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1353,8 +1353,8 @@ importers: specifier: 'catalog:' version: 14.2.9(@babel/core@7.25.2)(@opentelemetry/api@1.9.0)(@playwright/test@1.48.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-day-picker: - specifier: ^8.10.0 - version: 8.10.1(date-fns@3.6.0)(react@18.3.1) + specifier: ^9.4.2 + version: 9.4.2(react@18.3.1) schemas: specifier: workspace:* version: link:../schemas @@ -2405,6 +2405,9 @@ packages: resolution: {integrity: sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw==} engines: {node: '>17.0.0'} + '@date-fns/tz@1.2.0': + resolution: {integrity: sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==} + '@dnd-kit/accessibility@3.1.0': resolution: {integrity: sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ==} peerDependencies: @@ -7696,6 +7699,9 @@ packages: date-fns@3.6.0: resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} + date-fns@4.1.0: + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + dateformat@4.6.3: resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} @@ -10790,11 +10796,11 @@ packages: react: ^16.8.6 react-dom: ^16.8.6 - react-day-picker@8.10.1: - resolution: {integrity: sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==} + react-day-picker@9.4.2: + resolution: {integrity: sha512-qKunVfJ+QWJqdHylZ9TsXSSJzzK9vDLsZ+c80/r+ZwOWqGW8mADwPy1iOBrNcZiAokQ4xrSsPLnWzTRHS4mSsQ==} + engines: {node: '>=18'} peerDependencies: - date-fns: ^2.28.0 || ^3.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react: '>=16.8.0' react-docgen-typescript@2.2.2: resolution: {integrity: sha512-tvg2ZtOpOi6QDwsb3GZhOjDkkX0h8Z2gipvTg6OVMUyoYoURhEiRNePT8NZItTVCDh39JJHnLdfCOkzoLbFnTg==} @@ -14045,6 +14051,8 @@ snapshots: '@dagrejs/graphlib@2.2.4': {} + '@date-fns/tz@1.2.0': {} + '@dnd-kit/accessibility@3.1.0(react@18.3.1)': dependencies: react: 18.3.1 @@ -21018,6 +21026,8 @@ snapshots: date-fns@3.6.0: {} + date-fns@4.1.0: {} + dateformat@4.6.3: {} debounce@2.0.0: {} @@ -24784,9 +24794,10 @@ snapshots: react: 18.3.1 react-dom: 18.3.0(react@18.3.1) - react-day-picker@8.10.1(date-fns@3.6.0)(react@18.3.1): + react-day-picker@9.4.2(react@18.3.1): dependencies: - date-fns: 3.6.0 + '@date-fns/tz': 1.2.0 + date-fns: 4.1.0 react: 18.3.1 react-docgen-typescript@2.2.2(typescript@5.6.2):