@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
@@ -1,4 +1,4 @@
1
- import { redis } from "../libs/redis"
1
+ import { getRedis } from "../libs/redis"
2
2
  import { getDatasetWorker } from "../libs/datalad-service"
3
3
  import {
4
4
  getPresignedUrl,
@@ -15,7 +15,9 @@ import {
15
15
  setTree,
16
16
  type TreeEntry,
17
17
  } from "../cache/tree"
18
+ import CacheItem, { CacheType } from "../cache/item"
18
19
  import { join } from "node:path"
20
+ import { captureException } from "@sentry/node"
19
21
 
20
22
  /**
21
23
  * Convert to URL compatible path
@@ -86,6 +88,8 @@ export type DatasetFile = {
86
88
  directory: boolean
87
89
  size: number
88
90
  urls: string[]
91
+ symlink: boolean
92
+ annexed: boolean
89
93
  }
90
94
 
91
95
  /**
@@ -140,6 +144,8 @@ export function workerFileToEntry(
140
144
  b: "",
141
145
  p: false,
142
146
  d: true,
147
+ a: false,
148
+ l: false,
143
149
  }
144
150
  }
145
151
  const parsed = file.urls[0] ? parseS3Url(file.urls[0]) : null
@@ -155,6 +161,8 @@ export function workerFileToEntry(
155
161
  b: bucket,
156
162
  p: needsPresign,
157
163
  d: false,
164
+ a: file.annexed,
165
+ l: file.symlink,
158
166
  }
159
167
  }
160
168
 
@@ -170,11 +178,13 @@ export async function entryToDatasetFile(
170
178
  directory: true,
171
179
  size: 0,
172
180
  urls: [],
181
+ symlink: false,
182
+ annexed: false,
173
183
  }
174
184
  }
175
185
  let url: string
176
186
  if (entry.p && entry.k && entry.v) {
177
- url = await getPresignedUrl(redis, entry.b, entry.k, entry.v)
187
+ url = await getPresignedUrl(getRedis(), entry.b, entry.k, entry.v)
178
188
  } else if (entry.k && entry.v) {
179
189
  url = publicS3Url(entry.b, entry.k, entry.v)
180
190
  } else {
@@ -189,6 +199,8 @@ export async function entryToDatasetFile(
189
199
  directory: false,
190
200
  size: entry.s,
191
201
  urls: [url],
202
+ symlink: entry.l,
203
+ annexed: entry.a,
192
204
  }
193
205
  }
194
206
 
@@ -248,19 +260,53 @@ async function cacheWorkerTrees(
248
260
  (f) => f.directory || f.urls[0]?.includes("s3.amazonaws.com"),
249
261
  )
250
262
  if (allExported) {
251
- void setTree(redis, hash, entries)
263
+ void setTree(getRedis(), hash, entries)
252
264
  permanentHashes.push(hash)
253
265
  } else {
254
- void setTree(redis, hash, entries, 600)
266
+ void setTree(getRedis(), hash, entries, 600)
255
267
  }
256
268
  }
257
269
  }
258
270
  if (permanentHashes.length > 0) {
259
- void addDatasetTrees(redis, datasetId, permanentHashes)
271
+ void addDatasetTrees(getRedis(), datasetId, permanentHashes)
260
272
  }
261
273
  return result
262
274
  }
263
275
 
276
+ /**
277
+ * Resolve a git reference (branch, tag, or short commit hash) to a full commit hash.
278
+ * @param datasetId The dataset ID.
279
+ * @param treeish The git reference to resolve.
280
+ * @returns The full commit hash.
281
+ */
282
+ export const resolveGitRef = async (
283
+ datasetId: string,
284
+ treeish: string,
285
+ ): Promise<string> => {
286
+ const cache = new CacheItem(getRedis(), CacheType.gitRef, [
287
+ datasetId,
288
+ treeish,
289
+ ])
290
+ return cache.get(async () => {
291
+ const url = `http://${
292
+ getDatasetWorker(datasetId)
293
+ }/datasets/${datasetId}/refs/${treeish}`
294
+ const response = await fetch(url)
295
+ if (!response.ok) {
296
+ throw new Error(
297
+ `Failed to resolve git reference ${treeish}: ${response.statusText}`,
298
+ )
299
+ }
300
+ const data = await response.json()
301
+ if (!data.hash) {
302
+ throw new Error(
303
+ `Invalid response from datalad worker for git reference ${treeish}`,
304
+ )
305
+ }
306
+ return data.hash
307
+ })
308
+ }
309
+
264
310
  /**
265
311
  * Get files for a specific revision (tree hash or commit hash).
266
312
  * Uses content-addressed caching keyed by full git hash.
@@ -269,8 +315,19 @@ export const getFiles = async (
269
315
  datasetId: string,
270
316
  treeish: string,
271
317
  ): Promise<DatasetFile[]> => {
318
+ // Guard against requests without a full git hash (40 = SHA-1, 64 = SHA-256)
319
+ if (treeish.length !== 40 && treeish.length !== 64) {
320
+ try {
321
+ treeish = await resolveGitRef(datasetId, treeish)
322
+ } catch (error) {
323
+ captureException(error, {
324
+ tags: { datasetId, treeish, source: "resolveGitRef" },
325
+ })
326
+ throw new Error(`Invalid git reference: ${treeish}`, { cause: error })
327
+ }
328
+ }
272
329
  // Try cache first
273
- const cached = await getTree(redis, treeish)
330
+ const cached = await getTree(getRedis(), treeish)
274
331
  if (cached) {
275
332
  return entriesToDatasetFiles(cached, datasetId)
276
333
  }
@@ -300,10 +357,10 @@ export async function getFilesRecursive(
300
357
  ): Promise<DatasetFile[]> {
301
358
  const needsPresign = await datasetNeedsPresign(datasetId)
302
359
  // Check for cached commit-to-trees mapping
303
- const cachedTreeHashes = await getCommitTrees(redis, tree)
360
+ const cachedTreeHashes = await getCommitTrees(getRedis(), tree)
304
361
  if (cachedTreeHashes) {
305
362
  // Bulk-fetch all trees in one pipeline
306
- const treesMap = await getTreesBulk(redis, cachedTreeHashes)
363
+ const treesMap = await getTreesBulk(getRedis(), cachedTreeHashes)
307
364
  if (treesMap.size < cachedTreeHashes.length) {
308
365
  // Batch-fetch all missing trees from the worker in one request
309
366
  const missingHashes = cachedTreeHashes.filter((h) => !treesMap.has(h))
@@ -327,7 +384,7 @@ export async function getFilesRecursive(
327
384
 
328
385
  while (pendingHashes.length > 0) {
329
386
  // Check cache for all pending hashes
330
- const cached = await getTreesBulk(redis, pendingHashes)
387
+ const cached = await getTreesBulk(getRedis(), pendingHashes)
331
388
  const uncached = pendingHashes.filter((h) => !cached.has(h))
332
389
 
333
390
  // Fetch all uncached trees in one worker request
@@ -363,8 +420,8 @@ export async function getFilesRecursive(
363
420
  // Cache the commit-to-trees mapping for next time
364
421
  if (collectedHashes.size > 0) {
365
422
  const hashArray = [...collectedHashes]
366
- void setCommitTrees(redis, tree, hashArray)
367
- void addDatasetTrees(redis, datasetId, hashArray)
423
+ void setCommitTrees(getRedis(), tree, hashArray)
424
+ void addDatasetTrees(getRedis(), datasetId, hashArray)
368
425
  }
369
426
 
370
427
  return reconstructFromTrees(treesMap, tree, path, datasetId)
@@ -409,6 +466,8 @@ async function reconstructFromTrees(
409
466
  directory: false,
410
467
  size: entry.s,
411
468
  urls: [],
469
+ symlink: entry.l,
470
+ annexed: entry.a,
412
471
  }
413
472
  if (entry.p && entry.k && entry.v) {
414
473
  // To be presigned
@@ -429,7 +488,7 @@ async function reconstructFromTrees(
429
488
  // Bulk-resolve presigned URLs in minimal Redis requests
430
489
  if (presignIndices.length > 0) {
431
490
  const urls = await getPresignedUrlsBulk(
432
- redis,
491
+ getRedis(),
433
492
  presignIndices.map((i) => ({
434
493
  bucket: fileEntries[i].entry.b,
435
494
  s3Key: fileEntries[i].entry.k,
@@ -1,5 +1,5 @@
1
1
  import { addFileString, commitFiles } from "./dataset"
2
- import { redis } from "../libs/redis"
2
+ import { getRedis } from "../libs/redis"
3
3
  import CacheItem, { CacheType } from "../cache/item"
4
4
  import { getDatasetWorker } from "../libs/datalad-service"
5
5
  import { datasetOrSnapshot } from "../utils/datasetOrSnapshot"
@@ -14,7 +14,7 @@ export const readmeUrl = (datasetId, revision) => {
14
14
 
15
15
  export const readme = (obj) => {
16
16
  const { datasetId, revision } = datasetOrSnapshot(obj)
17
- const cache = new CacheItem(redis, CacheType.readme, [
17
+ const cache = new CacheItem(getRedis(), CacheType.readme, [
18
18
  datasetId,
19
19
  revision.substring(0, 7),
20
20
  ])
@@ -3,13 +3,10 @@
3
3
  */
4
4
  import * as Sentry from "@sentry/node"
5
5
  import request from "superagent"
6
- import { redis, redlock } from "../libs/redis"
6
+ import { getRedis, getRedlock } from "../libs/redis"
7
7
  import CacheItem, { CacheType } from "../cache/item"
8
8
  import config from "../config"
9
- import {
10
- snapshotCreationComparison,
11
- updateDatasetName,
12
- } from "../graphql/resolvers/dataset"
9
+ import { snapshotCreationComparison } from "../utils/snapshots"
13
10
  import { createDraftDoi } from "../libs/doi/index"
14
11
  import { assembleMetadata } from "../libs/doi/metadata"
15
12
  import Doi from "../models/doi"
@@ -25,7 +22,7 @@ import { createEvent, updateEvent } from "../libs/events"
25
22
  import { queueIndexDataset } from "../queues/producer-methods"
26
23
 
27
24
  const lockSnapshot = (datasetId, tag) => {
28
- return redlock.lock(
25
+ return getRedlock().lock(
29
26
  `openneuro:create-snapshot-lock:${datasetId}:${tag}`,
30
27
  1800000,
31
28
  )
@@ -76,7 +73,7 @@ const createIfNotExistsDoi = async (
76
73
  Sentry.captureException(err)
77
74
  // eslint-disable-next-line no-console
78
75
  console.error(err)
79
- throw new Error(`DOI minting failed: ${err.message}`)
76
+ throw new Error(`DOI minting failed: ${err.message}`, { cause: err })
80
77
  }
81
78
  }
82
79
 
@@ -110,7 +107,12 @@ const postSnapshot = async (
110
107
  export const getSnapshots = async (datasetId): Promise<SnapshotDocument[]> => {
111
108
  const dataset = await Dataset.findOne({ id: datasetId })
112
109
  if (!dataset) return null
113
- const cache = new CacheItem(redis, CacheType.snapshot, [datasetId], 432000)
110
+ const cache = new CacheItem(
111
+ getRedis(),
112
+ CacheType.snapshot,
113
+ [datasetId],
114
+ 432000,
115
+ )
114
116
  return cache.get(() => {
115
117
  const url = `${getDatasetWorker(datasetId)}/datasets/${datasetId}/snapshots`
116
118
  return request
@@ -144,7 +146,7 @@ export const createSnapshot = async (
144
146
  descriptionFieldUpdates = {},
145
147
  snapshotChanges = [],
146
148
  ) => {
147
- const snapshotCache = new CacheItem(redis, CacheType.snapshot, [
149
+ const snapshotCache = new CacheItem(getRedis(), CacheType.snapshot, [
148
150
  datasetId,
149
151
  tag,
150
152
  ])
@@ -182,10 +184,13 @@ export const createSnapshot = async (
182
184
  createSnapshotMetadata(datasetId, tag, snapshot.hexsha, snapshot.created),
183
185
 
184
186
  // Trigger an async update for the name field (cache for sorting)
185
- updateDatasetName(datasetId),
187
+ // Dynamic import breaks circular dependency: datalad/snapshots → resolvers/dataset
188
+ import("../graphql/resolvers/dataset").then((m) =>
189
+ m.updateDatasetName(datasetId)
190
+ ),
186
191
  ])
187
192
 
188
- const snapshotListCache = new CacheItem(redis, CacheType.snapshot, [
193
+ const snapshotListCache = new CacheItem(getRedis(), CacheType.snapshot, [
189
194
  datasetId,
190
195
  ])
191
196
  await snapshotListCache.drop()
@@ -215,12 +220,12 @@ export const deleteSnapshot = (datasetId, tag) => {
215
220
  )
216
221
  }/datasets/${datasetId}/snapshots/${tag}`
217
222
  return request.del(url).then(async ({ body }) => {
218
- const snapshotCache = new CacheItem(redis, CacheType.snapshot, [
223
+ const snapshotCache = new CacheItem(getRedis(), CacheType.snapshot, [
219
224
  datasetId,
220
225
  tag,
221
226
  ])
222
227
  await snapshotCache.drop()
223
- const snapshotListCache = new CacheItem(redis, CacheType.snapshot, [
228
+ const snapshotListCache = new CacheItem(getRedis(), CacheType.snapshot, [
224
229
  datasetId,
225
230
  ])
226
231
  await snapshotListCache.drop()
@@ -244,7 +249,7 @@ export const getSnapshot = (
244
249
  )
245
250
  }/datasets/${datasetId}/snapshots/${commitRef}`
246
251
  const cache = new CacheItem(
247
- redis,
252
+ getRedis(),
248
253
  CacheType.snapshot,
249
254
  [datasetId, commitRef],
250
255
  432000,
@@ -1,11 +1,16 @@
1
1
  import config from "../config"
2
2
  import { Client } from "@elastic/elasticsearch"
3
3
 
4
- const elasticConfig = {
5
- node: config.elasticsearch.connection || "http://mock-client",
6
- maxRetries: 3,
7
- }
4
+ let _client: Client | null = null
8
5
 
9
- export const elasticClient = new Client(elasticConfig)
6
+ export function getElasticClient(): Client {
7
+ if (!_client) {
8
+ _client = new Client({
9
+ node: config.elasticsearch.connection || "http://mock-client",
10
+ maxRetries: 3,
11
+ })
12
+ }
13
+ return _client
14
+ }
10
15
 
11
- export default elasticClient
16
+ export default getElasticClient
@@ -1,6 +1,6 @@
1
1
  import config from "../config"
2
2
  import { indexDataset, indexingToken, queryForIndex } from "@openneuro/search"
3
- import { elasticClient } from "./elastic-client"
3
+ import { getElasticClient } from "./elastic-client"
4
4
  import { ApolloClient, from, InMemoryCache } from "@apollo/client"
5
5
  import type { NormalizedCacheObject } from "@apollo/client"
6
6
  import { setContext } from "@apollo/client/link/context"
@@ -28,9 +28,13 @@ export const schemaLinkClient = (): ApolloClient<NormalizedCacheObject> => {
28
28
  })
29
29
  }
30
30
 
31
- const client = schemaLinkClient()
31
+ let _client: ApolloClient<NormalizedCacheObject> | null = null
32
+ function getClient(): ApolloClient<NormalizedCacheObject> {
33
+ if (!_client) _client = schemaLinkClient()
34
+ return _client
35
+ }
32
36
 
33
37
  export const reindexDataset = async (datasetId: string): Promise<void> => {
34
- const datasetIndexQueryResult = await queryForIndex(client, datasetId)
35
- await indexDataset(elasticClient, datasetIndexQueryResult.data.dataset)
38
+ const datasetIndexQueryResult = await queryForIndex(getClient(), datasetId)
39
+ await indexDataset(getElasticClient(), datasetIndexQueryResult.data.dataset)
36
40
  }
@@ -3,6 +3,7 @@ import { connect } from "mongoose"
3
3
  import { deleteComment, flatten } from "../resolvers/comment"
4
4
  import { MongoMemoryServer } from "mongodb-memory-server"
5
5
  import Comment from "../../models/comment"
6
+ import type { GraphQLContext } from "../builder"
6
7
 
7
8
  vi.mock("ioredis")
8
9
 
@@ -12,11 +13,11 @@ describe("comment resolver helpers", () => {
12
13
  const adminUser = {
13
14
  user: "1234",
14
15
  userInfo: { admin: true },
15
- }
16
+ } as GraphQLContext
16
17
  const nonAdminUser = {
17
18
  user: "5678",
18
19
  userInfo: { admin: false },
19
- }
20
+ } as GraphQLContext
20
21
  let mongod
21
22
  beforeAll(async () => {
22
23
  // Setup MongoDB with mongodb-memory-server
@@ -0,0 +1,28 @@
1
+ import { vi } from "vitest"
2
+
3
+ vi.mock("ioredis", () => {
4
+ const RedisMock = vi.fn()
5
+ RedisMock.prototype.on = vi.fn()
6
+ RedisMock.prototype.connect = vi.fn()
7
+ return { default: RedisMock }
8
+ })
9
+ vi.mock("../../elasticsearch/elastic-client", () => ({
10
+ elasticClient: {},
11
+ }))
12
+ vi.mock("../../config", () => ({
13
+ default: {
14
+ url: "http://localhost",
15
+ auth: { jwt: { secret: "test-secret-for-schema-smoke" } },
16
+ datalad: { uri: "http://localhost" },
17
+ mongo: { url: "mongodb://localhost", dbName: "test" },
18
+ redis: {},
19
+ },
20
+ }))
21
+
22
+ import schema from "../schema"
23
+
24
+ it("builds the schema without error", () => {
25
+ expect(schema).toBeDefined()
26
+ expect(schema.getQueryType()).toBeDefined()
27
+ expect(schema.getMutationType()).toBeDefined()
28
+ })
@@ -0,0 +1,42 @@
1
+ import SchemaBuilder from "@pothos/core"
2
+ import SimpleObjectsPlugin from "@pothos/plugin-simple-objects"
3
+ import DirectivesPlugin from "@pothos/plugin-directives"
4
+
5
+ export interface UserInfo {
6
+ id: string
7
+ userId: string
8
+ admin: boolean
9
+ username?: string
10
+ provider?: string
11
+ providerId?: string
12
+ blocked?: boolean
13
+ orcidConsent?: boolean | null
14
+ reviewer?: boolean
15
+ exp?: string
16
+ scopes?: string[]
17
+ indexer?: boolean
18
+ }
19
+
20
+ export interface GraphQLContext {
21
+ user: string
22
+ userInfo: UserInfo
23
+ }
24
+
25
+ export const builder = new SchemaBuilder<{
26
+ Context: GraphQLContext
27
+ Scalars: {
28
+ ID: { Input: string; Output: string }
29
+ Date: { Input: string; Output: Date }
30
+ DateTime: { Input: string; Output: Date }
31
+ BigInt: { Input: number; Output: number }
32
+ JSON: { Input: unknown; Output: unknown }
33
+ }
34
+ DefaultFieldNullability: true
35
+ }>({
36
+ plugins: [SimpleObjectsPlugin, DirectivesPlugin],
37
+ notStrict:
38
+ "Pothos may not work correctly when strict mode is not enabled in tsconfig.json",
39
+ directives: {
40
+ useGraphQLToolsUnorderedDirectives: true,
41
+ },
42
+ })
@@ -21,127 +21,12 @@ describe("dataset resolvers", () => {
21
21
  const { id: dsId } = await ds.createDataset(
22
22
  null,
23
23
  { affirmedDefaced: true, affirmedConsent: false },
24
- { user: "123456", userInfo: {} },
25
- )
26
- expect(dsId).toEqual(expect.stringMatching(/^ds[0-9]{6}$/))
27
- })
28
- })
29
- describe("snapshotCreationComparison()", () => {
30
- it('sorts array of objects by the "created" and "tag" properties', () => {
31
- const testArray = [
32
- { id: 2, created: new Date("2018-11-20T00:05:43.473Z"), tag: "1.0.0" },
33
- { id: 1, created: new Date("2018-11-19T00:05:43.473Z"), tag: "1.0.1" },
34
- { id: 3, created: new Date("2018-11-23T00:05:43.473Z"), tag: "1.0.2" },
35
- { id: 5, created: new Date("2018-11-23T00:05:43.473Z"), tag: "1.0.10" },
36
- { id: 4, created: new Date("2018-11-23T00:05:43.473Z"), tag: "1.0.3" },
37
- ]
38
- const sorted = testArray.sort(ds.snapshotCreationComparison)
39
- expect(sorted[0].id).toBe(2)
40
- expect(sorted[1].id).toBe(1)
41
- expect(sorted[2].id).toBe(3)
42
- expect(sorted[3].id).toBe(4)
43
- expect(sorted[4].id).toBe(5)
44
- })
45
- it('sorts array of objects by the "created" property as strings', () => {
46
- const testArray = [
47
- { id: 2, created: "2018-11-20T00:05:43.473Z", tag: "2.0.0" },
48
- { id: 1, created: "2018-11-19T00:05:43.473Z", tag: "1.0.0" },
49
- { id: 3, created: "2018-11-23T00:05:43.473Z", tag: "3.0.0" },
50
- ]
51
- const sorted = testArray.sort(ds.snapshotCreationComparison)
52
- expect(sorted[0].id).toBe(1)
53
- expect(sorted[1].id).toBe(2)
54
- expect(sorted[2].id).toBe(3)
55
- })
56
- it("sorts non-semver tags mixed with semver tags", () => {
57
- const testArray = [
58
- { id: 2, created: new Date("2018-11-19T00:05:43.473Z"), tag: "1.0.2" },
59
- {
60
- id: 1,
61
- created: new Date("2018-11-19T00:05:43.473Z"),
62
- tag: "57fed018cce88d000ac1757f",
63
- },
64
- { id: 3, created: new Date("2018-11-19T00:05:43.473Z"), tag: "1.0.1" },
65
- ]
66
- const sorted = testArray.sort(ds.snapshotCreationComparison)
67
- expect(sorted[0].id).toBe(2)
68
- expect(sorted[1].id).toBe(1)
69
- expect(sorted[2].id).toBe(3)
70
- })
71
- it("sorts snapshots with only non-semver tags", () => {
72
- const testArray = [
73
- {
74
- id: 2,
75
- created: new Date("2018-11-19T00:05:43.473Z"),
76
- tag: "00001",
77
- },
78
- {
79
- id: 1,
80
- created: new Date("2018-11-19T00:05:43.473Z"),
81
- tag: "57fed018cce88d000ac1757f",
82
- },
83
- {
84
- id: 3,
85
- created: new Date("2018-11-19T00:05:43.473Z"),
86
- tag: "57fed018cce88d000ac1757f",
87
- },
88
- ]
89
- const sorted = testArray.sort(ds.snapshotCreationComparison)
90
- expect(sorted[0].id).toBe(2)
91
- expect(sorted[1].id).toBe(1)
92
- expect(sorted[2].id).toBe(3)
93
- })
94
- it("sorts very similar creation times by semver order", () => {
95
- const testSnapshots = [
96
24
  {
97
- id: "ds002680:1.0.0",
98
- created: "2020-04-03T23:19:56.000Z",
99
- tag: "1.0.0",
25
+ user: "123456",
26
+ userInfo: { id: "123456", userId: "123456", admin: false },
100
27
  },
101
- {
102
- id: "ds002680:1.2.0",
103
- created: "2021-10-19T16:26:43.000Z",
104
- tag: "1.2.0",
105
- },
106
- {
107
- id: "ds002680:1.1.0",
108
- created: "2021-10-19T16:26:44.000Z",
109
- tag: "1.1.0",
110
- },
111
- ]
112
- const sorted = testSnapshots.sort(ds.snapshotCreationComparison)
113
- expect(sorted[0].id).toBe("ds002680:1.0.0")
114
- expect(sorted[1].id).toBe("ds002680:1.1.0")
115
- expect(sorted[2].id).toBe("ds002680:1.2.0")
116
- })
117
- it("sorts 000002 (legacy snapshots) before 1.0.1 (current format)", () => {
118
- const testSnapshots = [
119
- {
120
- id: "ds000247:00002",
121
- created: "2018-07-18T02:27:39.000Z",
122
- tag: "00002",
123
- },
124
- {
125
- id: "ds000247:00001",
126
- created: "2018-07-18T02:35:37.000Z",
127
- tag: "00001",
128
- },
129
- {
130
- id: "ds000247:1.0.0",
131
- created: "2021-07-05T15:58:18.000Z",
132
- tag: "1.0.0",
133
- },
134
- {
135
- id: "ds000247:1.0.1",
136
- created: "2021-08-25T23:37:53.000Z",
137
- tag: "1.0.1",
138
- },
139
- ]
140
- const sorted = testSnapshots.sort(ds.snapshotCreationComparison)
141
- expect(sorted[0].id).toBe("ds000247:00002")
142
- expect(sorted[1].id).toBe("ds000247:00001")
143
- expect(sorted[2].id).toBe("ds000247:1.0.0")
144
- expect(sorted[3].id).toBe("ds000247:1.0.1")
28
+ )
29
+ expect(dsId).toEqual(expect.stringMatching(/^ds[0-9]{6}$/))
145
30
  })
146
31
  })
147
32
  describe("deleteFiles", () => {
@@ -170,6 +55,8 @@ describe("dataset resolvers", () => {
170
55
  user: "a_user_id",
171
56
  userInfo: {
172
57
  // bypass permission checks
58
+ id: "a_user_id",
59
+ userId: "a_user_id",
173
60
  admin: true,
174
61
  },
175
62
  },
@@ -1,5 +1,6 @@
1
1
  import { vi } from "vitest"
2
2
  import { allowedImportUrl, importRemoteDataset } from "../importRemoteDataset"
3
+ import type { GraphQLContext } from "../../builder"
3
4
 
4
5
  vi.mock("ioredis")
5
6
  vi.mock("../../../config")
@@ -10,7 +11,7 @@ describe("importRemoteDataset mutation", () => {
10
11
  await importRemoteDataset(
11
12
  {},
12
13
  { datasetId: "ds000000", url: "" },
13
- { user: "1234", userInfo: { admin: true } },
14
+ { user: "1234", userInfo: { admin: true } } as GraphQLContext,
14
15
  )
15
16
  })
16
17
  describe("allowedImportUrl()", () => {
@@ -1,5 +1,6 @@
1
1
  import { vi } from "vitest"
2
2
  import { updatePermissions } from "../permissions"
3
+ import type { GraphQLContext } from "../../builder"
3
4
 
4
5
  vi.mock("ioredis")
5
6
  vi.mock("../../permissions", () => ({
@@ -22,8 +23,8 @@ describe("permissions resolvers", () => {
22
23
  try {
23
24
  await updatePermissions(
24
25
  {},
25
- { datasetId: "ds01234", userEmail: "fake@test.com" },
26
- { user: "1234", userInfo: { id: "1234" } },
26
+ { datasetId: "ds01234", userEmail: "fake@test.com", level: "ro" },
27
+ { user: "1234", userInfo: { id: "1234" } } as GraphQLContext,
27
28
  )
28
29
  } catch (err) {
29
30
  error = err