@openneuro/server 5.1.1 → 5.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openneuro/server",
3
- "version": "5.1.1",
3
+ "version": "5.1.3",
4
4
  "description": "Core service for the OpenNeuro platform.",
5
5
  "license": "MIT",
6
6
  "main": "src/server.js",
@@ -22,7 +22,7 @@
22
22
  "@elastic/elasticsearch": "8.13.1",
23
23
  "@graphql-tools/schema": "^10.0.0",
24
24
  "@keyv/redis": "^4.5.0",
25
- "@openneuro/search": "^5.1.1",
25
+ "@openneuro/search": "^5.1.3",
26
26
  "@pothos/core": "^4.12.0",
27
27
  "@pothos/plugin-directives": "^4.3.0",
28
28
  "@pothos/plugin-simple-objects": "^4.1.3",
@@ -72,7 +72,7 @@
72
72
  "ts-node": "10.9.2",
73
73
  "typescript": "5.6.3",
74
74
  "underscore": "^1.8.3",
75
- "uuid": "14.0.0",
75
+ "uuid": "11.1.1",
76
76
  "xmldoc": "^1.1.0"
77
77
  },
78
78
  "devDependencies": {
@@ -95,5 +95,5 @@
95
95
  "publishConfig": {
96
96
  "access": "public"
97
97
  },
98
- "gitHead": "035b0b4544d4b287506ea00aff3b424ed816611a"
98
+ "gitHead": "b1059b286940adb3b60150217e691fe75a895d94"
99
99
  }
@@ -54,7 +54,6 @@ export const snapshot = (obj, { datasetId, tag }, context: GraphQLContext) => {
54
54
  () => {
55
55
  return datalad.getSnapshot(datasetId, tag).then((snapshot) => ({
56
56
  ...snapshot,
57
- created: snapshot.created ? new Date(snapshot.created) : undefined,
58
57
  datasetId,
59
58
  dataset: () => dataset(snapshot, { id: datasetId }, context),
60
59
  description: () => description(snapshot),
@@ -0,0 +1,149 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest"
2
+ import { assembleMetadata } from "../metadata.js"
3
+
4
+ vi.mock("ioredis")
5
+ vi.mock("../../../config", () => ({
6
+ default: { url: "https://openneuro.org" },
7
+ }))
8
+ vi.mock("../index", () => ({
9
+ createDOI: (datasetId: string, snapshotId: string) =>
10
+ `10.18112/openneuro.${datasetId}.v${snapshotId}`,
11
+ }))
12
+ vi.mock("../../../utils/datacite-utils")
13
+ vi.mock("../../../datalad/description")
14
+ vi.mock("../../../graphql/resolvers/summary")
15
+
16
+ import { getDataciteYml } from "../../../utils/datacite-utils.js"
17
+ import { description } from "../../../datalad/description.js"
18
+
19
+ const mockGetDataciteYml = vi.mocked(getDataciteYml)
20
+ const mockDescription = vi.mocked(description)
21
+
22
+ const baseYml = {
23
+ data: {
24
+ attributes: {
25
+ creators: [{ name: "Doe, Jane", nameType: "Personal" }],
26
+ titles: [{ title: "Test Dataset" }],
27
+ },
28
+ },
29
+ }
30
+
31
+ const baseDesc = {
32
+ Name: "Test Dataset",
33
+ Authors: [],
34
+ }
35
+
36
+ beforeEach(() => {
37
+ vi.resetAllMocks()
38
+ mockDescription.mockResolvedValue(baseDesc as never)
39
+ })
40
+
41
+ describe("assembleMetadata", () => {
42
+ describe("contributor name validation", () => {
43
+ it("keeps a contributor whose name has one comma (surname, given)", async () => {
44
+ mockGetDataciteYml.mockResolvedValue({
45
+ ...baseYml,
46
+ data: {
47
+ attributes: {
48
+ ...baseYml.data.attributes,
49
+ contributors: [
50
+ { name: "Doe, Jane", contributorType: "DataCollector" },
51
+ ],
52
+ },
53
+ },
54
+ } as never)
55
+
56
+ const result = await assembleMetadata("ds000001", "1.0.0")
57
+ expect(result.contributors).toHaveLength(1)
58
+ expect(result.contributors![0].name).toBe("Doe, Jane")
59
+ })
60
+
61
+ it("keeps a contributor whose name has no comma", async () => {
62
+ mockGetDataciteYml.mockResolvedValue({
63
+ ...baseYml,
64
+ data: {
65
+ attributes: {
66
+ ...baseYml.data.attributes,
67
+ contributors: [
68
+ { name: "OpenNeuro Team", contributorType: "HostingInstitution" },
69
+ ],
70
+ },
71
+ },
72
+ } as never)
73
+
74
+ const result = await assembleMetadata("ds000001", "1.0.0")
75
+ expect(result.contributors).toHaveLength(1)
76
+ expect(result.contributors![0].name).toBe("OpenNeuro Team")
77
+ })
78
+
79
+ it("strips a contributor whose name contains more than one comma", async () => {
80
+ mockGetDataciteYml.mockResolvedValue({
81
+ ...baseYml,
82
+ data: {
83
+ attributes: {
84
+ ...baseYml.data.attributes,
85
+ contributors: [
86
+ {
87
+ name: "Doe, Jane, Smith, John",
88
+ contributorType: "DataCollector",
89
+ },
90
+ ],
91
+ },
92
+ },
93
+ } as never)
94
+
95
+ const result = await assembleMetadata("ds000001", "1.0.0")
96
+ expect(result.contributors).toBeUndefined()
97
+ })
98
+
99
+ it("strips only the invalid contributor and keeps the valid ones", async () => {
100
+ mockGetDataciteYml.mockResolvedValue({
101
+ ...baseYml,
102
+ data: {
103
+ attributes: {
104
+ ...baseYml.data.attributes,
105
+ contributors: [
106
+ { name: "Doe, Jane", contributorType: "DataCollector" },
107
+ {
108
+ name: "Smith, John, Doe, Jane",
109
+ contributorType: "DataCollector",
110
+ },
111
+ { name: "Williams, Bob", contributorType: "DataManager" },
112
+ ],
113
+ },
114
+ },
115
+ } as never)
116
+
117
+ const result = await assembleMetadata("ds000001", "1.0.0")
118
+ expect(result.contributors).toHaveLength(2)
119
+ expect(result.contributors!.map((c) => c.name)).toEqual([
120
+ "Doe, Jane",
121
+ "Williams, Bob",
122
+ ])
123
+ })
124
+ })
125
+
126
+ describe("contributor field stripping", () => {
127
+ it("removes the order field from contributors", async () => {
128
+ mockGetDataciteYml.mockResolvedValue({
129
+ ...baseYml,
130
+ data: {
131
+ attributes: {
132
+ ...baseYml.data.attributes,
133
+ contributors: [
134
+ {
135
+ name: "Doe, Jane",
136
+ contributorType: "DataCollector",
137
+ order: 1,
138
+ },
139
+ ],
140
+ },
141
+ },
142
+ } as never)
143
+
144
+ const result = await assembleMetadata("ds000001", "1.0.0")
145
+ expect(result.contributors).toHaveLength(1)
146
+ expect(result.contributors![0]).not.toHaveProperty("order")
147
+ })
148
+ })
149
+ })
@@ -37,21 +37,33 @@ export async function assembleMetadata(
37
37
  let contributors: DataCite["contributors"]
38
38
  let resourceType: string | undefined
39
39
 
40
+ const desc = await description({
41
+ id: datasetId,
42
+ revision: revision || "HEAD",
43
+ })
44
+
40
45
  if (hasDataciteCreators) {
41
- // Use datacite.yml metadata
42
46
  creators = ymlAttrs.creators
43
- titles = ymlAttrs.descriptions?.length
44
- ? [{ title: ymlAttrs.descriptions[0].description }]
45
- : []
47
+ titles = ymlAttrs.titles?.length
48
+ ? ymlAttrs.titles
49
+ : [{ title: desc.Name || datasetId }]
46
50
  descriptions = ymlAttrs.descriptions
51
+ // Strip empty givenName/familyName and the internal `order` field.
52
+ // Also filter out contributors whose name contains more than one comma,
53
+ // which indicates multiple names were concatenated into a single string.
47
54
  contributors = ymlAttrs.contributors
55
+ ?.filter((c) => (c.name?.split(",").length ?? 1) <= 2)
56
+ .map((c) => {
57
+ const { givenName, familyName, order: _order, ...rest } = c
58
+ return {
59
+ ...rest,
60
+ ...(givenName?.trim() ? { givenName } : {}),
61
+ ...(familyName?.trim() ? { familyName } : {}),
62
+ }
63
+ })
48
64
  resourceType = ymlAttrs.types?.resourceType
49
65
  } else {
50
66
  // Fall back to BIDS dataset_description.json
51
- const desc = await description({
52
- id: datasetId,
53
- revision: revision || "HEAD",
54
- })
55
67
  creators = (desc.Authors || [])
56
68
  .filter((author: string) => author)
57
69
  .map((author: string) => ({
@@ -66,15 +78,6 @@ export async function assembleMetadata(
66
78
  resourceType = await getPrimaryModality(datasetId)
67
79
  }
68
80
 
69
- // If datacite.yml had titles via a different path, use them
70
- if (hasDataciteCreators && titles.length === 0) {
71
- const desc = await description({
72
- id: datasetId,
73
- revision: revision || "HEAD",
74
- })
75
- titles = [{ title: desc.Name || datasetId }]
76
- }
77
-
78
81
  const attributes: DataCite = {
79
82
  doi,
80
83
  url,
@@ -70,7 +70,7 @@ export interface RawDataciteYml {
70
70
  attributes: Partial<
71
71
  Pick<
72
72
  DataCite,
73
- "contributors" | "creators" | "types" | "descriptions"
73
+ "contributors" | "creators" | "types" | "descriptions" | "titles"
74
74
  >
75
75
  >
76
76
  }