@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.
|
|
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.
|
|
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": "
|
|
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": "
|
|
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
|
+
})
|
package/src/libs/doi/metadata.ts
CHANGED
|
@@ -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.
|
|
44
|
-
?
|
|
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,
|