@openneuro/server 5.0.0 → 5.1.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.
Files changed (98) 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__/files.spec.ts +31 -1
  9. package/src/datalad/__tests__/snapshots.spec.ts +4 -4
  10. package/src/datalad/contributors.ts +2 -2
  11. package/src/datalad/dataRetentionNotifications.ts +5 -5
  12. package/src/datalad/dataset.ts +9 -5
  13. package/src/datalad/description.ts +2 -2
  14. package/src/datalad/draft.ts +8 -3
  15. package/src/datalad/files.ts +71 -12
  16. package/src/datalad/readme.ts +2 -2
  17. package/src/datalad/snapshots.ts +19 -14
  18. package/src/elasticsearch/elastic-client.ts +11 -6
  19. package/src/elasticsearch/reindex-dataset.ts +8 -4
  20. package/src/graphql/__tests__/comment.spec.ts +3 -2
  21. package/src/graphql/__tests__/schema.spec.ts +28 -0
  22. package/src/graphql/builder.ts +42 -0
  23. package/src/graphql/resolvers/__tests__/dataset.spec.ts +6 -119
  24. package/src/graphql/resolvers/__tests__/importRemoteDataset.spec.ts +2 -1
  25. package/src/graphql/resolvers/__tests__/permssions.spec.ts +3 -2
  26. package/src/graphql/resolvers/__tests__/user.spec.ts +39 -11
  27. package/src/graphql/resolvers/brainInitiative.ts +4 -3
  28. package/src/graphql/resolvers/cache.ts +7 -6
  29. package/src/graphql/resolvers/comment.ts +35 -19
  30. package/src/graphql/resolvers/dataset-search.ts +7 -6
  31. package/src/graphql/resolvers/dataset.ts +77 -45
  32. package/src/graphql/resolvers/datasetEvents.ts +18 -16
  33. package/src/graphql/resolvers/description.ts +2 -1
  34. package/src/graphql/resolvers/draft.ts +14 -3
  35. package/src/graphql/resolvers/fileCheck.ts +5 -4
  36. package/src/graphql/resolvers/flaggedFiles.ts +2 -1
  37. package/src/graphql/resolvers/follow.ts +2 -1
  38. package/src/graphql/resolvers/git.ts +2 -1
  39. package/src/graphql/resolvers/gitEvents.ts +2 -1
  40. package/src/graphql/resolvers/history.ts +3 -1
  41. package/src/graphql/resolvers/holdDeletion.ts +1 -1
  42. package/src/graphql/resolvers/importRemoteDataset.ts +3 -2
  43. package/src/graphql/resolvers/issues.ts +2 -1
  44. package/src/graphql/resolvers/metadata.ts +3 -2
  45. package/src/graphql/resolvers/permissions.ts +26 -6
  46. package/src/graphql/resolvers/publish.ts +2 -1
  47. package/src/graphql/resolvers/readme.ts +2 -1
  48. package/src/graphql/resolvers/reexporter.ts +2 -1
  49. package/src/graphql/resolvers/relation.ts +3 -2
  50. package/src/graphql/resolvers/reset.ts +2 -1
  51. package/src/graphql/resolvers/reviewer.ts +14 -6
  52. package/src/graphql/resolvers/snapshots.ts +42 -10
  53. package/src/graphql/resolvers/stars.ts +2 -1
  54. package/src/graphql/resolvers/upload.ts +3 -2
  55. package/src/graphql/resolvers/user.ts +44 -32
  56. package/src/graphql/resolvers/validation.ts +6 -5
  57. package/src/graphql/resolvers/worker.ts +2 -1
  58. package/src/graphql/schema/analytics.ts +12 -0
  59. package/src/graphql/schema/comment.ts +30 -0
  60. package/src/graphql/schema/dataset-events.ts +91 -0
  61. package/src/graphql/schema/dataset-search.ts +32 -0
  62. package/src/graphql/schema/dataset.ts +167 -0
  63. package/src/graphql/schema/description.ts +44 -0
  64. package/src/graphql/schema/draft.ts +80 -0
  65. package/src/graphql/schema/enums.ts +55 -0
  66. package/src/graphql/schema/files.ts +44 -0
  67. package/src/graphql/schema/inputs.ts +231 -0
  68. package/src/graphql/schema/metadata.ts +86 -0
  69. package/src/graphql/schema/misc.ts +154 -0
  70. package/src/graphql/schema/mutation.ts +549 -0
  71. package/src/graphql/schema/pagination.ts +12 -0
  72. package/src/graphql/schema/permissions.ts +21 -0
  73. package/src/graphql/schema/query.ts +119 -0
  74. package/src/graphql/schema/refs.ts +23 -0
  75. package/src/graphql/schema/reviewer.ts +11 -0
  76. package/src/graphql/schema/scalars.ts +9 -0
  77. package/src/graphql/schema/snapshot.ts +111 -0
  78. package/src/graphql/schema/upload.ts +13 -0
  79. package/src/graphql/schema/user.ts +61 -0
  80. package/src/graphql/schema/validation.ts +70 -0
  81. package/src/graphql/schema/worker.ts +16 -0
  82. package/src/graphql/schema.ts +29 -1114
  83. package/src/handlers/subscriptions.ts +1 -1
  84. package/src/libs/apikey.ts +1 -1
  85. package/src/libs/authentication/crypto.ts +14 -9
  86. package/src/libs/notifications.ts +1 -1
  87. package/src/libs/presign.ts +32 -20
  88. package/src/libs/redis.ts +12 -2
  89. package/src/models/comment.ts +2 -4
  90. package/src/models/counter.ts +2 -2
  91. package/src/models/ingestDataset.ts +1 -2
  92. package/src/models/notification.ts +0 -2
  93. package/src/models/subscription.ts +0 -1
  94. package/src/models/user.ts +1 -2
  95. package/src/models/userMigration.ts +1 -1
  96. package/src/models/userNotificationStatus.ts +0 -1
  97. package/src/utils/__tests__/snapshots.spec.ts +120 -0
  98. package/src/utils/snapshots.ts +12 -0
@@ -64,7 +64,7 @@ export const deleteAll = (req, res, next) => {
64
64
  .then((subscriptions) => {
65
65
  subscriptions.forEach((subscription) => {
66
66
  Subscription.deleteOne({
67
- _id: new ObjectID(subscription._id),
67
+ _id: new ObjectID(subscription._id.toString()),
68
68
  })
69
69
  })
70
70
  return res.send()
@@ -20,7 +20,7 @@ export const generateApiKey = (user) => {
20
20
  id: userId,
21
21
  hash: token,
22
22
  },
23
- { upsert: true, new: true },
23
+ { upsert: true },
24
24
  )
25
25
  .exec()
26
26
  .then(() => ({ key: token }))
@@ -1,17 +1,22 @@
1
1
  import crypto from "crypto"
2
2
  import config from "../../config"
3
3
 
4
- const secret = config.auth.jwt.secret
5
4
  const algorithm = "aes256"
6
- const key = crypto
7
- .createHash("sha256")
8
- .update(secret)
9
- .digest("base64")
10
- .substr(0, 32)
11
-
12
5
  const delimiter = "."
13
6
  const encoding = "base64"
14
7
 
8
+ let _key: string | null = null
9
+ function getKey(): string {
10
+ if (!_key) {
11
+ _key = crypto
12
+ .createHash("sha256")
13
+ .update(config.auth.jwt.secret)
14
+ .digest("base64")
15
+ .substr(0, 32)
16
+ }
17
+ return _key
18
+ }
19
+
15
20
  const pack = (iv, encrypted) => iv.toString(encoding) + delimiter + encrypted
16
21
 
17
22
  const unpack = (encryptedPackage) => {
@@ -22,7 +27,7 @@ const unpack = (encryptedPackage) => {
22
27
 
23
28
  export const encrypt = (plainText) => {
24
29
  const iv = crypto.randomBytes(16)
25
- const cipher = crypto.createCipheriv(algorithm, key, iv)
30
+ const cipher = crypto.createCipheriv(algorithm, getKey(), iv)
26
31
  const encryptedText = cipher.update(plainText, "utf8", "hex") +
27
32
  cipher.final("hex")
28
33
  const encryptedPackage = pack(iv, encryptedText)
@@ -31,7 +36,7 @@ export const encrypt = (plainText) => {
31
36
 
32
37
  export const decrypt = (encryptedPackage) => {
33
38
  const [iv, encryptedText] = unpack(encryptedPackage)
34
- const decipher = crypto.createDecipheriv(algorithm, key, iv)
39
+ const decipher = crypto.createDecipheriv(algorithm, getKey(), iv)
35
40
  const decryptedText = decipher.update(encryptedText, "hex", "utf8") +
36
41
  decipher.final("utf8")
37
42
  return decryptedText
@@ -141,7 +141,7 @@ const notifications = {
141
141
  from: "reply-" +
142
142
  encodeURIComponent(comment._id) +
143
143
  "-" +
144
- encodeURIComponent(user._id),
144
+ encodeURIComponent(user._id.toString()),
145
145
  subject: "Comment Created",
146
146
  html: commentCreated({
147
147
  name: user.name,
@@ -82,7 +82,7 @@ export async function getPresignedUrl(
82
82
  }
83
83
 
84
84
  /**
85
- * Bulk-resolve presigned URLs for many files in two pipelined Redis calls.
85
+ * Bulk-resolve presigned URLs for many files in pipelined Redis calls.
86
86
  * Returns an array of resolved URLs matching the input order.
87
87
  */
88
88
  export async function getPresignedUrlsBulk(
@@ -96,29 +96,41 @@ export async function getPresignedUrlsBulk(
96
96
  bucket: resolveBucket(item.bucket),
97
97
  }))
98
98
  const keys = resolved.map((r) => presignKey(r.bucket, r.s3Key, r.versionId))
99
- const cached = await redis.mget(...keys)
100
99
 
101
- // Fill hits from cache, sign misses and queue them for write-back
102
100
  const hmac = await getHMAC()
103
101
  const expires = Math.floor(Date.now() / 1000) + PRESIGN_EXPIRATION
104
- const writePipeline = redis.pipeline()
105
- let misses = 0
106
-
107
- const results = cached.map((val, i) => {
108
- if (val) return val
109
- misses++
110
- const url = presignV2(
111
- hmac,
112
- resolved[i].bucket,
113
- resolved[i].s3Key,
114
- resolved[i].versionId,
115
- expires,
116
- )
117
- writePipeline.setex(keys[i], PRESIGN_TTL, url)
118
- return url
119
- })
102
+ const results: string[] = []
103
+ const CHUNK_SIZE = 1000
104
+
105
+ // Process in chunks to avoid blocking Redis
106
+ for (let i = 0; i < keys.length; i += CHUNK_SIZE) {
107
+ const chunkKeys = keys.slice(i, i + CHUNK_SIZE)
108
+ const chunkResolved = resolved.slice(i, i + CHUNK_SIZE)
109
+ const cached = await redis.mget(chunkKeys)
110
+ const writePipeline = redis.pipeline()
111
+ let misses = 0
112
+
113
+ for (let j = 0; j < chunkKeys.length; j++) {
114
+ let val = cached[j]
115
+ if (!val) {
116
+ misses++
117
+ val = presignV2(
118
+ hmac,
119
+ chunkResolved[j].bucket,
120
+ chunkResolved[j].s3Key,
121
+ chunkResolved[j].versionId,
122
+ expires,
123
+ )
124
+ writePipeline.setex(chunkKeys[j], PRESIGN_TTL, val)
125
+ }
126
+ results.push(val)
127
+ }
128
+
129
+ if (misses > 0) {
130
+ await writePipeline.exec()
131
+ }
132
+ }
120
133
 
121
- if (misses > 0) await writePipeline.exec()
122
134
  return results
123
135
  }
124
136
 
package/src/libs/redis.ts CHANGED
@@ -5,5 +5,15 @@ import Redis from "ioredis"
5
5
  import Redlock from "redlock"
6
6
  import config from "../config"
7
7
 
8
- export const redis = new Redis(config.redis)
9
- export const redlock = new Redlock([redis])
8
+ let _redis: Redis | null = null
9
+ let _redlock: Redlock | null = null
10
+
11
+ export function getRedis(): Redis {
12
+ if (!_redis) _redis = new Redis(config.redis)
13
+ return _redis
14
+ }
15
+
16
+ export function getRedlock(): Redlock {
17
+ if (!_redlock) _redlock = new Redlock([getRedis()])
18
+ return _redlock
19
+ }
@@ -1,15 +1,13 @@
1
1
  import mongoose from "mongoose"
2
2
  import type { Document } from "mongoose"
3
+ import type { UserDocument } from "./user"
3
4
  const { Schema, model } = mongoose
4
5
 
5
6
  export interface CommentDocument extends Document {
6
- _id: string
7
7
  createDate: Date
8
8
  datasetId: string
9
9
  datasetLabel: string
10
- user: {
11
- _id: string
12
- }
10
+ user: UserDocument
13
11
  parentId: string
14
12
  text: string
15
13
  }
@@ -2,12 +2,12 @@ import mongoose from "mongoose"
2
2
  import type { Document } from "mongoose"
3
3
  const { Schema, model } = mongoose
4
4
 
5
- export interface CounterDocument extends Document {
5
+ export interface CounterDocument extends Omit<Document, "_id"> {
6
6
  _id: string
7
7
  sequence_value: number
8
8
  }
9
9
  const countersSchema = new Schema({
10
- _id: { type: String },
10
+ _id: { type: String, required: true },
11
11
  sequence_value: { type: Number, default: 0 },
12
12
  })
13
13
 
@@ -2,12 +2,11 @@
2
2
  * Model for ingest of new datasets from a remote URL (zip/tarball)
3
3
  */
4
4
  import mongoose from "mongoose"
5
- import type { Document, ObjectId } from "mongoose"
5
+ import type { Document } from "mongoose"
6
6
  const { Schema, model } = mongoose
7
7
  import { validateUrl } from "../utils/validateUrl"
8
8
 
9
9
  export interface IngestDatasetDocument extends Document {
10
- _id: ObjectId
11
10
  datasetId: string
12
11
  userId: string
13
12
  url: string
@@ -3,7 +3,6 @@ import type { Document } from "mongoose"
3
3
  const { Schema, model } = mongoose
4
4
 
5
5
  export interface NotificationDocument extends Document {
6
- _id: string
7
6
  type: string
8
7
  email: {
9
8
  to: string
@@ -38,7 +37,6 @@ export interface NotificationDocument extends Document {
38
37
  }
39
38
 
40
39
  const notificationSchema = new Schema({
41
- _id: String,
42
40
  type: String,
43
41
  email: {
44
42
  to: String,
@@ -3,7 +3,6 @@ import type { Document } from "mongoose"
3
3
  const { Schema, model } = mongoose
4
4
 
5
5
  export interface SubscriptionDocument extends Document {
6
- _id: string
7
6
  datasetId: string
8
7
  userId: string
9
8
  }
@@ -4,7 +4,6 @@ import type { Document } from "mongoose"
4
4
  const { Schema, model } = mongoose
5
5
 
6
6
  export interface UserDocument extends Document {
7
- _id: string
8
7
  // OpenNeuro specific user uuid
9
8
  id: string
10
9
  // Best contact email for the user (notifications)
@@ -12,7 +11,7 @@ export interface UserDocument extends Document {
12
11
  // User's preferred name (visible)
13
12
  name: string
14
13
  // Login provider
15
- provider: StaticRangeInit
14
+ provider: string
16
15
  // The id from the login provider
17
16
  providerId: string
18
17
  // ORCID iD associated with this OpenNeuro user
@@ -4,7 +4,7 @@ import type { Document } from "mongoose"
4
4
  const { Schema, model } = mongoose
5
5
 
6
6
  export interface UserMigrationDocument extends Document {
7
- _id: string
7
+ id: string
8
8
  orcid: string
9
9
  google: string
10
10
  users: object[]
@@ -5,7 +5,6 @@ const { Schema, model } = mongoose
5
5
  export type NotificationStatusType = "UNREAD" | "SAVED" | "ARCHIVED"
6
6
 
7
7
  export interface UserNotificationStatusDocument extends Document {
8
- _id: string
9
8
  userId: string
10
9
  datasetEventId: string
11
10
  status: NotificationStatusType
@@ -0,0 +1,120 @@
1
+ import { snapshotCreationComparison } from "../snapshots"
2
+
3
+ describe("snapshotCreationComparison()", () => {
4
+ it('sorts array of objects by the "created" and "tag" properties', () => {
5
+ const testArray = [
6
+ { id: 2, created: new Date("2018-11-20T00:05:43.473Z"), tag: "1.0.0" },
7
+ { id: 1, created: new Date("2018-11-19T00:05:43.473Z"), tag: "1.0.1" },
8
+ { id: 3, created: new Date("2018-11-23T00:05:43.473Z"), tag: "1.0.2" },
9
+ { id: 5, created: new Date("2018-11-23T00:05:43.473Z"), tag: "1.0.10" },
10
+ { id: 4, created: new Date("2018-11-23T00:05:43.473Z"), tag: "1.0.3" },
11
+ ]
12
+ const sorted = testArray.sort(snapshotCreationComparison)
13
+ expect(sorted[0].id).toBe(2)
14
+ expect(sorted[1].id).toBe(1)
15
+ expect(sorted[2].id).toBe(3)
16
+ expect(sorted[3].id).toBe(4)
17
+ expect(sorted[4].id).toBe(5)
18
+ })
19
+ it('sorts array of objects by the "created" property as strings', () => {
20
+ const testArray = [
21
+ { id: 2, created: "2018-11-20T00:05:43.473Z", tag: "2.0.0" },
22
+ { id: 1, created: "2018-11-19T00:05:43.473Z", tag: "1.0.0" },
23
+ { id: 3, created: "2018-11-23T00:05:43.473Z", tag: "3.0.0" },
24
+ ]
25
+ const sorted = testArray.sort(snapshotCreationComparison)
26
+ expect(sorted[0].id).toBe(1)
27
+ expect(sorted[1].id).toBe(2)
28
+ expect(sorted[2].id).toBe(3)
29
+ })
30
+ it("sorts non-semver tags mixed with semver tags", () => {
31
+ const testArray = [
32
+ { id: 2, created: new Date("2018-11-19T00:05:43.473Z"), tag: "1.0.2" },
33
+ {
34
+ id: 1,
35
+ created: new Date("2018-11-19T00:05:43.473Z"),
36
+ tag: "57fed018cce88d000ac1757f",
37
+ },
38
+ { id: 3, created: new Date("2018-11-19T00:05:43.473Z"), tag: "1.0.1" },
39
+ ]
40
+ const sorted = testArray.sort(snapshotCreationComparison)
41
+ expect(sorted[0].id).toBe(2)
42
+ expect(sorted[1].id).toBe(1)
43
+ expect(sorted[2].id).toBe(3)
44
+ })
45
+ it("sorts snapshots with only non-semver tags", () => {
46
+ const testArray = [
47
+ {
48
+ id: 2,
49
+ created: new Date("2018-11-19T00:05:43.473Z"),
50
+ tag: "00001",
51
+ },
52
+ {
53
+ id: 1,
54
+ created: new Date("2018-11-19T00:05:43.473Z"),
55
+ tag: "57fed018cce88d000ac1757f",
56
+ },
57
+ {
58
+ id: 3,
59
+ created: new Date("2018-11-19T00:05:43.473Z"),
60
+ tag: "57fed018cce88d000ac1757f",
61
+ },
62
+ ]
63
+ const sorted = testArray.sort(snapshotCreationComparison)
64
+ expect(sorted[0].id).toBe(2)
65
+ expect(sorted[1].id).toBe(1)
66
+ expect(sorted[2].id).toBe(3)
67
+ })
68
+ it("sorts very similar creation times by semver order", () => {
69
+ const testSnapshots = [
70
+ {
71
+ id: "ds002680:1.0.0",
72
+ created: "2020-04-03T23:19:56.000Z",
73
+ tag: "1.0.0",
74
+ },
75
+ {
76
+ id: "ds002680:1.2.0",
77
+ created: "2021-10-19T16:26:43.000Z",
78
+ tag: "1.2.0",
79
+ },
80
+ {
81
+ id: "ds002680:1.1.0",
82
+ created: "2021-10-19T16:26:44.000Z",
83
+ tag: "1.1.0",
84
+ },
85
+ ]
86
+ const sorted = testSnapshots.sort(snapshotCreationComparison)
87
+ expect(sorted[0].id).toBe("ds002680:1.0.0")
88
+ expect(sorted[1].id).toBe("ds002680:1.1.0")
89
+ expect(sorted[2].id).toBe("ds002680:1.2.0")
90
+ })
91
+ it("sorts 000002 (legacy snapshots) before 1.0.1 (current format)", () => {
92
+ const testSnapshots = [
93
+ {
94
+ id: "ds000247:00002",
95
+ created: "2018-07-18T02:27:39.000Z",
96
+ tag: "00002",
97
+ },
98
+ {
99
+ id: "ds000247:00001",
100
+ created: "2018-07-18T02:35:37.000Z",
101
+ tag: "00001",
102
+ },
103
+ {
104
+ id: "ds000247:1.0.0",
105
+ created: "2021-07-05T15:58:18.000Z",
106
+ tag: "1.0.0",
107
+ },
108
+ {
109
+ id: "ds000247:1.0.1",
110
+ created: "2021-08-25T23:37:53.000Z",
111
+ tag: "1.0.1",
112
+ },
113
+ ]
114
+ const sorted = testSnapshots.sort(snapshotCreationComparison)
115
+ expect(sorted[0].id).toBe("ds000247:00002")
116
+ expect(sorted[1].id).toBe("ds000247:00001")
117
+ expect(sorted[2].id).toBe("ds000247:1.0.0")
118
+ expect(sorted[3].id).toBe("ds000247:1.0.1")
119
+ })
120
+ })
@@ -0,0 +1,12 @@
1
+ import semver from "semver"
2
+
3
+ export const snapshotCreationComparison = (
4
+ { created: a, tag: a_tag }: { created: Date | string; tag: string },
5
+ { created: b, tag: b_tag }: { created: Date | string; tag: string },
6
+ ) => {
7
+ if (semver.valid(a_tag) && semver.valid(b_tag)) {
8
+ return semver.compare(a_tag, b_tag)
9
+ } else {
10
+ return new Date(a).getTime() - new Date(b).getTime()
11
+ }
12
+ }