@openneuro/server 5.0.0 → 5.1.1

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.
Files changed (101) hide show
  1. package/package.json +8 -5
  2. package/src/app.ts +2 -4
  3. package/src/cache/__tests__/tree.spec.ts +2 -0
  4. package/src/cache/tree.ts +4 -0
  5. package/src/cache/types.ts +1 -0
  6. package/src/datalad/__tests__/contributors.spec.ts +1 -1
  7. package/src/datalad/__tests__/dataRetentionNotifications.spec.ts +11 -11
  8. package/src/datalad/__tests__/dataset.spec.ts +2 -2
  9. package/src/datalad/__tests__/files.spec.ts +31 -1
  10. package/src/datalad/__tests__/snapshots.spec.ts +5 -5
  11. package/src/datalad/contributors.ts +2 -2
  12. package/src/datalad/dataRetentionNotifications.ts +5 -5
  13. package/src/datalad/dataset.ts +26 -10
  14. package/src/datalad/description.ts +2 -2
  15. package/src/datalad/draft.ts +8 -3
  16. package/src/datalad/files.ts +71 -12
  17. package/src/datalad/readme.ts +2 -2
  18. package/src/datalad/snapshots.ts +19 -14
  19. package/src/elasticsearch/elastic-client.ts +11 -6
  20. package/src/elasticsearch/reindex-dataset.ts +8 -4
  21. package/src/graphql/__tests__/comment.spec.ts +3 -2
  22. package/src/graphql/__tests__/schema.spec.ts +28 -0
  23. package/src/graphql/builder.ts +42 -0
  24. package/src/graphql/resolvers/__tests__/dataset.spec.ts +6 -119
  25. package/src/graphql/resolvers/__tests__/derivatives.spec.ts +11 -0
  26. package/src/graphql/resolvers/__tests__/importRemoteDataset.spec.ts +2 -1
  27. package/src/graphql/resolvers/__tests__/permssions.spec.ts +3 -2
  28. package/src/graphql/resolvers/__tests__/user.spec.ts +39 -11
  29. package/src/graphql/resolvers/brainInitiative.ts +4 -3
  30. package/src/graphql/resolvers/cache.ts +7 -6
  31. package/src/graphql/resolvers/comment.ts +35 -19
  32. package/src/graphql/resolvers/dataset-search.ts +7 -6
  33. package/src/graphql/resolvers/dataset.ts +78 -46
  34. package/src/graphql/resolvers/datasetEvents.ts +18 -16
  35. package/src/graphql/resolvers/derivatives.ts +5 -1
  36. package/src/graphql/resolvers/description.ts +2 -1
  37. package/src/graphql/resolvers/draft.ts +14 -3
  38. package/src/graphql/resolvers/fileCheck.ts +5 -4
  39. package/src/graphql/resolvers/flaggedFiles.ts +2 -1
  40. package/src/graphql/resolvers/follow.ts +2 -1
  41. package/src/graphql/resolvers/git.ts +2 -1
  42. package/src/graphql/resolvers/gitEvents.ts +2 -1
  43. package/src/graphql/resolvers/history.ts +3 -1
  44. package/src/graphql/resolvers/holdDeletion.ts +1 -1
  45. package/src/graphql/resolvers/importRemoteDataset.ts +3 -2
  46. package/src/graphql/resolvers/issues.ts +2 -1
  47. package/src/graphql/resolvers/metadata.ts +3 -2
  48. package/src/graphql/resolvers/permissions.ts +26 -6
  49. package/src/graphql/resolvers/publish.ts +2 -1
  50. package/src/graphql/resolvers/readme.ts +2 -1
  51. package/src/graphql/resolvers/reexporter.ts +2 -1
  52. package/src/graphql/resolvers/relation.ts +3 -2
  53. package/src/graphql/resolvers/reset.ts +2 -1
  54. package/src/graphql/resolvers/reviewer.ts +14 -6
  55. package/src/graphql/resolvers/snapshots.ts +42 -10
  56. package/src/graphql/resolvers/stars.ts +2 -1
  57. package/src/graphql/resolvers/upload.ts +3 -2
  58. package/src/graphql/resolvers/user.ts +48 -32
  59. package/src/graphql/resolvers/validation.ts +6 -5
  60. package/src/graphql/resolvers/worker.ts +2 -1
  61. package/src/graphql/schema/analytics.ts +12 -0
  62. package/src/graphql/schema/comment.ts +30 -0
  63. package/src/graphql/schema/dataset-events.ts +91 -0
  64. package/src/graphql/schema/dataset-search.ts +32 -0
  65. package/src/graphql/schema/dataset.ts +167 -0
  66. package/src/graphql/schema/description.ts +44 -0
  67. package/src/graphql/schema/draft.ts +80 -0
  68. package/src/graphql/schema/enums.ts +55 -0
  69. package/src/graphql/schema/files.ts +44 -0
  70. package/src/graphql/schema/inputs.ts +231 -0
  71. package/src/graphql/schema/metadata.ts +86 -0
  72. package/src/graphql/schema/misc.ts +154 -0
  73. package/src/graphql/schema/mutation.ts +551 -0
  74. package/src/graphql/schema/pagination.ts +12 -0
  75. package/src/graphql/schema/permissions.ts +21 -0
  76. package/src/graphql/schema/query.ts +119 -0
  77. package/src/graphql/schema/refs.ts +23 -0
  78. package/src/graphql/schema/reviewer.ts +11 -0
  79. package/src/graphql/schema/scalars.ts +9 -0
  80. package/src/graphql/schema/snapshot.ts +111 -0
  81. package/src/graphql/schema/upload.ts +13 -0
  82. package/src/graphql/schema/user.ts +62 -0
  83. package/src/graphql/schema/validation.ts +70 -0
  84. package/src/graphql/schema/worker.ts +16 -0
  85. package/src/graphql/schema.ts +29 -1114
  86. package/src/handlers/subscriptions.ts +1 -1
  87. package/src/libs/apikey.ts +1 -1
  88. package/src/libs/authentication/crypto.ts +14 -9
  89. package/src/libs/notifications.ts +1 -1
  90. package/src/libs/presign.ts +32 -20
  91. package/src/libs/redis.ts +12 -2
  92. package/src/models/comment.ts +2 -4
  93. package/src/models/counter.ts +2 -2
  94. package/src/models/ingestDataset.ts +1 -2
  95. package/src/models/notification.ts +0 -2
  96. package/src/models/subscription.ts +0 -1
  97. package/src/models/user.ts +7 -2
  98. package/src/models/userMigration.ts +1 -1
  99. package/src/models/userNotificationStatus.ts +0 -1
  100. package/src/utils/__tests__/snapshots.spec.ts +120 -0
  101. package/src/utils/snapshots.ts +12 -0
@@ -12,7 +12,7 @@ import mongoose, { Types } from "mongoose"
12
12
  import User from "../../../models/user"
13
13
  import DatasetEvent from "../../../models/datasetEvents"
14
14
  import { notifications, user, users } from "../user.js"
15
- import type { GraphQLContext } from "../user.js"
15
+ import type { GraphQLContext } from "../../builder"
16
16
 
17
17
  vi.mock("ioredis")
18
18
 
@@ -108,13 +108,25 @@ const testUsersSeedData = [
108
108
  ]
109
109
 
110
110
  // Admin context for tests
111
- const adminContext: GraphQLContext = {
112
- userInfo: { userId: "admin-user", admin: true, username: "adminUser" },
113
- }
111
+ const adminContext = {
112
+ user: "admin-user",
113
+ userInfo: {
114
+ id: "admin-user",
115
+ userId: "admin-user",
116
+ admin: true,
117
+ username: "adminUser",
118
+ },
119
+ } as GraphQLContext
114
120
  // Non-admin context for tests
115
- const nonAdminContext: GraphQLContext = {
116
- userInfo: { userId: "normal-user", admin: false, username: "normalUser" },
117
- }
121
+ const nonAdminContext = {
122
+ user: "normal-user",
123
+ userInfo: {
124
+ id: "normal-user",
125
+ userId: "normal-user",
126
+ admin: false,
127
+ username: "normalUser",
128
+ },
129
+ } as GraphQLContext
118
130
 
119
131
  describe("user resolvers", () => {
120
132
  beforeAll(async () => {
@@ -392,14 +404,26 @@ describe("user resolvers", () => {
392
404
  const result = await notifications(
393
405
  { id: "reviewer" },
394
406
  null,
395
- { userInfo: { reviewer: true, admin: false, blocked: false } },
407
+ {
408
+ userInfo: {
409
+ id: "reviewer",
410
+ userId: "reviewer",
411
+ reviewer: true,
412
+ admin: false,
413
+ blocked: false,
414
+ },
415
+ } as GraphQLContext,
396
416
  )
397
417
  expect(result).toEqual([])
398
418
  })
399
419
 
400
420
  it("does not crash when userInfo is undefined", async () => {
401
421
  await expect(
402
- notifications({ id: "u1" }, null, { userInfo: undefined }),
422
+ notifications(
423
+ { id: "u1" },
424
+ null,
425
+ { userInfo: undefined } as GraphQLContext,
426
+ ),
403
427
  ).rejects.toThrow("Not authorized to view these notifications.")
404
428
  })
405
429
 
@@ -422,7 +446,9 @@ describe("user resolvers", () => {
422
446
  const result = await notifications(
423
447
  { id: "u1" },
424
448
  null,
425
- { userInfo: { id: "u1", admin: false, userId: "u1" } },
449
+ {
450
+ userInfo: { id: "u1", admin: false, userId: "u1" },
451
+ } as GraphQLContext,
426
452
  )
427
453
  expect(result.length).toBe(1)
428
454
  expect(result[0].id).toBe("event-1")
@@ -434,7 +460,9 @@ describe("user resolvers", () => {
434
460
  notifications(
435
461
  { id: "u1" },
436
462
  null,
437
- { userInfo: { id: "u2", admin: false, userId: "u2" } },
463
+ {
464
+ userInfo: { id: "u2", admin: false, userId: "u2" },
465
+ } as GraphQLContext,
438
466
  ),
439
467
  ).rejects.toThrow("Not authorized to view these notifications.")
440
468
  })
@@ -1,4 +1,4 @@
1
- import { redis } from "../../libs/redis"
1
+ import { getRedis } from "../../libs/redis"
2
2
  import type { DatasetOrSnapshot } from "../../utils/datasetOrSnapshot"
3
3
  import { latestSnapshot } from "./snapshots"
4
4
  import { description } from "../../datalad/description"
@@ -6,6 +6,7 @@ import Metadata from "../../models/metadata"
6
6
  import CacheItem, { CacheType } from "../../cache/item"
7
7
  import * as Sentry from "@sentry/node"
8
8
  import fundedAwards from "../../data/funded_awards.json"
9
+ import type { GraphQLContext } from "../builder"
9
10
 
10
11
  const brainInitiativeMatch = new RegExp("brain.initiative", "i")
11
12
 
@@ -19,10 +20,10 @@ const brainInitiativeGrants = fundedAwards.map((award) =>
19
20
  export const brainInitiative = async (
20
21
  dataset: DatasetOrSnapshot,
21
22
  _,
22
- context,
23
+ context: GraphQLContext,
23
24
  ): Promise<boolean> => {
24
25
  const cache = new CacheItem(
25
- redis,
26
+ getRedis(),
26
27
  CacheType.brainInitiative,
27
28
  [dataset.id],
28
29
  86400,
@@ -1,25 +1,26 @@
1
- import { redis } from "../../libs/redis.js"
1
+ import { getRedis } from "../../libs/redis.js"
2
2
  import { clearDatasetTrees } from "../../cache/tree"
3
+ import type { GraphQLContext } from "../builder"
3
4
 
4
5
  /**
5
6
  * Clear all cache entries for a given datasetId
6
7
  */
7
8
  export async function cacheClear(
8
- obj: Record<string, unknown>,
9
+ obj: unknown,
9
10
  { datasetId }: { datasetId: string },
10
- { userInfo }: { userInfo: { admin: boolean } },
11
+ { userInfo }: GraphQLContext,
11
12
  ): Promise<boolean> {
12
13
  // Check for admin and validate datasetId argument
13
14
  if (userInfo?.admin && datasetId.length == 8 && datasetId.startsWith("ds")) {
14
15
  try {
15
16
  // Clear tree cache entries via the dataset-to-trees index
16
- await clearDatasetTrees(redis, datasetId)
17
+ await clearDatasetTrees(getRedis(), datasetId)
17
18
 
18
19
  // Also clear non-tree cache keys (descriptions, snapshots, etc.)
19
- const stream = redis.scanStream({
20
+ const stream = getRedis().scanStream({
20
21
  match: `*${datasetId}*`,
21
22
  })
22
- const pipeline = redis.pipeline()
23
+ const pipeline = getRedis().pipeline()
23
24
  for await (const keys of stream) {
24
25
  for (const key of keys) {
25
26
  pipeline.del(key)
@@ -1,21 +1,30 @@
1
1
  import Comment from "../../models/comment"
2
+ import type { CommentDocument } from "../../models/comment"
2
3
  import notifications from "../../libs/notifications"
3
4
  import { user } from "./user.js"
4
5
  import { checkAdmin } from "../permissions"
6
+ import type { GraphQLContext } from "../builder"
5
7
 
6
- export const comment = (obj, { id }) => {
8
+ export const comment = (
9
+ obj: unknown,
10
+ { id }: { id: string },
11
+ ): Promise<CommentDocument | null> => {
7
12
  return Comment.findOne({ _id: id }).exec()
8
13
  }
9
14
 
10
- export const datasetComments = (obj) => {
15
+ export const datasetComments = (
16
+ obj: { id: string },
17
+ ): Promise<CommentDocument[]> => {
11
18
  return Comment.find({ datasetId: obj.id }).exec()
12
19
  }
13
20
 
14
- export const userComments = (obj) => {
21
+ export const userComments = (
22
+ obj: { id: string },
23
+ ): Promise<CommentDocument[]> => {
15
24
  return Comment.find({ "user._id": obj.id }).exec()
16
25
  }
17
26
 
18
- const replies = (obj) => {
27
+ const replies = (obj: CommentDocument): Promise<CommentDocument[]> => {
19
28
  return Comment.find({ parentId: obj._id }).exec()
20
29
  }
21
30
 
@@ -31,7 +40,7 @@ export const flatten = (arr) => [].concat(...arr)
31
40
  * @param {import('../../models/comment').CommentDocument} obj
32
41
  * @returns {Promise<import('../../models/comment').CommentDocument[]>}
33
42
  */
34
- const allNestedReplies = async (obj) => {
43
+ const allNestedReplies = async (obj: CommentDocument) => {
35
44
  const replies = await Comment.find({ parentId: obj._id }).exec()
36
45
  if (!replies.length) {
37
46
  return replies
@@ -45,9 +54,13 @@ const allNestedReplies = async (obj) => {
45
54
  * Insert new comment and return the comment _id for replies to reference
46
55
  */
47
56
  export const addComment = (
48
- obj,
49
- { datasetId, parentId, comment: text },
50
- { user },
57
+ obj: unknown,
58
+ { datasetId, parentId, comment: text }: {
59
+ datasetId: string
60
+ parentId?: string
61
+ comment: string
62
+ },
63
+ { user }: GraphQLContext,
51
64
  ) => {
52
65
  if (!user) {
53
66
  return Promise.reject(
@@ -62,18 +75,18 @@ export const addComment = (
62
75
  })
63
76
  return newComment.save().then((commentInsert) => {
64
77
  notifications.commentCreated(commentInsert)
65
- return commentInsert._id
78
+ return commentInsert._id.toString()
66
79
  })
67
80
  }
68
81
 
69
82
  export const editComment = async (
70
- obj,
71
- { commentId, comment: text },
72
- { user },
83
+ obj: unknown,
84
+ { commentId, comment: text }: { commentId: string; comment: string },
85
+ { user }: GraphQLContext,
73
86
  ) => {
74
87
  const existingComment = await Comment.findById(commentId).exec()
75
88
  // You may only edit your own comments
76
- if (existingComment.user._id === user) {
89
+ if (existingComment.user._id.toString() === user) {
77
90
  existingComment.text = text
78
91
  return existingComment.save().then(() => true)
79
92
  } else {
@@ -82,9 +95,12 @@ export const editComment = async (
82
95
  }
83
96
 
84
97
  export const deleteComment = async (
85
- obj,
86
- { commentId, deleteChildren = true },
87
- { user, userInfo },
98
+ obj: unknown,
99
+ { commentId, deleteChildren = true }: {
100
+ commentId: string
101
+ deleteChildren?: boolean
102
+ },
103
+ { user, userInfo }: GraphQLContext,
88
104
  ) => {
89
105
  await checkAdmin(user, userInfo)
90
106
 
@@ -93,7 +109,7 @@ export const deleteComment = async (
93
109
  if (deleteChildren) {
94
110
  targetComments.push(...(await allNestedReplies(existingComment)))
95
111
  }
96
- const deletedCommentIds = targetComments.map((c) => c._id)
112
+ const deletedCommentIds = targetComments.map((c) => c._id.toString())
97
113
  return Comment.deleteMany({
98
114
  _id: {
99
115
  $in: deletedCommentIds,
@@ -103,9 +119,9 @@ export const deleteComment = async (
103
119
 
104
120
  //"5c9bf7e3088cea6fa775c42a"
105
121
  const CommentFields = {
106
- parent: (obj) => comment(obj, { id: obj.parentId }),
122
+ parent: (obj: CommentDocument) => comment(obj, { id: obj.parentId }),
107
123
  replies,
108
- user: (obj) => user(obj, { id: obj.user._id }),
124
+ user: (obj: CommentDocument) => user(obj, { id: obj.user._id.toString() }),
109
125
  }
110
126
 
111
127
  export default CommentFields
@@ -1,11 +1,12 @@
1
1
  import * as Sentry from "@sentry/node"
2
- import { elasticClient } from "../../elasticsearch/elastic-client"
2
+ import { getElasticClient } from "../../elasticsearch/elastic-client"
3
3
  import { dataset } from "./dataset"
4
4
  import Star from "../../models/stars"
5
5
  import Subscription from "../../models/subscription"
6
6
  import Permission from "../../models/permission"
7
7
  import { hashObject } from "../../libs/authentication/crypto"
8
8
  import { buildElasticQuery } from "./build-search-query"
9
+ import type { GraphQLContext } from "../builder"
9
10
 
10
11
  const elasticIndex = "datasets"
11
12
 
@@ -16,7 +17,7 @@ const elasticIndex = "datasets"
16
17
  * @returns {Promise}
17
18
  */
18
19
  export const removeDatasetSearchDocument = (id) =>
19
- elasticClient.delete({ id, index: elasticIndex })
20
+ getElasticClient().delete({ id, index: elasticIndex })
20
21
 
21
22
  /**
22
23
  * Accepts an array of fields representing the sort order for the search
@@ -56,7 +57,7 @@ export const elasticRelayConnection = (
56
57
  const node = childResolvers.dataset(
57
58
  null,
58
59
  { id: hit._source.id },
59
- { user, userInfo },
60
+ { user, userInfo } as GraphQLContext,
60
61
  )
61
62
  return { id: hit._source.id, node }
62
63
  }),
@@ -100,7 +101,7 @@ export const datasetSearchConnection = async (
100
101
  // Don't include search_after if parsing fails
101
102
  }
102
103
  }
103
- await elasticClient.search({
104
+ await getElasticClient().search({
104
105
  index: elasticIndex,
105
106
  size: first,
106
107
  q: `${q} AND public:true`,
@@ -225,7 +226,7 @@ export const advancedDatasetSearchConnection = async (
225
226
  after,
226
227
  first = 25,
227
228
  },
228
- { user, userInfo },
229
+ { user, userInfo }: GraphQLContext,
229
230
  ) => {
230
231
  // Build the ES query from structured input
231
232
  const { query: esQuery, isEmpty } = buildElasticQuery(searchInput)
@@ -269,7 +270,7 @@ export const advancedDatasetSearchConnection = async (
269
270
  search_after,
270
271
  }
271
272
  // Run the query
272
- const result = await elasticClient.search(requestBody)
273
+ const result = await getElasticClient().search(requestBody)
273
274
  // Extend with relay connection pagination
274
275
  return elasticRelayConnection(
275
276
  result,
@@ -1,5 +1,6 @@
1
1
  import * as datalad from "../../datalad/dataset"
2
2
  import { removeDatasetSearchDocument } from "./dataset-search"
3
+ import { snapshotCreationComparison } from "../../utils/snapshots"
3
4
  import { latestSnapshot, snapshots } from "./snapshots"
4
5
  import { description } from "./description"
5
6
  import {
@@ -23,15 +24,24 @@ import { brainInitiative } from "./brainInitiative"
23
24
  import { derivatives } from "./derivatives"
24
25
  import { promiseTimeout } from "../../utils/promiseTimeout"
25
26
  import { datasetEvents } from "./datasetEvents"
26
- import semver from "semver"
27
27
  import { getDraftInfo } from "../../datalad/draft"
28
+ import type { GraphQLContext } from "../builder"
29
+ import type { DatasetDocument } from "../../models/dataset"
28
30
 
29
- export const dataset = async (obj, { id }, { user, userInfo }) => {
31
+ export const dataset = async (
32
+ obj: unknown,
33
+ { id }: { id: string },
34
+ { user, userInfo }: GraphQLContext,
35
+ ) => {
30
36
  await checkDatasetRead(id, user, userInfo)
31
37
  return promiseTimeout(datalad.getDataset(id), 30000)
32
38
  }
33
39
 
34
- export const datasets = (parent, args, { user, userInfo }) => {
40
+ export const datasets = (
41
+ parent: unknown,
42
+ args: Record<string, unknown>,
43
+ { user, userInfo }: GraphQLContext,
44
+ ) => {
35
45
  if (user) {
36
46
  return datalad.getDatasets({
37
47
  ...args,
@@ -43,22 +53,11 @@ export const datasets = (parent, args, { user, userInfo }) => {
43
53
  }
44
54
  }
45
55
 
46
- export const snapshotCreationComparison = (
47
- { created: a, tag: a_tag },
48
- { created: b, tag: b_tag },
49
- ) => {
50
- if (semver.valid(a_tag) && semver.valid(b_tag)) {
51
- return semver.compare(a_tag, b_tag)
52
- } else {
53
- return new Date(a).getTime() - new Date(b).getTime()
54
- }
55
- }
56
-
57
56
  /**
58
57
  * Find the canonical name for a dataset from snapshots and drafts
59
58
  * @param {object} obj Dataset object (at least {id: "datasetId"})
60
59
  */
61
- export const datasetName = (obj) => {
60
+ export const datasetName = (obj: { id: string; revision?: string }) => {
62
61
  return snapshots(obj).then((results) => {
63
62
  if (results && results.length) {
64
63
  // Return the latest snapshot name
@@ -83,7 +82,7 @@ export const datasetName = (obj) => {
83
82
  * Resolve the best dataset name and cache in mongodb
84
83
  * @param {string} datasetId
85
84
  */
86
- export const updateDatasetName = (datasetId) =>
85
+ export const updateDatasetName = (datasetId: string) =>
87
86
  datasetName({ id: datasetId }).then((name) =>
88
87
  DatasetModel.updateOne({ id: datasetId }, { $set: { name } }).exec()
89
88
  )
@@ -92,9 +91,12 @@ export const updateDatasetName = (datasetId) =>
92
91
  * Create an empty dataset (new repo, new accession number)
93
92
  */
94
93
  export const createDataset = (
95
- obj,
96
- { affirmedDefaced, affirmedConsent },
97
- { user, userInfo },
94
+ obj: unknown,
95
+ { affirmedDefaced, affirmedConsent }: {
96
+ affirmedDefaced: boolean
97
+ affirmedConsent: boolean
98
+ },
99
+ { user, userInfo }: GraphQLContext,
98
100
  ) => {
99
101
  // Check for a valid login
100
102
  if (user) {
@@ -117,9 +119,9 @@ export const createDataset = (
117
119
  * Delete an existing dataset, as well as all snapshots
118
120
  */
119
121
  export const deleteDataset = async (
120
- obj,
121
- { id, reason, redirect },
122
- { user, userInfo },
122
+ obj: unknown,
123
+ { id, reason, redirect }: { id: string; reason: string; redirect: string },
124
+ { user, userInfo }: GraphQLContext,
123
125
  ) => {
124
126
  await checkDatasetWrite(id, user, userInfo)
125
127
  const deleted = await datalad.deleteDataset(id, userInfo)
@@ -144,9 +146,9 @@ export const deleteDataset = async (
144
146
  * Delete files from a draft
145
147
  */
146
148
  export const deleteFiles = async (
147
- obj,
148
- { datasetId, files },
149
- { user, userInfo },
149
+ obj: unknown,
150
+ { datasetId, files }: { datasetId: string; files: { path: string }[] },
151
+ { user, userInfo }: GraphQLContext,
150
152
  ) => {
151
153
  try {
152
154
  await checkDatasetWrite(datasetId, user, userInfo)
@@ -158,9 +160,15 @@ export const deleteFiles = async (
158
160
  }
159
161
 
160
162
  export const removeAnnexObject = async (
161
- obj,
162
- { datasetId, snapshot, path, filename, annexKey },
163
- { user, userInfo },
163
+ obj: unknown,
164
+ { datasetId, snapshot, path, filename, annexKey }: {
165
+ datasetId: string
166
+ snapshot: string
167
+ path: string
168
+ filename: string
169
+ annexKey: string
170
+ },
171
+ { user, userInfo }: GraphQLContext,
164
172
  ) => {
165
173
  try {
166
174
  await checkDatasetAdmin(datasetId, user, userInfo)
@@ -179,9 +187,14 @@ export const removeAnnexObject = async (
179
187
  }
180
188
 
181
189
  export const flagAnnexObject = async (
182
- obj,
183
- { datasetId, snapshot, filepath, annexKey },
184
- { user, userInfo },
190
+ obj: unknown,
191
+ { datasetId, snapshot, filepath, annexKey }: {
192
+ datasetId: string
193
+ snapshot: string
194
+ filepath: string
195
+ annexKey: string
196
+ },
197
+ { user, userInfo }: GraphQLContext,
185
198
  ) => {
186
199
  try {
187
200
  await checkDatasetWrite(datasetId, user, userInfo)
@@ -202,19 +215,21 @@ export const flagAnnexObject = async (
202
215
  * Update the dataset Public status
203
216
  */
204
217
  export const updatePublic = (
205
- obj,
206
- { datasetId, publicFlag },
207
- { user, userInfo },
218
+ obj: unknown,
219
+ { datasetId, publicFlag }: { datasetId: string; publicFlag: boolean },
220
+ { user, userInfo }: GraphQLContext,
208
221
  ) => {
209
222
  return checkDatasetWrite(datasetId, user, userInfo).then(() => {
210
- return datalad.updatePublic(datasetId, publicFlag, user)
223
+ return datalad.updatePublic(datasetId, publicFlag, userInfo)
211
224
  })
212
225
  }
213
226
 
214
227
  /**
215
228
  * Get analytics for a dataset or snapshot
216
229
  */
217
- export const analytics = async (obj) => {
230
+ export const analytics = async (
231
+ obj: { id?: string; dataset?: () => Promise<{ id: string }>; tag?: string },
232
+ ) => {
218
233
  // if the dataset field exists, the request is from a snapshot, and
219
234
  // we resolve the datasetId from the dataset snapshot field of context.
220
235
  // otherwise, just use the id field because the object is a dataset
@@ -228,7 +243,10 @@ export const analytics = async (obj) => {
228
243
  /**
229
244
  * Track analytic of type 'view' or 'download' for a dataset / snapshot
230
245
  */
231
- export const trackAnalytics = (obj, { datasetId, tag, type }) => {
246
+ export const trackAnalytics = (
247
+ obj: unknown,
248
+ { datasetId, tag, type }: { datasetId: string; tag: string; type: string },
249
+ ) => {
232
250
  try {
233
251
  dataladAnalytics.trackAnalytics(datasetId, tag, type)
234
252
  return true
@@ -240,7 +258,9 @@ export const trackAnalytics = (obj, { datasetId, tag, type }) => {
240
258
  /**
241
259
  * Get the star count for the dataset
242
260
  */
243
- export const stars = async (obj) => {
261
+ export const stars = async (
262
+ obj: { id?: string; dataset?: () => Promise<{ id: string }> },
263
+ ) => {
244
264
  const datasetId = obj && obj.dataset ? (await obj.dataset()).id : obj.id
245
265
  return datalad.getStars(datasetId)
246
266
  }
@@ -248,7 +268,9 @@ export const stars = async (obj) => {
248
268
  /**
249
269
  * Get the follower count for the dataset
250
270
  */
251
- export const followers = async (obj) => {
271
+ export const followers = async (
272
+ obj: { id?: string; dataset?: () => Promise<{ id: string }> },
273
+ ) => {
252
274
  const datasetId = obj && obj.dataset ? (await obj.dataset()).id : obj.id
253
275
  return datalad.getFollowers(datasetId)
254
276
  }
@@ -258,7 +280,11 @@ export const followers = async (obj) => {
258
280
  *
259
281
  * Returns null for anonymous users
260
282
  */
261
- export const following = (obj, _, { user }) =>
283
+ export const following = (
284
+ obj: { id: string },
285
+ _: unknown,
286
+ { user }: { user: string },
287
+ ) =>
262
288
  user
263
289
  ? datalad.getUserFollowed(obj.id, user).then((res) => (res ? true : false))
264
290
  : null
@@ -268,19 +294,24 @@ export const following = (obj, _, { user }) =>
268
294
  *
269
295
  * Returns null for anonymous users
270
296
  */
271
- export const starred = (obj, _, { user }) =>
297
+ export const starred = (
298
+ obj: { id: string },
299
+ _: unknown,
300
+ { user }: { user: string },
301
+ ) =>
272
302
  user
273
303
  ? datalad.getUserStarred(obj.id, user).then((res) => (res ? true : false))
274
304
  : null
275
305
 
276
- const worker = (obj) => getDatasetWorker(obj.id)
306
+ const worker = (obj: { id: string }) => getDatasetWorker(obj.id)
277
307
 
278
308
  /**
279
309
  * Dataset object
280
310
  */
281
311
  const Dataset = {
282
- uploader: (ds, _, context) => user(ds, { id: ds.uploader }, context),
283
- draft: async (obj) => {
312
+ uploader: (ds: DatasetDocument, _: unknown, context: GraphQLContext) =>
313
+ user(ds, { id: ds.uploader }, context as never),
314
+ draft: async (obj: DatasetDocument) => {
284
315
  const draftHead = await getDraftInfo(obj.id)
285
316
  return {
286
317
  id: obj.id,
@@ -288,8 +319,9 @@ const Dataset = {
288
319
  modified: draftHead.modified,
289
320
  }
290
321
  },
291
- snapshots,
292
- latestSnapshot,
322
+ // Wrapper functions defer access to break the resolvers/dataset ↔ resolvers/snapshots cycle
323
+ snapshots: (obj) => snapshots(obj),
324
+ latestSnapshot: (obj, args, context) => latestSnapshot(obj, args, context),
293
325
  analytics,
294
326
  stars,
295
327
  followers,
@@ -1,4 +1,5 @@
1
1
  import DatasetEvent from "../../models/datasetEvents"
2
+ import type { GraphQLContext } from "../builder"
2
3
  import { toDbStatus, toGraphqlStatus } from "./response-status"
3
4
  import type { DbStatus, GraphqlStatus } from "./response-status"
4
5
  import User from "../../models/user"
@@ -35,9 +36,9 @@ export type EnrichedDatasetEvent =
35
36
  * Get all events for a dataset
36
37
  */
37
38
  export async function datasetEvents(
38
- obj,
39
- _,
40
- { userInfo, user },
39
+ obj: { id: string },
40
+ _: unknown,
41
+ { userInfo, user }: GraphQLContext,
41
42
  ): Promise<EnrichedDatasetEvent[]> {
42
43
  const allEvents: DatasetEventDocument[] = await DatasetEvent.find({
43
44
  datasetId: obj.id,
@@ -106,9 +107,9 @@ export const DatasetEventDescriptionTypeResolvers = {
106
107
  * Create a 'contributor request' event
107
108
  */
108
109
  export async function createContributorRequestEvent(
109
- obj,
110
- { datasetId },
111
- { user },
110
+ obj: unknown,
111
+ { datasetId }: { datasetId: string },
112
+ { user }: GraphQLContext,
112
113
  ) {
113
114
  if (!user) {
114
115
  throw new Error("Authentication required to request contributor status.")
@@ -149,9 +150,9 @@ export async function createContributorRequestEvent(
149
150
  * Create or update an admin note event
150
151
  */
151
152
  export async function saveAdminNote(
152
- obj,
153
- { id, datasetId, note },
154
- { user, userInfo },
153
+ obj: unknown,
154
+ { id, datasetId, note }: { id?: string; datasetId: string; note: string },
155
+ { user, userInfo }: GraphQLContext,
155
156
  ) {
156
157
  if (!userInfo?.admin) throw new Error("Not authorized")
157
158
 
@@ -198,10 +199,7 @@ export async function processContributorRequest(
198
199
  resolutionStatus: "ACCEPTED" | "DENIED"
199
200
  reason?: string
200
201
  },
201
- { user: currentUserId, userInfo }: {
202
- user: string
203
- userInfo: { admin: boolean }
204
- },
202
+ { user: currentUserId, userInfo }: GraphQLContext,
205
203
  ) {
206
204
  if (!currentUserId) {
207
205
  throw new Error("Authentication required to process contributor requests.")
@@ -299,7 +297,11 @@ export async function processContributorRequest(
299
297
  /**
300
298
  * Update a user's notification status
301
299
  */
302
- export async function updateEventStatus(obj, { eventId, status }, { user }) {
300
+ export async function updateEventStatus(
301
+ obj: unknown,
302
+ { eventId, status }: { eventId: string; status: string },
303
+ { user }: GraphQLContext,
304
+ ) {
303
305
  if (!user) throw new Error("Authentication required.")
304
306
  return await UserNotificationStatus.findOneAndUpdate(
305
307
  { userId: user, datasetEventId: eventId },
@@ -328,7 +330,7 @@ export async function createContributorCitationEvent(
328
330
  familyName?: string
329
331
  }
330
332
  },
331
- { user }: { user: string },
333
+ { user }: GraphQLContext,
332
334
  ) {
333
335
  if (!user) throw new Error("Authentication required.")
334
336
 
@@ -404,7 +406,7 @@ export async function processContributorCitation(
404
406
  eventId: string
405
407
  status: "ACCEPTED" | "DENIED"
406
408
  },
407
- { user, userInfo }: { user: string; userInfo: { admin?: boolean } },
409
+ { user, userInfo }: GraphQLContext,
408
410
  ) {
409
411
  if (!user) throw new Error("Authentication required.")
410
412