forked from OpenNeuroOrg/openneuro
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdescription.ts
More file actions
168 lines (155 loc) · 5.41 KB
/
Copy pathdescription.ts
File metadata and controls
168 lines (155 loc) · 5.41 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
/**
* Get description data from backend
*/
import config from "../config"
import request from "superagent"
import { getRedis } from "../libs/redis"
import { commitFiles } from "./dataset"
import { fileUrl } from "./files"
import { generateDataladCookie } from "../libs/authentication/jwt"
import { getDatasetWorker } from "../libs/datalad-service"
import CacheItem, { CacheType } from "../cache/item"
import { datasetOrSnapshot } from "../utils/datasetOrSnapshot"
/**
* Checks if all elements in an array are strings.
* @param arr The array to check.
* @returns True if all elements are strings, false otherwise.
*/
const isArrayOfStrings = (arr: unknown): arr is string[] => {
return Array.isArray(arr) && arr.every((item) => typeof item === "string")
}
/**
* Find dataset_description.json id and fetch description object
* @param {string} datasetId
* @returns {Promise<Record<string, unknown>>} Promise resolving to dataset_description.json contents or defaults
*/
export const getDescriptionObject = async (datasetId, revision) => {
const res = await fetch(
fileUrl(datasetId, "", "dataset_description.json", revision),
)
const contentType = res.headers.get("content-type")
if (res.status === 200 && contentType.includes("application/json")) {
return await res.json()
} else {
throw new Error(
`Backend request failed, dataset_description.json may not exist or may be non-JSON (type: ${contentType}, status: ${res.status})`,
)
}
}
export const descriptionCacheKey = (datasetId, revision) => {
return `openneuro:dataset_description.json:${datasetId}:${revision}`
}
export const repairDescriptionTypes = (description) => {
const newDescription = { ...description }
// Define fields that should be arrays of strings
const arrayStringFields = [
"Authors",
"ReferencesAndLinks",
"Funding",
"EthicsApprovals",
]
// Repair array types - ensure they are arrays of strings
for (const field of arrayStringFields) {
if (Object.hasOwn(description, field)) {
if (!isArrayOfStrings(description[field])) {
// If it's not an array of strings (or not an array at all), replace with an empty array
newDescription[field] = []
}
// If it is already a valid array of strings, no change is needed.
}
// If the field doesn't exist, we don't add it.
}
// Define fields that should be strings
const stringFields = [
"Name",
"DatasetDOI",
"Acknowledgements",
"HowToAcknowledge",
"DatasetType",
]
// Repair string types - ensure they are strings
for (const field of stringFields) {
if (Object.hasOwn(description, field)) {
if (typeof description[field] !== "string") {
// Attempt to stringify non-string types, default to empty string or specific default
if (field === "DatasetType") {
newDescription[field] = "raw" // Specific default for DatasetType
} else {
try {
// Use JSON.stringify for complex types, otherwise just convert
const stringified = typeof description[field] === "object"
? JSON.stringify(description[field])
: String(description[field])
newDescription[field] = stringified || ""
} catch (_err) {
newDescription[field] = "" // Fallback to empty string on error
}
}
}
// If it's already a string, no change needed.
}
// If the field doesn't exist, we don't add it (except Name)
}
// Ensure BIDSVersion is present if missing (common default)
if (!newDescription.BIDSVersion) {
newDescription.BIDSVersion = "1.11.0"
}
// Ensure Name is present if missing
if (!newDescription.Name) {
newDescription.Name = "Unnamed Dataset"
}
return newDescription
}
/**
* Return the last author in dataset_description as the senior author if available
*/
export const appendSeniorAuthor = (description) => {
try {
const SeniorAuthor = description?.Authors[description.Authors.length - 1]
return { ...description, SeniorAuthor }
} catch (_err) {
return description
}
}
/**
* Get a parsed dataset_description.json
* @param {object} obj dataset or snapshot object
*/
export const description = async (obj) => {
// Obtain datasetId from Dataset or Snapshot objects
const { datasetId, revision } = datasetOrSnapshot(obj)
// Default fallback if dataset_description.json is not valid or missing
const defaultDescription = {
Name: datasetId,
BIDSVersion: "1.8.0",
}
const cache = new CacheItem(getRedis(), CacheType.datasetDescription, [
datasetId,
revision.substring(0, 7),
])
try {
const datasetDescription = await cache.get(() => {
return getDescriptionObject(datasetId, revision).then(
(uncachedDescription) => ({ id: revision, ...uncachedDescription }),
)
})
return appendSeniorAuthor(repairDescriptionTypes(datasetDescription))
} catch (_err) {
return defaultDescription
}
}
export const setDescription = (datasetId, user, descriptionFieldUpdates) => {
const url = `${getDatasetWorker(datasetId)}/datasets/${datasetId}/description`
return request
.post(url)
.send({ description_fields: descriptionFieldUpdates })
.set("Accept", "application/json")
.set("Cookie", generateDataladCookie(config)(user))
.then((res) => {
const description = res.body
return commitFiles(datasetId, user).then((gitRef) => ({
id: gitRef,
...description,
}))
})
}