@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.
- package/package.json +8 -5
- package/src/app.ts +2 -4
- package/src/cache/__tests__/tree.spec.ts +2 -0
- package/src/cache/tree.ts +4 -0
- package/src/cache/types.ts +1 -0
- package/src/datalad/__tests__/contributors.spec.ts +1 -1
- package/src/datalad/__tests__/dataRetentionNotifications.spec.ts +11 -11
- package/src/datalad/__tests__/files.spec.ts +31 -1
- package/src/datalad/__tests__/snapshots.spec.ts +4 -4
- package/src/datalad/contributors.ts +2 -2
- package/src/datalad/dataRetentionNotifications.ts +5 -5
- package/src/datalad/dataset.ts +9 -5
- package/src/datalad/description.ts +2 -2
- package/src/datalad/draft.ts +8 -3
- package/src/datalad/files.ts +71 -12
- package/src/datalad/readme.ts +2 -2
- package/src/datalad/snapshots.ts +19 -14
- package/src/elasticsearch/elastic-client.ts +11 -6
- package/src/elasticsearch/reindex-dataset.ts +8 -4
- package/src/graphql/__tests__/comment.spec.ts +3 -2
- package/src/graphql/__tests__/schema.spec.ts +28 -0
- package/src/graphql/builder.ts +42 -0
- package/src/graphql/resolvers/__tests__/dataset.spec.ts +6 -119
- package/src/graphql/resolvers/__tests__/importRemoteDataset.spec.ts +2 -1
- package/src/graphql/resolvers/__tests__/permssions.spec.ts +3 -2
- package/src/graphql/resolvers/__tests__/user.spec.ts +39 -11
- package/src/graphql/resolvers/brainInitiative.ts +4 -3
- package/src/graphql/resolvers/cache.ts +7 -6
- package/src/graphql/resolvers/comment.ts +35 -19
- package/src/graphql/resolvers/dataset-search.ts +7 -6
- package/src/graphql/resolvers/dataset.ts +77 -45
- package/src/graphql/resolvers/datasetEvents.ts +18 -16
- package/src/graphql/resolvers/description.ts +2 -1
- package/src/graphql/resolvers/draft.ts +14 -3
- package/src/graphql/resolvers/fileCheck.ts +5 -4
- package/src/graphql/resolvers/flaggedFiles.ts +2 -1
- package/src/graphql/resolvers/follow.ts +2 -1
- package/src/graphql/resolvers/git.ts +2 -1
- package/src/graphql/resolvers/gitEvents.ts +2 -1
- package/src/graphql/resolvers/history.ts +3 -1
- package/src/graphql/resolvers/holdDeletion.ts +1 -1
- package/src/graphql/resolvers/importRemoteDataset.ts +3 -2
- package/src/graphql/resolvers/issues.ts +2 -1
- package/src/graphql/resolvers/metadata.ts +3 -2
- package/src/graphql/resolvers/permissions.ts +26 -6
- package/src/graphql/resolvers/publish.ts +2 -1
- package/src/graphql/resolvers/readme.ts +2 -1
- package/src/graphql/resolvers/reexporter.ts +2 -1
- package/src/graphql/resolvers/relation.ts +3 -2
- package/src/graphql/resolvers/reset.ts +2 -1
- package/src/graphql/resolvers/reviewer.ts +14 -6
- package/src/graphql/resolvers/snapshots.ts +42 -10
- package/src/graphql/resolvers/stars.ts +2 -1
- package/src/graphql/resolvers/upload.ts +3 -2
- package/src/graphql/resolvers/user.ts +44 -32
- package/src/graphql/resolvers/validation.ts +6 -5
- package/src/graphql/resolvers/worker.ts +2 -1
- package/src/graphql/schema/analytics.ts +12 -0
- package/src/graphql/schema/comment.ts +30 -0
- package/src/graphql/schema/dataset-events.ts +91 -0
- package/src/graphql/schema/dataset-search.ts +32 -0
- package/src/graphql/schema/dataset.ts +167 -0
- package/src/graphql/schema/description.ts +44 -0
- package/src/graphql/schema/draft.ts +80 -0
- package/src/graphql/schema/enums.ts +55 -0
- package/src/graphql/schema/files.ts +44 -0
- package/src/graphql/schema/inputs.ts +231 -0
- package/src/graphql/schema/metadata.ts +86 -0
- package/src/graphql/schema/misc.ts +154 -0
- package/src/graphql/schema/mutation.ts +549 -0
- package/src/graphql/schema/pagination.ts +12 -0
- package/src/graphql/schema/permissions.ts +21 -0
- package/src/graphql/schema/query.ts +119 -0
- package/src/graphql/schema/refs.ts +23 -0
- package/src/graphql/schema/reviewer.ts +11 -0
- package/src/graphql/schema/scalars.ts +9 -0
- package/src/graphql/schema/snapshot.ts +111 -0
- package/src/graphql/schema/upload.ts +13 -0
- package/src/graphql/schema/user.ts +61 -0
- package/src/graphql/schema/validation.ts +70 -0
- package/src/graphql/schema/worker.ts +16 -0
- package/src/graphql/schema.ts +29 -1114
- package/src/handlers/subscriptions.ts +1 -1
- package/src/libs/apikey.ts +1 -1
- package/src/libs/authentication/crypto.ts +14 -9
- package/src/libs/notifications.ts +1 -1
- package/src/libs/presign.ts +32 -20
- package/src/libs/redis.ts +12 -2
- package/src/models/comment.ts +2 -4
- package/src/models/counter.ts +2 -2
- package/src/models/ingestDataset.ts +1 -2
- package/src/models/notification.ts +0 -2
- package/src/models/subscription.ts +0 -1
- package/src/models/user.ts +1 -2
- package/src/models/userMigration.ts +1 -1
- package/src/models/userNotificationStatus.ts +0 -1
- package/src/utils/__tests__/snapshots.spec.ts +120 -0
- package/src/utils/snapshots.ts +12 -0
package/src/datalad/files.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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(
|
|
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(
|
|
263
|
+
void setTree(getRedis(), hash, entries)
|
|
252
264
|
permanentHashes.push(hash)
|
|
253
265
|
} else {
|
|
254
|
-
void setTree(
|
|
266
|
+
void setTree(getRedis(), hash, entries, 600)
|
|
255
267
|
}
|
|
256
268
|
}
|
|
257
269
|
}
|
|
258
270
|
if (permanentHashes.length > 0) {
|
|
259
|
-
void addDatasetTrees(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
367
|
-
void addDatasetTrees(
|
|
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
|
-
|
|
491
|
+
getRedis(),
|
|
433
492
|
presignIndices.map((i) => ({
|
|
434
493
|
bucket: fileEntries[i].entry.b,
|
|
435
494
|
s3Key: fileEntries[i].entry.k,
|
package/src/datalad/readme.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { addFileString, commitFiles } from "./dataset"
|
|
2
|
-
import {
|
|
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(
|
|
17
|
+
const cache = new CacheItem(getRedis(), CacheType.readme, [
|
|
18
18
|
datasetId,
|
|
19
19
|
revision.substring(0, 7),
|
|
20
20
|
])
|
package/src/datalad/snapshots.ts
CHANGED
|
@@ -3,13 +3,10 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import * as Sentry from "@sentry/node"
|
|
5
5
|
import request from "superagent"
|
|
6
|
-
import {
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
5
|
-
node: config.elasticsearch.connection || "http://mock-client",
|
|
6
|
-
maxRetries: 3,
|
|
7
|
-
}
|
|
4
|
+
let _client: Client | null = null
|
|
8
5
|
|
|
9
|
-
export
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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(
|
|
35
|
-
await indexDataset(
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
tag: "1.0.0",
|
|
25
|
+
user: "123456",
|
|
26
|
+
userInfo: { id: "123456", userId: "123456", admin: false },
|
|
100
27
|
},
|
|
101
|
-
|
|
102
|
-
|
|
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
|