@openneuro/server 4.38.3 → 4.39.0-alpha.0
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/Dockerfile +1 -1
- package/package.json +6 -5
- package/src/cache/types.ts +1 -0
- package/src/datalad/__tests__/contributors.spec.ts +177 -0
- package/src/datalad/contributors.ts +153 -0
- package/src/datalad/dataset.ts +1 -1
- package/src/graphql/resolvers/__tests__/importRemoteDataset.spec.ts +2 -6
- package/src/graphql/resolvers/__tests__/user.spec.ts +18 -3
- package/src/graphql/resolvers/cache.ts +13 -9
- package/src/graphql/resolvers/datasetEvents.ts +470 -35
- package/src/graphql/resolvers/draft.ts +2 -0
- package/src/graphql/resolvers/mutation.ts +15 -1
- package/src/graphql/resolvers/snapshots.ts +10 -0
- package/src/graphql/resolvers/user.ts +182 -31
- package/src/graphql/schema.ts +98 -5
- package/src/libs/events.ts +5 -0
- package/src/models/datasetEvents.ts +126 -22
- package/src/models/user.ts +8 -0
- package/src/models/userNotificationStatus.ts +37 -0
- package/src/types/datacite.ts +97 -0
- package/src/utils/datacite-mapper.ts +21 -0
- package/src/utils/datacite-utils.ts +256 -0
- package/src/utils/orcid-utils.ts +17 -0
|
@@ -1,27 +1,196 @@
|
|
|
1
1
|
import DatasetEvent from "../../models/datasetEvents"
|
|
2
|
+
import User from "../../models/user"
|
|
3
|
+
import type { UserDocument } from "../../models/user"
|
|
4
|
+
import { checkDatasetAdmin } from "../permissions"
|
|
5
|
+
import type {
|
|
6
|
+
DatasetEventContributorCitation,
|
|
7
|
+
DatasetEventContributorCitationResponse,
|
|
8
|
+
DatasetEventContributorRequest,
|
|
9
|
+
DatasetEventContributorRequestResponse,
|
|
10
|
+
DatasetEventDocument,
|
|
11
|
+
} from "../../models/datasetEvents"
|
|
12
|
+
import { UserNotificationStatus } from "../../models/userNotificationStatus"
|
|
13
|
+
import type { UserNotificationStatusDocument } from "../../models/userNotificationStatus"
|
|
14
|
+
import {
|
|
15
|
+
getDataciteYml,
|
|
16
|
+
updateContributorsUtil,
|
|
17
|
+
} from "../../utils/datacite-utils"
|
|
18
|
+
import type { Contributor } from "../../types/datacite"
|
|
19
|
+
|
|
20
|
+
/** Helper type guards */
|
|
21
|
+
function isContributorRequest(
|
|
22
|
+
event: DatasetEventDocument,
|
|
23
|
+
): event is DatasetEventDocument & { event: DatasetEventContributorRequest } {
|
|
24
|
+
return event.event.type === "contributorRequest"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isContributorCitation(
|
|
28
|
+
event: DatasetEventDocument,
|
|
29
|
+
): event is DatasetEventDocument & { event: DatasetEventContributorCitation } {
|
|
30
|
+
return event.event.type === "contributorCitation"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isContributorRequestResponse(
|
|
34
|
+
event: DatasetEventDocument,
|
|
35
|
+
): event is DatasetEventDocument & {
|
|
36
|
+
event: DatasetEventContributorRequestResponse
|
|
37
|
+
} {
|
|
38
|
+
return event.event.type === "contributorRequestResponse"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isContributorCitationResponse(
|
|
42
|
+
event: DatasetEventDocument,
|
|
43
|
+
): event is DatasetEventDocument & {
|
|
44
|
+
event: DatasetEventContributorCitationResponse
|
|
45
|
+
} {
|
|
46
|
+
return event.event.type === "contributorCitationResponse"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Enriched type for GraphQL */
|
|
50
|
+
export type EnrichedDatasetEvent =
|
|
51
|
+
& Omit<DatasetEventDocument, "notificationStatus">
|
|
52
|
+
& {
|
|
53
|
+
hasBeenRespondedTo?: boolean
|
|
54
|
+
responseStatus?: "pending" | "accepted" | "denied" | null
|
|
55
|
+
notificationStatus?: UserNotificationStatusDocument
|
|
56
|
+
}
|
|
2
57
|
|
|
3
58
|
/**
|
|
4
59
|
* Get all events for a dataset
|
|
5
60
|
*/
|
|
6
|
-
export function datasetEvents(
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
61
|
+
export async function datasetEvents(
|
|
62
|
+
obj,
|
|
63
|
+
_,
|
|
64
|
+
{ userInfo, user },
|
|
65
|
+
): Promise<EnrichedDatasetEvent[]> {
|
|
66
|
+
const allEvents: DatasetEventDocument[] = await DatasetEvent.find({
|
|
67
|
+
datasetId: obj.id,
|
|
68
|
+
})
|
|
69
|
+
.sort({ timestamp: -1 })
|
|
70
|
+
.populate("user")
|
|
71
|
+
.populate({ path: "notificationStatus", match: { userId: user } })
|
|
72
|
+
|
|
73
|
+
const enriched: EnrichedDatasetEvent[] = allEvents.map((e) => {
|
|
74
|
+
const ev = e.toObject() as EnrichedDatasetEvent
|
|
75
|
+
|
|
76
|
+
if (!ev.notificationStatus || typeof ev.notificationStatus === "string") {
|
|
77
|
+
ev.notificationStatus = new UserNotificationStatus({
|
|
78
|
+
userId: user,
|
|
79
|
+
datasetEventId: e.id,
|
|
80
|
+
status: "UNREAD",
|
|
81
|
+
}) as UserNotificationStatusDocument
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if ("resolutionStatus" in e.event) {
|
|
85
|
+
ev.responseStatus = e.event.resolutionStatus as
|
|
86
|
+
| "pending"
|
|
87
|
+
| "accepted"
|
|
88
|
+
| "denied"
|
|
89
|
+
ev.hasBeenRespondedTo = ev.responseStatus !== null &&
|
|
90
|
+
ev.responseStatus !== "pending"
|
|
91
|
+
} else {
|
|
92
|
+
ev.responseStatus = null
|
|
93
|
+
ev.hasBeenRespondedTo = false
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return ev
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
return userInfo?.admin ? enriched : enriched.filter(
|
|
100
|
+
(ev) =>
|
|
101
|
+
!(ev.event.type === "note" && ev.event.admin) &&
|
|
102
|
+
ev.event.type !== "permissionChange",
|
|
103
|
+
)
|
|
23
104
|
}
|
|
24
105
|
|
|
106
|
+
// --- Field-level resolvers ---
|
|
107
|
+
export const DatasetEventResolvers = {
|
|
108
|
+
hasBeenRespondedTo: (ev: EnrichedDatasetEvent) =>
|
|
109
|
+
ev.hasBeenRespondedTo ?? false,
|
|
110
|
+
responseStatus: (ev: EnrichedDatasetEvent) => ev.responseStatus ?? null,
|
|
111
|
+
notificationStatus: (ev: EnrichedDatasetEvent) =>
|
|
112
|
+
ev.notificationStatus?.status ?? "UNREAD",
|
|
113
|
+
requestId: (ev: EnrichedDatasetEvent) =>
|
|
114
|
+
isContributorRequest(ev) || isContributorRequestResponse(ev)
|
|
115
|
+
? ev.event.requestId
|
|
116
|
+
: null,
|
|
117
|
+
target: async (ev: EnrichedDatasetEvent): Promise<UserDocument | null> => {
|
|
118
|
+
const targetUserId = isContributorRequestResponse(ev) ||
|
|
119
|
+
isContributorCitation(ev) ||
|
|
120
|
+
isContributorCitationResponse(ev)
|
|
121
|
+
? ev.event.targetUserId
|
|
122
|
+
: undefined
|
|
123
|
+
|
|
124
|
+
if (!targetUserId) return null
|
|
125
|
+
// Use findOne({ id }) for UUID strings
|
|
126
|
+
return User.findOne({ id: targetUserId })
|
|
127
|
+
},
|
|
128
|
+
user: async (ev: EnrichedDatasetEvent): Promise<UserDocument | null> =>
|
|
129
|
+
ev.userId ? User.findOne({ id: ev.userId }) : null,
|
|
130
|
+
contributorData: (ev: EnrichedDatasetEvent) => {
|
|
131
|
+
let data: DatasetEventContributorCitation["contributorData"] | undefined
|
|
132
|
+
|
|
133
|
+
if (
|
|
134
|
+
(isContributorCitation(ev) || isContributorCitationResponse(ev)) &&
|
|
135
|
+
ev.event.contributorData
|
|
136
|
+
) {
|
|
137
|
+
data = ev.event.contributorData
|
|
138
|
+
} else if (
|
|
139
|
+
(isContributorRequest(ev) || isContributorRequestResponse(ev)) &&
|
|
140
|
+
ev.event.contributorData
|
|
141
|
+
) {
|
|
142
|
+
data = ev.event.contributorData
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
...data,
|
|
147
|
+
contributorType: data?.contributorType || "Researcher",
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Create a 'contributor request' event
|
|
153
|
+
*/
|
|
154
|
+
export async function createContributorRequestEvent(
|
|
155
|
+
obj,
|
|
156
|
+
{ datasetId },
|
|
157
|
+
{ user },
|
|
158
|
+
) {
|
|
159
|
+
if (!user) {
|
|
160
|
+
throw new Error("Authentication required to request contributor status.")
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Fetch user info for contributorData
|
|
164
|
+
const targetUser = await User.findOne({ id: user })
|
|
165
|
+
if (!targetUser) throw new Error("User not found.")
|
|
166
|
+
|
|
167
|
+
const contributorData = {
|
|
168
|
+
userId: targetUser.id,
|
|
169
|
+
name: targetUser.name || "Unknown Contributor",
|
|
170
|
+
givenName: targetUser.givenName || "",
|
|
171
|
+
familyName: targetUser.familyName || "",
|
|
172
|
+
orcid: targetUser.orcid,
|
|
173
|
+
contributorType: "Researcher",
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const event = new DatasetEvent({
|
|
177
|
+
datasetId,
|
|
178
|
+
userId: user,
|
|
179
|
+
event: {
|
|
180
|
+
type: "contributorRequest",
|
|
181
|
+
datasetId,
|
|
182
|
+
resolutionStatus: "pending",
|
|
183
|
+
contributorData,
|
|
184
|
+
},
|
|
185
|
+
success: true,
|
|
186
|
+
note: "User requested contributor status for this dataset.",
|
|
187
|
+
})
|
|
188
|
+
;(event.event as DatasetEventContributorRequest).requestId = event.id
|
|
189
|
+
|
|
190
|
+
await event.save()
|
|
191
|
+
await event.populate("user")
|
|
192
|
+
return event
|
|
193
|
+
}
|
|
25
194
|
/**
|
|
26
195
|
* Create or update an admin note event
|
|
27
196
|
*/
|
|
@@ -30,30 +199,296 @@ export async function saveAdminNote(
|
|
|
30
199
|
{ id, datasetId, note },
|
|
31
200
|
{ user, userInfo },
|
|
32
201
|
) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
throw new Error("Not authorized")
|
|
36
|
-
}
|
|
202
|
+
if (!userInfo?.admin) throw new Error("Not authorized")
|
|
203
|
+
|
|
37
204
|
if (id) {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
205
|
+
const updatedEvent = await DatasetEvent.findOneAndUpdate(
|
|
206
|
+
{ id, datasetId },
|
|
207
|
+
{ note },
|
|
208
|
+
{ new: true },
|
|
209
|
+
)
|
|
210
|
+
if (!updatedEvent) {
|
|
211
|
+
throw new Error(`Event with ID ${id} not found for dataset ${datasetId}.`)
|
|
212
|
+
}
|
|
213
|
+
await updatedEvent.populate("user")
|
|
214
|
+
return updatedEvent
|
|
43
215
|
} else {
|
|
44
|
-
const
|
|
45
|
-
id,
|
|
216
|
+
const newEvent = new DatasetEvent({
|
|
46
217
|
datasetId,
|
|
47
218
|
userId: user,
|
|
48
|
-
event: {
|
|
49
|
-
type: "note",
|
|
50
|
-
admin: true,
|
|
51
|
-
},
|
|
219
|
+
event: { type: "note", admin: true, datasetId },
|
|
52
220
|
success: true,
|
|
53
221
|
note,
|
|
54
222
|
})
|
|
55
|
-
await
|
|
56
|
-
await
|
|
57
|
-
return
|
|
223
|
+
await newEvent.save()
|
|
224
|
+
await newEvent.populate("user")
|
|
225
|
+
return newEvent
|
|
58
226
|
}
|
|
59
227
|
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Process a contributor request (accept or deny) and update Datacite YAML if accepted
|
|
231
|
+
*/
|
|
232
|
+
export async function processContributorRequest(
|
|
233
|
+
obj: unknown,
|
|
234
|
+
{
|
|
235
|
+
datasetId,
|
|
236
|
+
requestId,
|
|
237
|
+
targetUserId,
|
|
238
|
+
resolutionStatus,
|
|
239
|
+
reason,
|
|
240
|
+
}: {
|
|
241
|
+
datasetId: string
|
|
242
|
+
requestId: string
|
|
243
|
+
targetUserId: string
|
|
244
|
+
resolutionStatus: "accepted" | "denied"
|
|
245
|
+
reason?: string
|
|
246
|
+
},
|
|
247
|
+
{ user: currentUserId, userInfo }: {
|
|
248
|
+
user: string
|
|
249
|
+
userInfo: { admin: boolean }
|
|
250
|
+
},
|
|
251
|
+
) {
|
|
252
|
+
if (!currentUserId) {
|
|
253
|
+
throw new Error("Authentication required to process contributor requests.")
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
await checkDatasetAdmin(datasetId, currentUserId, userInfo)
|
|
257
|
+
|
|
258
|
+
const originalRequestEvent = await DatasetEvent.findOne({
|
|
259
|
+
"event.type": "contributorRequest",
|
|
260
|
+
"event.requestId": requestId,
|
|
261
|
+
}).populate("user")
|
|
262
|
+
|
|
263
|
+
if (!originalRequestEvent || !isContributorRequest(originalRequestEvent)) {
|
|
264
|
+
throw new Error("Original contributor request event not found or invalid.")
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const existingResponse = await DatasetEvent.findOne({
|
|
268
|
+
"event.type": "contributorRequestResponse",
|
|
269
|
+
"event.requestId": requestId,
|
|
270
|
+
})
|
|
271
|
+
if (existingResponse) {
|
|
272
|
+
throw new Error("This contributor request has already been processed.")
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
originalRequestEvent.event.resolutionStatus = resolutionStatus
|
|
276
|
+
await originalRequestEvent.save()
|
|
277
|
+
|
|
278
|
+
const responseEvent = new DatasetEvent({
|
|
279
|
+
datasetId,
|
|
280
|
+
userId: currentUserId,
|
|
281
|
+
event: {
|
|
282
|
+
type: "contributorRequestResponse",
|
|
283
|
+
requestId,
|
|
284
|
+
targetUserId,
|
|
285
|
+
reason,
|
|
286
|
+
datasetId,
|
|
287
|
+
resolutionStatus,
|
|
288
|
+
contributorData: originalRequestEvent.event.contributorData,
|
|
289
|
+
},
|
|
290
|
+
success: true,
|
|
291
|
+
note: reason?.trim() ||
|
|
292
|
+
`Admin ${currentUserId} processed contributor request for user ${targetUserId} as '${resolutionStatus}'.`,
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
await responseEvent.save()
|
|
296
|
+
await responseEvent.populate("user")
|
|
297
|
+
|
|
298
|
+
if (resolutionStatus === "accepted") {
|
|
299
|
+
const targetUser = await User.findOne({ id: targetUserId })
|
|
300
|
+
if (!targetUser) throw new Error("Target user not found.")
|
|
301
|
+
|
|
302
|
+
const existingDatacite = await getDataciteYml(datasetId)
|
|
303
|
+
const existingContributors =
|
|
304
|
+
existingDatacite?.data.attributes.contributors || []
|
|
305
|
+
|
|
306
|
+
const mappedExisting: Contributor[] = existingContributors.map((
|
|
307
|
+
c,
|
|
308
|
+
index,
|
|
309
|
+
) => ({
|
|
310
|
+
name: c.name || "Unknown Contributor",
|
|
311
|
+
givenName: c.givenName || "",
|
|
312
|
+
familyName: c.familyName || "",
|
|
313
|
+
orcid: c.nameIdentifiers?.[0]?.nameIdentifier,
|
|
314
|
+
contributorType: c.contributorType || "Researcher",
|
|
315
|
+
order: index + 1,
|
|
316
|
+
}))
|
|
317
|
+
|
|
318
|
+
const newContributor: Contributor = {
|
|
319
|
+
name: targetUser.name || "Unknown Contributor",
|
|
320
|
+
givenName: targetUser?.givenName || "",
|
|
321
|
+
familyName: targetUser?.familyName || "",
|
|
322
|
+
orcid: targetUser.orcid,
|
|
323
|
+
contributorType: "Researcher",
|
|
324
|
+
order: mappedExisting.length + 1,
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
await updateContributorsUtil(
|
|
328
|
+
datasetId,
|
|
329
|
+
[...mappedExisting, newContributor],
|
|
330
|
+
currentUserId,
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return responseEvent
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Update a user's notification status
|
|
339
|
+
*/
|
|
340
|
+
export async function updateEventStatus(obj, { eventId, status }, { user }) {
|
|
341
|
+
if (!user) throw new Error("Authentication required.")
|
|
342
|
+
return await UserNotificationStatus.findOneAndUpdate(
|
|
343
|
+
{ userId: user, datasetEventId: eventId },
|
|
344
|
+
{ status },
|
|
345
|
+
{ new: true, upsert: true },
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Create a 'contributor citation' event
|
|
351
|
+
* Immediately adds the contributor to datacite.yml
|
|
352
|
+
* Automatically sets the resolutionStatus to 'accepted'
|
|
353
|
+
*/
|
|
354
|
+
export async function createContributorCitationEvent(
|
|
355
|
+
obj,
|
|
356
|
+
{ datasetId, targetUserId, contributorData }: {
|
|
357
|
+
datasetId: string
|
|
358
|
+
targetUserId: string
|
|
359
|
+
contributorData: {
|
|
360
|
+
orcid?: string
|
|
361
|
+
name?: string
|
|
362
|
+
email?: string
|
|
363
|
+
userId?: string
|
|
364
|
+
contributorType?: string
|
|
365
|
+
givenName?: string
|
|
366
|
+
familyName?: string
|
|
367
|
+
}
|
|
368
|
+
},
|
|
369
|
+
{ user }: { user: string },
|
|
370
|
+
) {
|
|
371
|
+
if (!user) throw new Error("Authentication required.")
|
|
372
|
+
|
|
373
|
+
const finalContributorData = {
|
|
374
|
+
...contributorData,
|
|
375
|
+
contributorType: contributorData.contributorType || "Researcher",
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// --- Immediately add to datacite.yml ---
|
|
379
|
+
const existingDatacite = await getDataciteYml(datasetId)
|
|
380
|
+
const existingContributors = existingDatacite?.data.attributes.contributors ||
|
|
381
|
+
[]
|
|
382
|
+
|
|
383
|
+
const mappedExisting: Contributor[] = existingContributors.map((
|
|
384
|
+
c,
|
|
385
|
+
index,
|
|
386
|
+
) => ({
|
|
387
|
+
name: c.name || "Unknown Contributor",
|
|
388
|
+
givenName: c.givenName || "",
|
|
389
|
+
familyName: c.familyName || "",
|
|
390
|
+
orcid: c.nameIdentifiers?.[0]?.nameIdentifier,
|
|
391
|
+
contributorType: c.contributorType || "Researcher",
|
|
392
|
+
order: index + 1,
|
|
393
|
+
}))
|
|
394
|
+
|
|
395
|
+
const newContributor: Contributor = {
|
|
396
|
+
name: finalContributorData.name || "Unknown Contributor",
|
|
397
|
+
givenName: finalContributorData.givenName || "",
|
|
398
|
+
familyName: finalContributorData.familyName || "",
|
|
399
|
+
orcid: finalContributorData.orcid,
|
|
400
|
+
contributorType: finalContributorData.contributorType || "Researcher",
|
|
401
|
+
order: mappedExisting.length + 1,
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
await updateContributorsUtil(
|
|
405
|
+
datasetId,
|
|
406
|
+
[...mappedExisting, newContributor],
|
|
407
|
+
user,
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
// --- Log dataset event ---
|
|
411
|
+
const event = new DatasetEvent({
|
|
412
|
+
datasetId,
|
|
413
|
+
userId: user,
|
|
414
|
+
event: {
|
|
415
|
+
type: "contributorCitation",
|
|
416
|
+
datasetId,
|
|
417
|
+
addedBy: user,
|
|
418
|
+
targetUserId,
|
|
419
|
+
contributorData: finalContributorData,
|
|
420
|
+
resolutionStatus: "accepted", // auto-approved
|
|
421
|
+
},
|
|
422
|
+
success: true,
|
|
423
|
+
note: `User ${user} added a contributor citation for user ${targetUserId}.`,
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
await event.save()
|
|
427
|
+
await event.populate("user")
|
|
428
|
+
|
|
429
|
+
return event
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Process a contributor citation (accept or deny)
|
|
434
|
+
* No longer updates datacite.yml — only logs a response event
|
|
435
|
+
*/
|
|
436
|
+
export async function processContributorCitation(
|
|
437
|
+
obj,
|
|
438
|
+
{
|
|
439
|
+
eventId,
|
|
440
|
+
status,
|
|
441
|
+
}: {
|
|
442
|
+
eventId: string
|
|
443
|
+
status: "accepted" | "denied"
|
|
444
|
+
},
|
|
445
|
+
{ user, userInfo }: { user: string; userInfo: { admin?: boolean } },
|
|
446
|
+
) {
|
|
447
|
+
if (!user) throw new Error("Authentication required.")
|
|
448
|
+
|
|
449
|
+
const citationEvent = await DatasetEvent.findOne({ id: eventId })
|
|
450
|
+
|
|
451
|
+
if (!citationEvent || citationEvent.event.type !== "contributorCitation") {
|
|
452
|
+
throw new Error("Contributor citation event not found.")
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const currentUser = await User.findOne({ id: user })
|
|
456
|
+
|
|
457
|
+
const isTargetUser = citationEvent.event.targetUserId === user ||
|
|
458
|
+
citationEvent.event.targetUserId === currentUser?.orcid
|
|
459
|
+
const isAdmin = userInfo?.admin === true
|
|
460
|
+
|
|
461
|
+
if (!isTargetUser && !isAdmin) {
|
|
462
|
+
throw new Error("Not authorized to respond to this contributor citation.")
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (citationEvent.event.resolutionStatus !== "pending") {
|
|
466
|
+
throw new Error("This contributor citation has already been responded to.")
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
citationEvent.event.resolutionStatus = status
|
|
470
|
+
await citationEvent.save()
|
|
471
|
+
|
|
472
|
+
// --- Only log response event ---
|
|
473
|
+
const responseEvent = new DatasetEvent({
|
|
474
|
+
datasetId: citationEvent.datasetId,
|
|
475
|
+
userId: user,
|
|
476
|
+
event: {
|
|
477
|
+
type: "contributorCitationResponse",
|
|
478
|
+
originalCitationId: citationEvent.id,
|
|
479
|
+
resolutionStatus: status,
|
|
480
|
+
datasetId: citationEvent.datasetId,
|
|
481
|
+
addedBy: citationEvent.event.addedBy,
|
|
482
|
+
targetUserId: citationEvent.event.targetUserId,
|
|
483
|
+
contributorData: citationEvent.event.contributorData,
|
|
484
|
+
},
|
|
485
|
+
success: true,
|
|
486
|
+
note:
|
|
487
|
+
`User ${user} ${status} contributor citation for ${citationEvent.event.targetUserId}.`,
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
await responseEvent.save()
|
|
491
|
+
await responseEvent.populate("user")
|
|
492
|
+
|
|
493
|
+
return responseEvent
|
|
494
|
+
}
|
|
@@ -9,6 +9,7 @@ import { getFiles } from "../../datalad/files"
|
|
|
9
9
|
import { filterRemovedAnnexObjects } from "../utils/file.js"
|
|
10
10
|
import { validation } from "./validation"
|
|
11
11
|
import FileCheck from "../../models/fileCheck"
|
|
12
|
+
import { contributors } from "../../datalad/contributors"
|
|
12
13
|
|
|
13
14
|
// A draft must have a dataset parent
|
|
14
15
|
export const draftFiles = async (dataset, args, { userInfo }) => {
|
|
@@ -59,6 +60,7 @@ const draft = {
|
|
|
59
60
|
readme,
|
|
60
61
|
head: (obj) => obj.revision,
|
|
61
62
|
fileCheck,
|
|
63
|
+
contributors: (parent) => contributors(parent),
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
export default draft
|
|
@@ -43,9 +43,17 @@ import {
|
|
|
43
43
|
finishImportRemoteDataset,
|
|
44
44
|
importRemoteDataset,
|
|
45
45
|
} from "./importRemoteDataset"
|
|
46
|
-
import {
|
|
46
|
+
import {
|
|
47
|
+
createContributorCitationEvent,
|
|
48
|
+
createContributorRequestEvent,
|
|
49
|
+
processContributorCitation,
|
|
50
|
+
processContributorRequest,
|
|
51
|
+
saveAdminNote,
|
|
52
|
+
updateEventStatus,
|
|
53
|
+
} from "./datasetEvents"
|
|
47
54
|
import { createGitEvent } from "./gitEvents"
|
|
48
55
|
import { updateFileCheck } from "./fileCheck"
|
|
56
|
+
import { updateContributors } from "../../datalad/contributors"
|
|
49
57
|
import { updateWorkerTask } from "./worker"
|
|
50
58
|
|
|
51
59
|
const Mutation = {
|
|
@@ -94,8 +102,14 @@ const Mutation = {
|
|
|
94
102
|
finishImportRemoteDataset,
|
|
95
103
|
updateUser,
|
|
96
104
|
saveAdminNote,
|
|
105
|
+
createContributorRequestEvent,
|
|
106
|
+
createContributorCitationEvent,
|
|
107
|
+
processContributorRequest,
|
|
108
|
+
processContributorCitation,
|
|
97
109
|
createGitEvent,
|
|
98
110
|
updateFileCheck,
|
|
111
|
+
updateEventStatus,
|
|
112
|
+
updateContributors,
|
|
99
113
|
updateWorkerTask,
|
|
100
114
|
}
|
|
101
115
|
|
|
@@ -18,6 +18,7 @@ import { getDraftHead } from "../../datalad/dataset"
|
|
|
18
18
|
import { downloadFiles } from "../../datalad/snapshots"
|
|
19
19
|
import { snapshotValidation } from "./validation"
|
|
20
20
|
import { advancedDatasetSearchConnection } from "./dataset-search"
|
|
21
|
+
import { contributors } from "../../datalad/contributors"
|
|
21
22
|
|
|
22
23
|
export const snapshots = (obj) => {
|
|
23
24
|
return datalad.getSnapshots(obj.id)
|
|
@@ -28,6 +29,7 @@ export const snapshot = (obj, { datasetId, tag }, context) => {
|
|
|
28
29
|
() => {
|
|
29
30
|
return datalad.getSnapshot(datasetId, tag).then((snapshot) => ({
|
|
30
31
|
...snapshot,
|
|
32
|
+
datasetId,
|
|
31
33
|
dataset: () => dataset(snapshot, { id: datasetId }, context),
|
|
32
34
|
description: () => description(snapshot),
|
|
33
35
|
readme: () => readme(snapshot),
|
|
@@ -310,6 +312,14 @@ const Snapshot = {
|
|
|
310
312
|
issues: (snapshot) => snapshotIssues(snapshot),
|
|
311
313
|
issuesStatus: (snapshot) => issuesSnapshotStatus(snapshot),
|
|
312
314
|
validation: (snapshot) => snapshotValidation(snapshot),
|
|
315
|
+
contributors: (snapshot) => {
|
|
316
|
+
const datasetId = snapshot.datasetId
|
|
317
|
+
return contributors({
|
|
318
|
+
id: `${datasetId}:${snapshot.hexsha}`,
|
|
319
|
+
tag: snapshot.tag,
|
|
320
|
+
hexsha: snapshot.hexsha,
|
|
321
|
+
})
|
|
322
|
+
},
|
|
313
323
|
}
|
|
314
324
|
|
|
315
325
|
export default Snapshot
|