@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openneuro/server",
3
- "version": "5.0.0",
3
+ "version": "5.1.0",
4
4
  "description": "Core service for the OpenNeuro platform.",
5
5
  "license": "MIT",
6
6
  "main": "src/server.js",
@@ -22,7 +22,10 @@
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.0.0",
25
+ "@openneuro/search": "^5.1.0",
26
+ "@pothos/core": "^4.12.0",
27
+ "@pothos/plugin-directives": "^4.3.0",
28
+ "@pothos/plugin-simple-objects": "^4.1.3",
26
29
  "@sentry/node": "^10.37.0",
27
30
  "@sentry/profiling-node": "^10.37.0",
28
31
  "base64url": "^3.0.0",
@@ -34,9 +37,9 @@
34
37
  "express": "5",
35
38
  "graphql": "16.8.1",
36
39
  "graphql-bigint": "^1.0.0",
37
- "graphql-compose": "9.0.10",
38
40
  "graphql-iso-date": "^3.6.1",
39
41
  "graphql-tools": "9.0.0",
42
+ "graphql-type-json": "^0.3.2",
40
43
  "hash-wasm": "^4.12.0",
41
44
  "immutable": "^4.3.8",
42
45
  "ioredis": "^5.6.1",
@@ -69,7 +72,7 @@
69
72
  "ts-node": "10.9.2",
70
73
  "typescript": "5.6.3",
71
74
  "underscore": "^1.8.3",
72
- "uuid": "10.0.0",
75
+ "uuid": "14.0.0",
73
76
  "xmldoc": "^1.1.0"
74
77
  },
75
78
  "devDependencies": {
@@ -92,5 +95,5 @@
92
95
  "publishConfig": {
93
96
  "access": "public"
94
97
  },
95
- "gitHead": "437b7f1a10abf79d6e49058b86cf0ddb625de684"
98
+ "gitHead": "e3444f63f43cb7e9d3ecfac51d2a42cdcc4f4e60"
96
99
  }
package/src/app.ts CHANGED
@@ -22,13 +22,12 @@ import * as jwt from "./libs/authentication/jwt"
22
22
  import * as auth from "./libs/authentication/states"
23
23
  import { sitemapHandler } from "./handlers/sitemap"
24
24
  import { setupPassportAuth } from "./libs/authentication/passport"
25
- import { redis } from "./libs/redis"
25
+ import { getRedis } from "./libs/redis"
26
26
  import { version } from "./lerna.json"
27
27
  export { Express } from "express-serve-static-core"
28
28
 
29
29
  interface OpenNeuroRequestContext {
30
30
  user: string
31
- isSuperUser: boolean
32
31
  userInfo: {
33
32
  id: string
34
33
  exp: string
@@ -68,7 +67,7 @@ export async function expressApolloSetup() {
68
67
  // Always allow introspection - our schema is public
69
68
  introspection: true,
70
69
  // @ts-expect-error Type mismatch for keyv and ioredis recent releases
71
- cache: new KeyvAdapter(new Keyv({ store: new KeyvRedis(redis) })),
70
+ cache: new KeyvAdapter(new Keyv({ store: new KeyvRedis(getRedis()) })),
72
71
  plugins: [
73
72
  ApolloServerPluginLandingPageLocalDefault(),
74
73
  ApolloServerPluginDrainHttpServer({
@@ -109,7 +108,6 @@ export async function expressApolloSetup() {
109
108
  if (req.isAuthenticated()) {
110
109
  return {
111
110
  user: req.user.id,
112
- isSuperUser: req.user.admin,
113
111
  userInfo: req.user,
114
112
  }
115
113
  }
@@ -22,6 +22,8 @@ function makeEntry(overrides: Partial<TreeEntry> = {}): TreeEntry {
22
22
  b: "",
23
23
  p: false,
24
24
  d: false,
25
+ a: false,
26
+ l: false,
25
27
  ...overrides,
26
28
  }
27
29
  }
package/src/cache/tree.ts CHANGED
@@ -19,6 +19,10 @@ export interface TreeEntry {
19
19
  p: boolean
20
20
  /** is directory */
21
21
  d: boolean
22
+ /** is annexed */
23
+ a: boolean
24
+ /** is a symlink */
25
+ l: boolean
22
26
  }
23
27
 
24
28
  function treeKey(hash: string): string {
@@ -16,4 +16,5 @@ export enum CacheType {
16
16
  brainInitiative = "brainInitiative",
17
17
  validation = "validation",
18
18
  dataciteYml = "dataciteYml",
19
+ gitRef = "ref",
19
20
  }
@@ -27,7 +27,7 @@ vi.mock("../../cache/item")
27
27
  vi.mock("../files")
28
28
  vi.mock("../../utils/datasetOrSnapshot")
29
29
  vi.mock("../libs/redis", () => ({
30
- redis: vi.fn(),
30
+ getRedis: vi.fn(),
31
31
  }))
32
32
 
33
33
  const mockYamlLoad = vi.mocked(yaml.load)
@@ -89,7 +89,7 @@ describe("checkDataRetentionNotifications", () => {
89
89
  await Deletion.create({
90
90
  datasetId: TEST_DATASET,
91
91
  reason: "test deletion",
92
- user: { _id: TEST_USER.id },
92
+ user: { id: TEST_USER.id },
93
93
  })
94
94
  mockDraft(daysAgo(15))
95
95
  mockSnapshots([{ hexsha: "other" }])
@@ -131,7 +131,7 @@ describe("checkDataRetentionNotifications", () => {
131
131
  expect(notifications.send).toHaveBeenCalledTimes(1)
132
132
  expect(notifications.send).toHaveBeenCalledWith(
133
133
  expect.objectContaining({
134
- _id: expect.stringContaining("no_snapshot_reminder"),
134
+ id: expect.stringContaining("no_snapshot_reminder"),
135
135
  }),
136
136
  )
137
137
  })
@@ -145,7 +145,7 @@ describe("checkDataRetentionNotifications", () => {
145
145
  expect(notifications.send).toHaveBeenCalledTimes(1)
146
146
  expect(notifications.send).toHaveBeenCalledWith(
147
147
  expect.objectContaining({
148
- _id: expect.stringContaining("retention_14day"),
148
+ id: expect.stringContaining("retention_14day"),
149
149
  }),
150
150
  )
151
151
  })
@@ -158,7 +158,7 @@ describe("checkDataRetentionNotifications", () => {
158
158
  expect(notifications.send).toHaveBeenCalledTimes(1)
159
159
  expect(notifications.send).toHaveBeenCalledWith(
160
160
  expect.objectContaining({
161
- _id: expect.stringContaining("retention_14day"),
161
+ id: expect.stringContaining("retention_14day"),
162
162
  }),
163
163
  )
164
164
  })
@@ -172,7 +172,7 @@ describe("checkDataRetentionNotifications", () => {
172
172
  expect(notifications.send).toHaveBeenCalledTimes(1)
173
173
  expect(notifications.send).toHaveBeenCalledWith(
174
174
  expect.objectContaining({
175
- _id: expect.stringContaining("retention_14day"),
175
+ id: expect.stringContaining("retention_14day"),
176
176
  }),
177
177
  )
178
178
 
@@ -214,7 +214,7 @@ describe("checkDataRetentionNotifications", () => {
214
214
  expect(notifications.send).toHaveBeenCalledTimes(1)
215
215
  expect(notifications.send).toHaveBeenCalledWith(
216
216
  expect.objectContaining({
217
- _id: expect.stringContaining("retention_7day"),
217
+ id: expect.stringContaining("retention_7day"),
218
218
  }),
219
219
  )
220
220
  })
@@ -235,7 +235,7 @@ describe("checkDataRetentionNotifications", () => {
235
235
  expect(notifications.send).toHaveBeenCalledTimes(1)
236
236
  expect(notifications.send).toHaveBeenCalledWith(
237
237
  expect.objectContaining({
238
- _id: expect.stringContaining("retention_deletion"),
238
+ id: expect.stringContaining("retention_deletion"),
239
239
  }),
240
240
  )
241
241
  })
@@ -274,7 +274,7 @@ describe("checkDataRetentionNotifications", () => {
274
274
  expect(notifications.send).toHaveBeenCalledTimes(1)
275
275
  expect(notifications.send).toHaveBeenCalledWith(
276
276
  expect.objectContaining({
277
- _id: expect.stringContaining("retention_14day"),
277
+ id: expect.stringContaining("retention_14day"),
278
278
  }),
279
279
  )
280
280
 
@@ -295,7 +295,7 @@ describe("checkDataRetentionNotifications", () => {
295
295
  expect(notifications.send).toHaveBeenCalledTimes(1)
296
296
  expect(notifications.send).toHaveBeenLastCalledWith(
297
297
  expect.objectContaining({
298
- _id: expect.stringContaining("retention_14day"),
298
+ id: expect.stringContaining("retention_14day"),
299
299
  }),
300
300
  )
301
301
 
@@ -312,7 +312,7 @@ describe("checkDataRetentionNotifications", () => {
312
312
  expect(notifications.send).toHaveBeenCalledTimes(1)
313
313
  expect(notifications.send).toHaveBeenLastCalledWith(
314
314
  expect.objectContaining({
315
- _id: expect.stringContaining("retention_7day"),
315
+ id: expect.stringContaining("retention_7day"),
316
316
  }),
317
317
  )
318
318
 
@@ -329,7 +329,7 @@ describe("checkDataRetentionNotifications", () => {
329
329
  expect(notifications.send).toHaveBeenCalledTimes(1)
330
330
  expect(notifications.send).toHaveBeenLastCalledWith(
331
331
  expect.objectContaining({
332
- _id: expect.stringContaining("retention_deletion"),
332
+ id: expect.stringContaining("retention_deletion"),
333
333
  }),
334
334
  )
335
335
 
@@ -13,7 +13,7 @@ import type { TreeEntry } from "../../cache/tree"
13
13
 
14
14
  vi.mock("ioredis")
15
15
  vi.mock("../../config.ts")
16
- vi.mock("../../libs/redis", () => ({ redis: {} }))
16
+ vi.mock("../../libs/redis", () => ({ getRedis: () => ({}) }))
17
17
  vi.mock("../../libs/presign", () => ({
18
18
  getPresignedUrl: vi.fn().mockResolvedValue(
19
19
  "https://s3.amazonaws.com/bucket/key?presigned=true",
@@ -74,6 +74,8 @@ describe("datalad files", () => {
74
74
  b: "",
75
75
  p: false,
76
76
  d: true,
77
+ a: false,
78
+ l: false,
77
79
  }
78
80
  const result = await entryToDatasetFile(entry, "ds000001")
79
81
  expect(result).toEqual({
@@ -82,6 +84,8 @@ describe("datalad files", () => {
82
84
  directory: true,
83
85
  size: 0,
84
86
  urls: [],
87
+ annexed: false,
88
+ symlink: false,
85
89
  })
86
90
  })
87
91
  it("returns a presigned URL for private files", async () => {
@@ -94,6 +98,8 @@ describe("datalad files", () => {
94
98
  b: "private-bucket",
95
99
  p: true,
96
100
  d: false,
101
+ a: false,
102
+ l: false,
97
103
  }
98
104
  const result = await entryToDatasetFile(entry, "ds000001")
99
105
  expect(result).toEqual({
@@ -102,6 +108,8 @@ describe("datalad files", () => {
102
108
  directory: false,
103
109
  size: 12345,
104
110
  urls: ["https://s3.amazonaws.com/bucket/key?presigned=true"],
111
+ annexed: false,
112
+ symlink: false,
105
113
  })
106
114
  })
107
115
  it("returns a public S3 URL for public files with S3 keys", async () => {
@@ -114,6 +122,8 @@ describe("datalad files", () => {
114
122
  b: "",
115
123
  p: false,
116
124
  d: false,
125
+ a: false,
126
+ l: false,
117
127
  }
118
128
  const result = await entryToDatasetFile(entry, "ds000001")
119
129
  expect(result).toEqual({
@@ -122,6 +132,8 @@ describe("datalad files", () => {
122
132
  directory: false,
123
133
  size: 500,
124
134
  urls: ["https://s3.amazonaws.com/bucket/key?versionId=abc123"],
135
+ annexed: false,
136
+ symlink: false,
125
137
  })
126
138
  })
127
139
  it("falls back to server object URL when no S3 key/version", async () => {
@@ -135,6 +147,8 @@ describe("datalad files", () => {
135
147
  b: "",
136
148
  p: false,
137
149
  d: false,
150
+ a: false,
151
+ l: false,
138
152
  }
139
153
  const result = await entryToDatasetFile(entry, "ds000001")
140
154
  expect(result).toEqual({
@@ -145,6 +159,8 @@ describe("datalad files", () => {
145
159
  urls: [
146
160
  "https://openneuro.org/crn/datasets/ds000001/objects/jkl012?filename=sub-01_T1w.nii.gz",
147
161
  ],
162
+ annexed: false,
163
+ symlink: false,
148
164
  })
149
165
  })
150
166
  })
@@ -192,6 +208,8 @@ describe("datalad files", () => {
192
208
  directory: true,
193
209
  size: 0,
194
210
  urls: [],
211
+ annexed: false,
212
+ symlink: false,
195
213
  }
196
214
  expect(workerFileToEntry(file, false)).toEqual({
197
215
  n: "sub-01",
@@ -202,6 +220,8 @@ describe("datalad files", () => {
202
220
  b: "",
203
221
  p: false,
204
222
  d: true,
223
+ a: false,
224
+ l: false,
205
225
  })
206
226
  })
207
227
  it("converts a public file with S3 URL", () => {
@@ -213,6 +233,8 @@ describe("datalad files", () => {
213
233
  urls: [
214
234
  "https://s3.amazonaws.com/openneuro.org/datasets/ds000001/README?versionId=ver2",
215
235
  ],
236
+ annexed: false,
237
+ symlink: false,
216
238
  }
217
239
  const result = workerFileToEntry(file, false)
218
240
  expect(result).toEqual({
@@ -224,6 +246,8 @@ describe("datalad files", () => {
224
246
  b: "openneuro.org",
225
247
  p: false,
226
248
  d: false,
249
+ a: false,
250
+ l: false,
227
251
  })
228
252
  })
229
253
  it("stores empty bucket when it matches the default bucket", () => {
@@ -237,6 +261,8 @@ describe("datalad files", () => {
237
261
  urls: [
238
262
  "https://s3.amazonaws.com/openneuro.org/datasets/ds000001/README?versionId=ver2",
239
263
  ],
264
+ symlink: false,
265
+ annexed: false,
240
266
  }
241
267
  const result = workerFileToEntry(file, false)
242
268
  expect(result.b).toBe("")
@@ -251,6 +277,8 @@ describe("datalad files", () => {
251
277
  urls: [
252
278
  "https://s3.amazonaws.com/private-bucket/data.nii.gz?versionId=v1",
253
279
  ],
280
+ symlink: false,
281
+ annexed: false,
254
282
  }
255
283
  expect(workerFileToEntry(file, true).p).toBe(true)
256
284
  expect(workerFileToEntry(file, false).p).toBe(false)
@@ -262,6 +290,8 @@ describe("datalad files", () => {
262
290
  directory: false,
263
291
  size: 1000,
264
292
  urls: [],
293
+ symlink: false,
294
+ annexed: false,
265
295
  }
266
296
  const result = workerFileToEntry(file, false)
267
297
  expect(result.k).toBe("")
@@ -10,12 +10,12 @@ import { connect } from "mongoose"
10
10
  // Mock requests to Datalad service
11
11
  vi.mock("superagent")
12
12
  vi.mock("../../libs/redis.js", () => ({
13
- redis: {
13
+ getRedis: () => ({
14
14
  del: vi.fn(),
15
- },
16
- redlock: {
15
+ }),
16
+ getRedlock: () => ({
17
17
  lock: vi.fn().mockImplementation(() => ({ unlock: vi.fn() })),
18
- },
18
+ }),
19
19
  }))
20
20
  // Mock draft files calls
21
21
  vi.mock("../draft.ts", () => ({
@@ -1,6 +1,6 @@
1
1
  import * as Sentry from "@sentry/node"
2
2
  import CacheItem, { CacheType } from "../cache/item"
3
- import { redis } from "../libs/redis"
3
+ import { getRedis } from "../libs/redis"
4
4
  import {
5
5
  type DatasetOrSnapshot,
6
6
  datasetOrSnapshot,
@@ -26,7 +26,7 @@ export const contributors = async (
26
26
  if (!datasetId) return []
27
27
 
28
28
  const revisionShort = revision ? revision.substring(0, 7) : "HEAD"
29
- const dataciteCache = new CacheItem(redis, CacheType.dataciteYml, [
29
+ const dataciteCache = new CacheItem(getRedis(), CacheType.dataciteYml, [
30
30
  datasetId,
31
31
  revisionShort,
32
32
  ])
@@ -18,7 +18,7 @@ const DAY = 24 * 60 * 60 * 1000
18
18
  */
19
19
  async function notifyWriteUsers(
20
20
  datasetId: string,
21
- makeEmail: (user: { _id: string; email: string; name: string }) => object,
21
+ makeEmail: (user: { id: string; email: string; name: string }) => object,
22
22
  ) {
23
23
  const permissions = await Permission.find({
24
24
  datasetId,
@@ -86,7 +86,7 @@ export async function checkDataRetentionNotifications(
86
86
  !lastSnapshot && age >= DAY && age < 14 * DAY && !record.notifiedNoSnapshot
87
87
  ) {
88
88
  await notifyWriteUsers(datasetId, (user) => ({
89
- _id: `${datasetId}_${user._id}_no_snapshot_reminder`,
89
+ id: `${datasetId}_${user.id}_no_snapshot_reminder`,
90
90
  type: "email",
91
91
  email: {
92
92
  to: user.email,
@@ -107,7 +107,7 @@ export async function checkDataRetentionNotifications(
107
107
  // Retention warnings sent in order from 14 days, 7 days, and 0 days.
108
108
  if (age >= 14 * DAY && !record.notifiedAt14Days) {
109
109
  await notifyWriteUsers(datasetId, (user) => ({
110
- _id: `${datasetId}_${user._id}_retention_14day`,
110
+ id: `${datasetId}_${user.id}_retention_14day`,
111
111
  type: "email",
112
112
  email: {
113
113
  to: user.email,
@@ -129,7 +129,7 @@ export async function checkDataRetentionNotifications(
129
129
  now.getTime() - new Date(record.notifiedAt14Days).getTime() >= 7 * DAY
130
130
  ) {
131
131
  await notifyWriteUsers(datasetId, (user) => ({
132
- _id: `${datasetId}_${user._id}_retention_7day`,
132
+ id: `${datasetId}_${user.id}_retention_7day`,
133
133
  type: "email",
134
134
  email: {
135
135
  to: user.email,
@@ -151,7 +151,7 @@ export async function checkDataRetentionNotifications(
151
151
  now.getTime() - new Date(record.notifiedAt7Days).getTime() >= 7 * DAY
152
152
  ) {
153
153
  await notifyWriteUsers(datasetId, (user) => ({
154
- _id: `${datasetId}_${user._id}_retention_deletion`,
154
+ id: `${datasetId}_${user.id}_retention_deletion`,
155
155
  type: "email",
156
156
  email: {
157
157
  to: user.email,
@@ -12,12 +12,12 @@ import type * as Mongoose from "mongoose"
12
12
  import config from "../config"
13
13
  import * as subscriptions from "../handlers/subscriptions"
14
14
  import { generateDataladCookie } from "../libs/authentication/jwt"
15
- import { redis } from "../libs/redis"
15
+ import { getRedis } from "../libs/redis"
16
16
  import CacheItem, { CacheType } from "../cache/item"
17
17
  import { getDraftRevision, updateDatasetRevision } from "./draft"
18
18
  import { encodeFilePath, filesUrl, fileUrl, getFileName } from "./files"
19
19
  import { getAccessionNumber } from "../libs/dataset"
20
- import Dataset from "../models/dataset"
20
+ import Dataset, { type DatasetDocument } from "../models/dataset"
21
21
  import Metadata from "../models/metadata"
22
22
  import Permission from "../models/permission"
23
23
  import Star from "../models/stars"
@@ -80,12 +80,16 @@ export const createDataset = async (
80
80
  }
81
81
  }
82
82
 
83
+ type DatasetWithRevision = Mongoose.FlattenMaps<DatasetDocument> & {
84
+ revision: Promise<string>
85
+ }
86
+
83
87
  /**
84
88
  * Fetch dataset document and related fields
85
89
  */
86
- export const getDataset = async (id) => {
90
+ export const getDataset = async (id): Promise<DatasetWithRevision | null> => {
87
91
  const dataset = await Dataset.findOne({ id }).lean()
88
- return {
92
+ return dataset && {
89
93
  ...dataset,
90
94
  revision: getDraftRevision(id),
91
95
  }
@@ -114,7 +118,7 @@ export const deleteDataset = async (datasetId, user) => {
114
118
  export const cacheDatasetConnection = (options) => (connectionArguments) => {
115
119
  const connection = datasetsConnection(options)
116
120
  const cache = new CacheItem(
117
- redis,
121
+ getRedis(),
118
122
  CacheType.datasetsConnection,
119
123
  [objectHash(options)],
120
124
  60,
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import config from "../config"
5
5
  import request from "superagent"
6
- import { redis } from "../libs/redis"
6
+ import { getRedis } from "../libs/redis"
7
7
  import { commitFiles } from "./dataset"
8
8
  import { fileUrl } from "./files"
9
9
  import { generateDataladCookie } from "../libs/authentication/jwt"
@@ -135,7 +135,7 @@ export const description = async (obj) => {
135
135
  Name: datasetId,
136
136
  BIDSVersion: "1.8.0",
137
137
  }
138
- const cache = new CacheItem(redis, CacheType.datasetDescription, [
138
+ const cache = new CacheItem(getRedis(), CacheType.datasetDescription, [
139
139
  datasetId,
140
140
  revision.substring(0, 7),
141
141
  ])
@@ -5,7 +5,7 @@ import request from "superagent"
5
5
  import Dataset from "../models/dataset"
6
6
  import { getDatasetWorker } from "../libs/datalad-service"
7
7
  import CacheItem, { CacheType } from "../cache/item"
8
- import { redis } from "../libs/redis"
8
+ import { getRedis } from "../libs/redis"
9
9
 
10
10
  // Draft info resolver
11
11
  type DraftInfo = {
@@ -13,7 +13,7 @@ type DraftInfo = {
13
13
  hexsha: string // Duplicate of ref for backwards compatibility
14
14
  tree: string
15
15
  message: string
16
- modified: Date
16
+ modified: string
17
17
  }
18
18
 
19
19
  export const getDraftRevision = async (datasetId): Promise<string> => {
@@ -22,7 +22,12 @@ export const getDraftRevision = async (datasetId): Promise<string> => {
22
22
  }
23
23
 
24
24
  export const getDraftInfo = async (datasetId) => {
25
- const cache = new CacheItem(redis, CacheType.draftRevision, [datasetId], 10)
25
+ const cache = new CacheItem(
26
+ getRedis(),
27
+ CacheType.draftRevision,
28
+ [datasetId],
29
+ 10,
30
+ )
26
31
  return cache.get(async (_doNotCache): Promise<DraftInfo> => {
27
32
  const draftUrl = `http://${
28
33
  getDatasetWorker(