@openneuro/server 4.20.4 → 4.20.6-alpha.2
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 +4 -6
- package/src/__mocks__/{config.js → config.ts} +5 -5
- package/src/app.ts +32 -31
- package/src/cache/item.ts +6 -7
- package/src/cache/types.ts +8 -8
- package/src/{config.js → config.ts} +6 -6
- package/src/datalad/__tests__/changelog.spec.ts +83 -0
- package/src/datalad/__tests__/dataset.spec.ts +109 -0
- package/src/datalad/__tests__/description.spec.ts +141 -0
- package/src/datalad/__tests__/files.spec.ts +77 -0
- package/src/datalad/__tests__/pagination.spec.ts +136 -0
- package/src/datalad/__tests__/{snapshots.spec.js → snapshots.spec.ts} +17 -17
- package/src/datalad/{analytics.js → analytics.ts} +4 -4
- package/src/datalad/{changelog.js → changelog.ts} +17 -14
- package/src/datalad/{dataset.js → dataset.ts} +95 -93
- package/src/datalad/{description.js → description.ts} +37 -37
- package/src/datalad/draft.ts +38 -0
- package/src/datalad/files.ts +26 -20
- package/src/datalad/{pagination.js → pagination.ts} +47 -47
- package/src/datalad/{readme.js → readme.ts} +13 -11
- package/src/datalad/{reexporter.js → reexporter.ts} +4 -4
- package/src/datalad/{snapshots.js → snapshots.ts} +56 -62
- package/src/datalad/{upload.js → upload.ts} +7 -5
- package/src/elasticsearch/elastic-client.ts +11 -0
- package/src/elasticsearch/reindex-dataset.ts +7 -7
- package/src/graphql/__tests__/__snapshots__/permissions.spec.ts.snap +5 -0
- package/src/graphql/__tests__/{comment.spec.js → comment.spec.ts} +17 -17
- package/src/graphql/__tests__/permissions.spec.ts +113 -0
- package/src/graphql/{permissions.js → permissions.ts} +14 -14
- package/src/graphql/resolvers/__tests__/brainlife.spec.ts +11 -11
- package/src/graphql/resolvers/__tests__/{dataset-search.spec.js → dataset-search.spec.ts} +25 -23
- package/src/graphql/resolvers/__tests__/dataset.spec.ts +175 -0
- package/src/graphql/resolvers/__tests__/derivatives.spec.ts +19 -19
- package/src/graphql/resolvers/__tests__/importRemoteDataset.spec.ts +20 -20
- package/src/graphql/resolvers/__tests__/permssions.spec.ts +35 -0
- package/src/graphql/resolvers/__tests__/snapshots.spec.ts +59 -0
- package/src/graphql/resolvers/__tests__/user.spec.ts +18 -0
- package/src/graphql/resolvers/brainlife.ts +4 -4
- package/src/graphql/resolvers/cache.ts +4 -4
- package/src/graphql/resolvers/{comment.js → comment.ts} +16 -16
- package/src/graphql/resolvers/{dataset-search.js → dataset-search.ts} +45 -43
- package/src/graphql/resolvers/{dataset.js → dataset.ts} +38 -52
- package/src/graphql/resolvers/datasetType.ts +3 -3
- package/src/graphql/resolvers/derivatives.ts +11 -11
- package/src/graphql/resolvers/description.ts +18 -0
- package/src/graphql/resolvers/{draft.js → draft.ts} +13 -13
- package/src/graphql/resolvers/{flaggedFiles.js → flaggedFiles.ts} +4 -4
- package/src/graphql/resolvers/{follow.js → follow.ts} +1 -1
- package/src/graphql/resolvers/git.ts +3 -3
- package/src/graphql/resolvers/history.ts +13 -0
- package/src/graphql/resolvers/importRemoteDataset.ts +12 -11
- package/src/graphql/resolvers/index.ts +25 -0
- package/src/graphql/resolvers/{issues.js → issues.ts} +9 -9
- package/src/graphql/resolvers/metadata.ts +8 -8
- package/src/graphql/resolvers/{mutation.js → mutation.ts} +26 -26
- package/src/graphql/resolvers/{newsletter.js → newsletter.ts} +2 -2
- package/src/graphql/resolvers/permissions.ts +15 -21
- package/src/graphql/resolvers/publish.ts +17 -0
- package/src/graphql/resolvers/query.ts +21 -0
- package/src/graphql/resolvers/{readme.js → readme.ts} +3 -3
- package/src/graphql/resolvers/{reexporter.js → reexporter.ts} +2 -2
- package/src/graphql/resolvers/relation.ts +5 -5
- package/src/graphql/resolvers/{reset.js → reset.ts} +2 -2
- package/src/graphql/resolvers/reviewer.ts +4 -4
- package/src/graphql/resolvers/{snapshots.js → snapshots.ts} +49 -49
- package/src/graphql/resolvers/{stars.js → stars.ts} +1 -1
- package/src/graphql/resolvers/summary.ts +3 -3
- package/src/graphql/resolvers/{upload.js → upload.ts} +5 -5
- package/src/graphql/resolvers/{user.js → user.ts} +16 -18
- package/src/graphql/resolvers/{validation.js → validation.ts} +12 -14
- package/src/graphql/{schema.js → schema.ts} +4 -6
- package/src/graphql/utils/{file.js → file.ts} +2 -2
- package/src/handlers/{comments.js → comments.ts} +11 -11
- package/src/handlers/{config.js → config.ts} +1 -1
- package/src/handlers/{datalad.js → datalad.ts} +22 -22
- package/src/handlers/{doi.js → doi.ts} +6 -6
- package/src/handlers/reviewer.ts +6 -6
- package/src/handlers/{sitemap.js → sitemap.ts} +19 -19
- package/src/handlers/stars.ts +11 -10
- package/src/handlers/{subscriptions.js → subscriptions.ts} +17 -16
- package/src/handlers/{users.js → users.ts} +3 -3
- package/src/libs/__tests__/apikey.spec.ts +25 -0
- package/src/libs/__tests__/datalad-service.spec.ts +27 -0
- package/src/libs/__tests__/{dataset.spec.js → dataset.spec.ts} +9 -9
- package/src/libs/{apikey.js → apikey.ts} +5 -5
- package/src/libs/authentication/__tests__/jwt.spec.ts +59 -0
- package/src/libs/authentication/{crypto.js → crypto.ts} +16 -16
- package/src/libs/authentication/google.ts +18 -0
- package/src/libs/authentication/jwt.ts +40 -33
- package/src/libs/authentication/{orcid.js → orcid.ts} +11 -11
- package/src/libs/authentication/{passport.js → passport.ts} +45 -30
- package/src/libs/authentication/{states.js → states.ts} +17 -20
- package/src/libs/{counter.js → counter.ts} +1 -1
- package/src/libs/{datalad-service.js → datalad-service.ts} +4 -4
- package/src/libs/dataset.ts +9 -0
- package/src/libs/doi/__tests__/__snapshots__/doi.spec.ts.snap +17 -0
- package/src/libs/doi/__tests__/doi.spec.ts +25 -0
- package/src/libs/doi/__tests__/normalize.spec.ts +19 -19
- package/src/libs/doi/{index.js → index.ts} +27 -21
- package/src/libs/doi/normalize.ts +2 -2
- package/src/libs/email/__tests__/index.spec.ts +14 -14
- package/src/libs/email/index.ts +4 -4
- package/src/libs/email/templates/__tests__/comment-created.spec.ts +12 -12
- package/src/libs/email/templates/__tests__/dataset-deleted.spec.ts +6 -6
- package/src/libs/email/templates/__tests__/owner-unsubscribed.spec.ts +6 -6
- package/src/libs/email/templates/__tests__/snapshot-created.spec.ts +9 -9
- package/src/libs/email/templates/__tests__/snapshot-reminder.spec.ts +7 -7
- package/src/libs/email/templates/comment-created.ts +2 -1
- package/src/libs/email/templates/dataset-deleted.ts +2 -1
- package/src/libs/email/templates/dataset-import-failed.ts +2 -1
- package/src/libs/email/templates/dataset-imported.ts +2 -1
- package/src/libs/email/templates/owner-unsubscribed.ts +2 -1
- package/src/libs/email/templates/snapshot-created.ts +2 -1
- package/src/libs/email/templates/snapshot-reminder.ts +2 -1
- package/src/libs/{notifications.js → notifications.ts} +100 -113
- package/src/libs/{orcid.js → orcid.ts} +20 -20
- package/src/libs/{redis.js → redis.ts} +6 -6
- package/src/models/__tests__/ingestDataset.spec.ts +15 -15
- package/src/models/analytics.ts +2 -2
- package/src/models/badAnnexObject.ts +6 -6
- package/src/models/comment.ts +10 -10
- package/src/models/counter.ts +2 -2
- package/src/models/dataset.ts +16 -16
- package/src/models/deletion.ts +3 -3
- package/src/models/deprecatedSnapshot.ts +2 -2
- package/src/models/doi.ts +2 -2
- package/src/models/file.ts +2 -2
- package/src/models/ingestDataset.ts +4 -4
- package/src/models/issue.ts +2 -2
- package/src/models/key.ts +2 -2
- package/src/models/mailgunIdentifier.ts +2 -2
- package/src/models/metadata.ts +3 -3
- package/src/models/newsletter.ts +3 -3
- package/src/models/notification.ts +2 -2
- package/src/models/permission.ts +4 -4
- package/src/models/reviewer.ts +7 -7
- package/src/models/snapshot.ts +2 -2
- package/src/models/stars.ts +6 -6
- package/src/models/subscription.ts +2 -2
- package/src/models/summary.ts +2 -2
- package/src/models/upload.ts +3 -3
- package/src/models/user.ts +4 -4
- package/src/{routes.js → routes.ts} +62 -62
- package/src/server.ts +9 -9
- package/src/utils/__tests__/datasetOrSnapshot.spec.ts +25 -25
- package/src/utils/__tests__/validateUrl.spec.ts +10 -10
- package/src/utils/datasetOrSnapshot.ts +2 -2
- package/src/utils/validateUrl.ts +1 -1
- package/src/datalad/__tests__/changelog.spec.js +0 -82
- package/src/datalad/__tests__/dataset.spec.js +0 -109
- package/src/datalad/__tests__/description.spec.js +0 -137
- package/src/datalad/__tests__/files.spec.js +0 -75
- package/src/datalad/__tests__/pagination.spec.js +0 -136
- package/src/datalad/draft.js +0 -37
- package/src/elasticsearch/elastic-client.js +0 -11
- package/src/graphql/__tests__/permissions.spec.js +0 -107
- package/src/graphql/pubsub.js +0 -5
- package/src/graphql/resolvers/__tests__/dataset.spec.js +0 -175
- package/src/graphql/resolvers/__tests__/permssions.spec.js +0 -34
- package/src/graphql/resolvers/__tests__/snapshots.spec.js +0 -58
- package/src/graphql/resolvers/__tests__/user.spec.js +0 -17
- package/src/graphql/resolvers/description.js +0 -29
- package/src/graphql/resolvers/history.js +0 -11
- package/src/graphql/resolvers/index.js +0 -25
- package/src/graphql/resolvers/publish.js +0 -17
- package/src/graphql/resolvers/query.js +0 -21
- package/src/graphql/resolvers/subscriptions.js +0 -81
- package/src/graphql/utils/publish-draft-update.js +0 -13
- package/src/libs/__tests__/apikey.spec.js +0 -24
- package/src/libs/__tests__/datalad-service.spec.js +0 -26
- package/src/libs/authentication/__tests__/jwt.spec.js +0 -23
- package/src/libs/authentication/globus.js +0 -11
- package/src/libs/authentication/google.js +0 -19
- package/src/libs/bidsId.js +0 -68
- package/src/libs/dataset.js +0 -9
- package/src/libs/doi/__tests__/doi.spec.js +0 -24
- package/src/libs/redis-pubsub.js +0 -5
- package/src/libs/request.js +0 -155
- package/src/libs/scitran.js +0 -25
- package/src/libs/subscription-server.js +0 -20
- package/src/libs/testing-utils.js +0 -17
- package/src/persistent/datasets/.gitignore +0 -3
- package/src/persistent/temp/.gitignore +0 -3
- /package/src/libs/__mocks__/{notifications.js → notifications.ts} +0 -0
- /package/src/libs/authentication/{verifyUser.js → verifyUser.ts} +0 -0
|
@@ -1,48 +1,48 @@
|
|
|
1
|
-
import { vi } from
|
|
2
|
-
import { connect } from
|
|
3
|
-
import { deleteComment, flatten } from
|
|
4
|
-
import Comment from
|
|
1
|
+
import { vi } from "vitest"
|
|
2
|
+
import { connect } from "mongoose"
|
|
3
|
+
import { deleteComment, flatten } from "../resolvers/comment"
|
|
4
|
+
import Comment from "../../models/comment"
|
|
5
5
|
|
|
6
|
-
vi.mock(
|
|
6
|
+
vi.mock("ioredis")
|
|
7
7
|
|
|
8
|
-
describe(
|
|
9
|
-
describe(
|
|
8
|
+
describe("comment resolver helpers", () => {
|
|
9
|
+
describe("deleteComment", () => {
|
|
10
10
|
let aId
|
|
11
11
|
const adminUser = {
|
|
12
|
-
user:
|
|
12
|
+
user: "1234",
|
|
13
13
|
userInfo: { admin: true },
|
|
14
14
|
}
|
|
15
15
|
const nonAdminUser = {
|
|
16
|
-
user:
|
|
16
|
+
user: "5678",
|
|
17
17
|
userInfo: { admin: false },
|
|
18
18
|
}
|
|
19
19
|
beforeAll(async () => {
|
|
20
20
|
await connect(globalThis.__MONGO_URI__)
|
|
21
21
|
const comment = new Comment({
|
|
22
|
-
text:
|
|
22
|
+
text: "a",
|
|
23
23
|
createDate: new Date().toISOString(),
|
|
24
|
-
user: { id:
|
|
24
|
+
user: { id: "5678" },
|
|
25
25
|
})
|
|
26
26
|
await comment.save()
|
|
27
27
|
aId = comment.id
|
|
28
28
|
})
|
|
29
29
|
|
|
30
|
-
it(
|
|
30
|
+
it("returns an array of the deleted comment ids", async () => {
|
|
31
31
|
const deletedIds = (
|
|
32
32
|
await deleteComment({}, { commentId: aId }, adminUser)
|
|
33
|
-
).map(id => id.toString())
|
|
33
|
+
).map((id) => id.toString())
|
|
34
34
|
expect(deletedIds[0]).toEqual(aId)
|
|
35
35
|
})
|
|
36
36
|
|
|
37
|
-
it(
|
|
37
|
+
it("prevents non-admin users from deleting comments", () => {
|
|
38
38
|
return expect(
|
|
39
39
|
deleteComment({}, { commentId: aId }, nonAdminUser),
|
|
40
|
-
).rejects.toMatch(
|
|
40
|
+
).rejects.toMatch("You do not have admin access to this dataset.")
|
|
41
41
|
})
|
|
42
42
|
})
|
|
43
43
|
|
|
44
|
-
describe(
|
|
45
|
-
it(
|
|
44
|
+
describe("flatten", () => {
|
|
45
|
+
it("unrolls an array", () => {
|
|
46
46
|
const arrarr = [
|
|
47
47
|
[1, 2, 3],
|
|
48
48
|
[4, 5],
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { vi } from "vitest"
|
|
2
|
+
import {
|
|
3
|
+
checkDatasetAdmin,
|
|
4
|
+
checkDatasetWrite,
|
|
5
|
+
checkPermissionLevel,
|
|
6
|
+
datasetReadQuery,
|
|
7
|
+
states,
|
|
8
|
+
} from "../permissions"
|
|
9
|
+
|
|
10
|
+
vi.mock("ioredis")
|
|
11
|
+
|
|
12
|
+
describe("resolver permissions helpers", () => {
|
|
13
|
+
describe("datasetReadQuery()", () => {
|
|
14
|
+
it("returns public for anonymous users", () => {
|
|
15
|
+
expect(datasetReadQuery("ds000001", null, null)).toHaveProperty(
|
|
16
|
+
"public",
|
|
17
|
+
true,
|
|
18
|
+
)
|
|
19
|
+
})
|
|
20
|
+
it("returns non-public for admins", () => {
|
|
21
|
+
expect(
|
|
22
|
+
datasetReadQuery("ds000001", "1234", { admin: true }),
|
|
23
|
+
).not.toHaveProperty("public", true)
|
|
24
|
+
})
|
|
25
|
+
it("returns public for logged in users", () => {
|
|
26
|
+
expect(
|
|
27
|
+
datasetReadQuery("ds000001", "1234", { admin: false }),
|
|
28
|
+
).toHaveProperty("public", true)
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
describe("checkPermissionLevel(..., READ)", () => {
|
|
32
|
+
it("returns false if no permission passed in", () => {
|
|
33
|
+
expect(checkPermissionLevel(null, states.READ)).toBe(false)
|
|
34
|
+
})
|
|
35
|
+
it("returns true for valid read access level", () => {
|
|
36
|
+
expect(checkPermissionLevel({ level: "admin" }, states.READ)).toBe(true)
|
|
37
|
+
expect(checkPermissionLevel({ level: "ro" }, states.READ)).toBe(true)
|
|
38
|
+
})
|
|
39
|
+
it("returns false if an unexpected level is present", () => {
|
|
40
|
+
expect(checkPermissionLevel({ level: "not-real" }, states.READ)).toBe(
|
|
41
|
+
false,
|
|
42
|
+
)
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
describe("checkPermissionLevel(..., WRITE)", () => {
|
|
46
|
+
it("returns false if no permission passed in", () => {
|
|
47
|
+
expect(checkPermissionLevel(null, states.WRITE)).toBe(false)
|
|
48
|
+
})
|
|
49
|
+
it("returns true for admin", () => {
|
|
50
|
+
expect(checkPermissionLevel({ level: "admin" }, states.WRITE)).toBe(true)
|
|
51
|
+
})
|
|
52
|
+
it("returns false for read only access", () => {
|
|
53
|
+
expect(checkPermissionLevel({ level: "ro" }, states.WRITE)).toBe(false)
|
|
54
|
+
})
|
|
55
|
+
it("returns false if an unexpected level is present", () => {
|
|
56
|
+
expect(checkPermissionLevel({ level: "not-real" }, states.WRITE)).toBe(
|
|
57
|
+
false,
|
|
58
|
+
)
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
describe("checkDatasetWrite()", () => {
|
|
62
|
+
it("resolves to false for anonymous users", () => {
|
|
63
|
+
return expect(
|
|
64
|
+
checkDatasetWrite("ds000001", null, null, undefined, {
|
|
65
|
+
checkExists: false,
|
|
66
|
+
}),
|
|
67
|
+
).rejects.toThrowErrorMatchingSnapshot()
|
|
68
|
+
})
|
|
69
|
+
it("resolves to true for admins", () => {
|
|
70
|
+
return expect(
|
|
71
|
+
checkDatasetWrite("ds000001", "1234", { admin: true }, undefined, {
|
|
72
|
+
checkExists: false,
|
|
73
|
+
}),
|
|
74
|
+
).resolves.toBe(true)
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
describe("checkPermissionLevel(..., ADMIN)", () => {
|
|
78
|
+
it("returns false if no permission passed in", () => {
|
|
79
|
+
expect(checkPermissionLevel(null, states.ADMIN)).toBe(false)
|
|
80
|
+
})
|
|
81
|
+
it("returns true for admin", () => {
|
|
82
|
+
expect(checkPermissionLevel({ level: "admin" }, states.ADMIN)).toBe(true)
|
|
83
|
+
})
|
|
84
|
+
it("returns false for read write access", () => {
|
|
85
|
+
expect(checkPermissionLevel({ level: "rw" }, states.ADMIN)).toBe(false)
|
|
86
|
+
})
|
|
87
|
+
it("returns false for read only access", () => {
|
|
88
|
+
expect(checkPermissionLevel({ level: "ro" }, states.ADMIN)).toBe(false)
|
|
89
|
+
})
|
|
90
|
+
it("returns false if an unexpected level is present", () => {
|
|
91
|
+
expect(checkPermissionLevel({ level: "not-real" }, states.ADMIN)).toBe(
|
|
92
|
+
false,
|
|
93
|
+
)
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
describe("checkDatasetAdmin()", () => {
|
|
97
|
+
it("resolves to false for anonymous users", () => {
|
|
98
|
+
return expect(
|
|
99
|
+
checkDatasetAdmin("ds000001", null, null, { checkExists: false }),
|
|
100
|
+
).rejects.toThrowErrorMatchingSnapshot()
|
|
101
|
+
})
|
|
102
|
+
it("resolves to true for admins", () => {
|
|
103
|
+
return expect(
|
|
104
|
+
checkDatasetAdmin(
|
|
105
|
+
"ds000001",
|
|
106
|
+
"1234",
|
|
107
|
+
{ admin: true },
|
|
108
|
+
{ checkExists: false },
|
|
109
|
+
),
|
|
110
|
+
).resolves.toBe(true)
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
})
|
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
import config from
|
|
2
|
-
import { GraphQLError } from
|
|
3
|
-
import Permission from
|
|
4
|
-
import Dataset from
|
|
5
|
-
import Deletion from
|
|
1
|
+
import config from "../config"
|
|
2
|
+
import { GraphQLError } from "graphql"
|
|
3
|
+
import Permission from "../models/permission"
|
|
4
|
+
import Dataset from "../models/dataset"
|
|
5
|
+
import Deletion from "../models/deletion"
|
|
6
6
|
|
|
7
7
|
// Definitions for permission levels allowed
|
|
8
8
|
// Admin is write + manage user permissions
|
|
9
9
|
export const states = {
|
|
10
10
|
READ: {
|
|
11
|
-
errorMessage:
|
|
12
|
-
allowed: [
|
|
11
|
+
errorMessage: "You do not have access to read this dataset.",
|
|
12
|
+
allowed: ["ro", "rw", "admin"],
|
|
13
13
|
},
|
|
14
14
|
WRITE: {
|
|
15
|
-
errorMessage:
|
|
16
|
-
allowed: [
|
|
15
|
+
errorMessage: "You do not have access to modify this dataset.",
|
|
16
|
+
allowed: ["rw", "admin"],
|
|
17
17
|
},
|
|
18
18
|
ADMIN: {
|
|
19
|
-
errorMessage:
|
|
20
|
-
allowed: [
|
|
19
|
+
errorMessage: "You do not have admin access to this dataset.",
|
|
20
|
+
allowed: ["admin"],
|
|
21
21
|
},
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -62,10 +62,10 @@ export class DeletedDatasetError extends GraphQLError {
|
|
|
62
62
|
const url = new URL(redirect)
|
|
63
63
|
if (
|
|
64
64
|
url.hostname === canonical.hostname &&
|
|
65
|
-
url.pathname.startsWith(
|
|
65
|
+
url.pathname.startsWith("/datasets")
|
|
66
66
|
) {
|
|
67
67
|
// Only return a relative path to avoid cross site risks
|
|
68
|
-
extensions = { code:
|
|
68
|
+
extensions = { code: "DELETED_DATASET", redirect: url.pathname }
|
|
69
69
|
}
|
|
70
70
|
} catch (err) {
|
|
71
71
|
// Do nothing
|
|
@@ -77,7 +77,7 @@ export class DeletedDatasetError extends GraphQLError {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
export const checkDatasetExists = async datasetId => {
|
|
80
|
+
export const checkDatasetExists = async (datasetId) => {
|
|
81
81
|
const deleted = await Deletion.findOne({ datasetId }).exec()
|
|
82
82
|
if (deleted) {
|
|
83
83
|
throw new DeletedDatasetError(datasetId, deleted.reason, deleted.redirect)
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import { vi } from
|
|
2
|
-
import { HasId } from
|
|
3
|
-
import { brainlifeQuery } from
|
|
1
|
+
import { vi } from "vitest"
|
|
2
|
+
import { HasId } from "../../../utils/datasetOrSnapshot"
|
|
3
|
+
import { brainlifeQuery } from "../brainlife"
|
|
4
4
|
|
|
5
|
-
vi.mock(
|
|
5
|
+
vi.mock("ioredis")
|
|
6
6
|
|
|
7
|
-
describe(
|
|
8
|
-
it(
|
|
9
|
-
expect(brainlifeQuery({ id:
|
|
10
|
-
|
|
7
|
+
describe("brainlife resolvers", () => {
|
|
8
|
+
it("correctly queries drafts", () => {
|
|
9
|
+
expect(brainlifeQuery({ id: "ds000001" } as HasId).toString()).toEqual(
|
|
10
|
+
"https://brainlife.io/api/warehouse/datalad/datasets?find=%7B%22removed%22%3Afalse%2C%22path%22%3A%7B%22%24regex%22%3A%22%5EOpenNeuro%2Fds000001%22%7D%7D",
|
|
11
11
|
)
|
|
12
12
|
})
|
|
13
|
-
it(
|
|
13
|
+
it("correctly queries versioned datasets", () => {
|
|
14
14
|
expect(
|
|
15
|
-
brainlifeQuery({ id:
|
|
15
|
+
brainlifeQuery({ id: "ds000001:1.0.0", tag: "1.0.0" }).toString(),
|
|
16
16
|
).toEqual(
|
|
17
|
-
|
|
17
|
+
"https://brainlife.io/api/warehouse/datalad/datasets?find=%7B%22removed%22%3Afalse%2C%22path%22%3A%7B%22%24regex%22%3A%22%5EOpenNeuro%2Fds000001%22%7D%2C%22version%22%3A%221.0.0%22%7D",
|
|
18
18
|
)
|
|
19
19
|
})
|
|
20
20
|
})
|
|
@@ -1,31 +1,32 @@
|
|
|
1
|
+
import { vi } from "vitest"
|
|
1
2
|
import {
|
|
2
|
-
encodeCursor,
|
|
3
3
|
decodeCursor,
|
|
4
4
|
elasticRelayConnection,
|
|
5
|
-
|
|
5
|
+
encodeCursor,
|
|
6
|
+
} from "../dataset-search"
|
|
6
7
|
|
|
7
|
-
vi.mock(
|
|
8
|
-
vi.mock(
|
|
9
|
-
vi.mock(
|
|
8
|
+
vi.mock("ioredis")
|
|
9
|
+
vi.mock("../../../elasticsearch/elastic-client.ts")
|
|
10
|
+
vi.mock("../../../config.ts")
|
|
10
11
|
|
|
11
|
-
describe(
|
|
12
|
-
describe(
|
|
13
|
-
it(
|
|
14
|
-
expect(encodeCursor([2.513,
|
|
15
|
-
|
|
12
|
+
describe("dataset search resolvers", () => {
|
|
13
|
+
describe("encodeCursor()", () => {
|
|
14
|
+
it("returns an encoded string", () => {
|
|
15
|
+
expect(encodeCursor([2.513, "ds00005"])).toEqual(
|
|
16
|
+
"WzIuNTEzLCJkczAwMDA1Il0=",
|
|
16
17
|
)
|
|
17
18
|
})
|
|
18
19
|
})
|
|
19
|
-
describe(
|
|
20
|
-
it(
|
|
21
|
-
expect(decodeCursor(
|
|
20
|
+
describe("decodeCursor()", () => {
|
|
21
|
+
it("returns a decoded array", () => {
|
|
22
|
+
expect(decodeCursor("WzIuNTEzLCJkczAwMDA1Il0=")).toEqual([
|
|
22
23
|
2.513,
|
|
23
|
-
|
|
24
|
+
"ds00005",
|
|
24
25
|
])
|
|
25
26
|
})
|
|
26
27
|
})
|
|
27
|
-
describe(
|
|
28
|
-
it(
|
|
28
|
+
describe("elasticRelayConnection()", () => {
|
|
29
|
+
it("returns a relay cursor for empty ApiResponse", async () => {
|
|
29
30
|
const emptyApiResponse = {
|
|
30
31
|
body: {
|
|
31
32
|
hits: {
|
|
@@ -46,13 +47,14 @@ describe('dataset search resolvers', () => {
|
|
|
46
47
|
hasPreviousPage: false,
|
|
47
48
|
},
|
|
48
49
|
}
|
|
50
|
+
// @ts-expect-error Mock version does not use all arguments
|
|
49
51
|
const connection = await elasticRelayConnection(emptyApiResponse, {
|
|
50
52
|
dataset: vi.fn(),
|
|
51
53
|
})
|
|
52
54
|
expect(connection).toMatchObject(nullRelayConnection)
|
|
53
55
|
})
|
|
54
56
|
|
|
55
|
-
it(
|
|
57
|
+
it("returns a relay cursor for ApiResponse with results", () => {
|
|
56
58
|
const mockResolvers = {
|
|
57
59
|
dataset: vi.fn(),
|
|
58
60
|
}
|
|
@@ -61,9 +63,9 @@ describe('dataset search resolvers', () => {
|
|
|
61
63
|
body: {
|
|
62
64
|
hits: {
|
|
63
65
|
hits: [
|
|
64
|
-
{ _source: { id:
|
|
65
|
-
{ _source: { id:
|
|
66
|
-
{ _source: { id:
|
|
66
|
+
{ _source: { id: "testdataset1" } },
|
|
67
|
+
{ _source: { id: "testdataset2" } },
|
|
68
|
+
{ _source: { id: "testdataset3" }, sort: [1] },
|
|
67
69
|
],
|
|
68
70
|
total: { value: 10 },
|
|
69
71
|
},
|
|
@@ -71,7 +73,7 @@ describe('dataset search resolvers', () => {
|
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
const resultsRelayConnection = {
|
|
74
|
-
edges: expectedApiResponse.body.hits.hits.map(hit => {
|
|
76
|
+
edges: expectedApiResponse.body.hits.hits.map((hit) => {
|
|
75
77
|
// This skips the dataset resolver logic and passes this back
|
|
76
78
|
mockResolvers.dataset.mockReturnValueOnce(hit._source)
|
|
77
79
|
return {
|
|
@@ -81,7 +83,7 @@ describe('dataset search resolvers', () => {
|
|
|
81
83
|
}),
|
|
82
84
|
pageInfo: {
|
|
83
85
|
count: 10,
|
|
84
|
-
endCursor:
|
|
86
|
+
endCursor: "WzFd",
|
|
85
87
|
hasNextPage: true,
|
|
86
88
|
startCursor: null,
|
|
87
89
|
hasPreviousPage: false,
|
|
@@ -89,7 +91,7 @@ describe('dataset search resolvers', () => {
|
|
|
89
91
|
}
|
|
90
92
|
const connection = elasticRelayConnection(
|
|
91
93
|
expectedApiResponse,
|
|
92
|
-
|
|
94
|
+
"test",
|
|
93
95
|
3,
|
|
94
96
|
mockResolvers,
|
|
95
97
|
)
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { vi } from "vitest"
|
|
2
|
+
import { connect } from "mongoose"
|
|
3
|
+
import request from "superagent"
|
|
4
|
+
import * as ds from "../dataset"
|
|
5
|
+
|
|
6
|
+
vi.mock("superagent")
|
|
7
|
+
vi.mock("ioredis")
|
|
8
|
+
vi.mock("../../../config.ts")
|
|
9
|
+
vi.mock("../../../libs/notifications.ts")
|
|
10
|
+
|
|
11
|
+
describe("dataset resolvers", () => {
|
|
12
|
+
beforeAll(() => {
|
|
13
|
+
connect(globalThis.__MONGO_URI__)
|
|
14
|
+
})
|
|
15
|
+
describe("createDataset()", () => {
|
|
16
|
+
it("createDataset mutation succeeds", async () => {
|
|
17
|
+
const { id: dsId } = await ds.createDataset(
|
|
18
|
+
null,
|
|
19
|
+
{ affirmedDefaced: true, affirmedConsent: false },
|
|
20
|
+
{ user: "123456", userInfo: {} },
|
|
21
|
+
)
|
|
22
|
+
expect(dsId).toEqual(expect.stringMatching(/^ds[0-9]{6}$/))
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
describe("snapshotCreationComparison()", () => {
|
|
26
|
+
it('sorts array of objects by the "created" and "tag" properties', () => {
|
|
27
|
+
const testArray = [
|
|
28
|
+
{ id: 2, created: new Date("2018-11-20T00:05:43.473Z"), tag: "1.0.0" },
|
|
29
|
+
{ id: 1, created: new Date("2018-11-19T00:05:43.473Z"), tag: "1.0.1" },
|
|
30
|
+
{ id: 3, created: new Date("2018-11-23T00:05:43.473Z"), tag: "1.0.2" },
|
|
31
|
+
{ id: 5, created: new Date("2018-11-23T00:05:43.473Z"), tag: "1.0.10" },
|
|
32
|
+
{ id: 4, created: new Date("2018-11-23T00:05:43.473Z"), tag: "1.0.3" },
|
|
33
|
+
]
|
|
34
|
+
const sorted = testArray.sort(ds.snapshotCreationComparison)
|
|
35
|
+
expect(sorted[0].id).toBe(2)
|
|
36
|
+
expect(sorted[1].id).toBe(1)
|
|
37
|
+
expect(sorted[2].id).toBe(3)
|
|
38
|
+
expect(sorted[3].id).toBe(4)
|
|
39
|
+
expect(sorted[4].id).toBe(5)
|
|
40
|
+
})
|
|
41
|
+
it('sorts array of objects by the "created" property as strings', () => {
|
|
42
|
+
const testArray = [
|
|
43
|
+
{ id: 2, created: "2018-11-20T00:05:43.473Z", tag: "2.0.0" },
|
|
44
|
+
{ id: 1, created: "2018-11-19T00:05:43.473Z", tag: "1.0.0" },
|
|
45
|
+
{ id: 3, created: "2018-11-23T00:05:43.473Z", tag: "3.0.0" },
|
|
46
|
+
]
|
|
47
|
+
const sorted = testArray.sort(ds.snapshotCreationComparison)
|
|
48
|
+
expect(sorted[0].id).toBe(1)
|
|
49
|
+
expect(sorted[1].id).toBe(2)
|
|
50
|
+
expect(sorted[2].id).toBe(3)
|
|
51
|
+
})
|
|
52
|
+
it("sorts non-semver tags mixed with semver tags", () => {
|
|
53
|
+
const testArray = [
|
|
54
|
+
{ id: 2, created: new Date("2018-11-19T00:05:43.473Z"), tag: "1.0.2" },
|
|
55
|
+
{
|
|
56
|
+
id: 1,
|
|
57
|
+
created: new Date("2018-11-19T00:05:43.473Z"),
|
|
58
|
+
tag: "57fed018cce88d000ac1757f",
|
|
59
|
+
},
|
|
60
|
+
{ id: 3, created: new Date("2018-11-19T00:05:43.473Z"), tag: "1.0.1" },
|
|
61
|
+
]
|
|
62
|
+
const sorted = testArray.sort(ds.snapshotCreationComparison)
|
|
63
|
+
expect(sorted[0].id).toBe(2)
|
|
64
|
+
expect(sorted[1].id).toBe(1)
|
|
65
|
+
expect(sorted[2].id).toBe(3)
|
|
66
|
+
})
|
|
67
|
+
it("sorts snapshots with only non-semver tags", () => {
|
|
68
|
+
const testArray = [
|
|
69
|
+
{
|
|
70
|
+
id: 2,
|
|
71
|
+
created: new Date("2018-11-19T00:05:43.473Z"),
|
|
72
|
+
tag: "00001",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: 1,
|
|
76
|
+
created: new Date("2018-11-19T00:05:43.473Z"),
|
|
77
|
+
tag: "57fed018cce88d000ac1757f",
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: 3,
|
|
81
|
+
created: new Date("2018-11-19T00:05:43.473Z"),
|
|
82
|
+
tag: "57fed018cce88d000ac1757f",
|
|
83
|
+
},
|
|
84
|
+
]
|
|
85
|
+
const sorted = testArray.sort(ds.snapshotCreationComparison)
|
|
86
|
+
expect(sorted[0].id).toBe(2)
|
|
87
|
+
expect(sorted[1].id).toBe(1)
|
|
88
|
+
expect(sorted[2].id).toBe(3)
|
|
89
|
+
})
|
|
90
|
+
it("sorts very similar creation times by semver order", () => {
|
|
91
|
+
const testSnapshots = [
|
|
92
|
+
{
|
|
93
|
+
id: "ds002680:1.0.0",
|
|
94
|
+
created: "2020-04-03T23:19:56.000Z",
|
|
95
|
+
tag: "1.0.0",
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: "ds002680:1.2.0",
|
|
99
|
+
created: "2021-10-19T16:26:43.000Z",
|
|
100
|
+
tag: "1.2.0",
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: "ds002680:1.1.0",
|
|
104
|
+
created: "2021-10-19T16:26:44.000Z",
|
|
105
|
+
tag: "1.1.0",
|
|
106
|
+
},
|
|
107
|
+
]
|
|
108
|
+
const sorted = testSnapshots.sort(ds.snapshotCreationComparison)
|
|
109
|
+
expect(sorted[0].id).toBe("ds002680:1.0.0")
|
|
110
|
+
expect(sorted[1].id).toBe("ds002680:1.1.0")
|
|
111
|
+
expect(sorted[2].id).toBe("ds002680:1.2.0")
|
|
112
|
+
})
|
|
113
|
+
it("sorts 000002 (legacy snapshots) before 1.0.1 (current format)", () => {
|
|
114
|
+
const testSnapshots = [
|
|
115
|
+
{
|
|
116
|
+
id: "ds000247:00002",
|
|
117
|
+
created: "2018-07-18T02:27:39.000Z",
|
|
118
|
+
tag: "00002",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: "ds000247:00001",
|
|
122
|
+
created: "2018-07-18T02:35:37.000Z",
|
|
123
|
+
tag: "00001",
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: "ds000247:1.0.0",
|
|
127
|
+
created: "2021-07-05T15:58:18.000Z",
|
|
128
|
+
tag: "1.0.0",
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: "ds000247:1.0.1",
|
|
132
|
+
created: "2021-08-25T23:37:53.000Z",
|
|
133
|
+
tag: "1.0.1",
|
|
134
|
+
},
|
|
135
|
+
]
|
|
136
|
+
const sorted = testSnapshots.sort(ds.snapshotCreationComparison)
|
|
137
|
+
expect(sorted[0].id).toBe("ds000247:00002")
|
|
138
|
+
expect(sorted[1].id).toBe("ds000247:00001")
|
|
139
|
+
expect(sorted[2].id).toBe("ds000247:1.0.0")
|
|
140
|
+
expect(sorted[3].id).toBe("ds000247:1.0.1")
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
describe("deleteFiles", () => {
|
|
144
|
+
beforeEach(() => {
|
|
145
|
+
request.post.mockClear()
|
|
146
|
+
})
|
|
147
|
+
it("makes correct delete call to datalad", () => {
|
|
148
|
+
// capture and check datalad delete request
|
|
149
|
+
request.del = (url) => ({
|
|
150
|
+
set: (header1, headerValue1) => ({
|
|
151
|
+
set: () => ({
|
|
152
|
+
send: ({ filenames }) => {
|
|
153
|
+
expect(url).toEqual("http://datalad-0/datasets/ds999999/files")
|
|
154
|
+
expect(filenames).toEqual([":sub-99"])
|
|
155
|
+
expect(header1).toEqual("Cookie")
|
|
156
|
+
expect(headerValue1).toMatch(/^accessToken=/)
|
|
157
|
+
},
|
|
158
|
+
}),
|
|
159
|
+
}),
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
return ds.deleteFiles(
|
|
163
|
+
null,
|
|
164
|
+
{ datasetId: "ds999999", files: [{ path: "/sub-99" }] },
|
|
165
|
+
{
|
|
166
|
+
user: "a_user_id",
|
|
167
|
+
userInfo: {
|
|
168
|
+
// bypass permission checks
|
|
169
|
+
admin: true,
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
)
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
})
|
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
import { vi } from
|
|
2
|
-
import {
|
|
1
|
+
import { vi } from "vitest"
|
|
2
|
+
import { derivativeObject, githubDerivativeQuery } from "../derivatives"
|
|
3
3
|
|
|
4
|
-
vi.mock(
|
|
4
|
+
vi.mock("ioredis")
|
|
5
5
|
|
|
6
|
-
describe(
|
|
7
|
-
describe(
|
|
8
|
-
it(
|
|
9
|
-
expect(githubDerivativeQuery(
|
|
10
|
-
|
|
6
|
+
describe("GraphQL derivatives", () => {
|
|
7
|
+
describe("githubDerivativeQuery()", () => {
|
|
8
|
+
it("constructs a correct URL", () => {
|
|
9
|
+
expect(githubDerivativeQuery("ds000102", "mriqc").toString()).toEqual(
|
|
10
|
+
"https://api.github.com/repos/OpenNeuroDerivatives/ds000102-mriqc",
|
|
11
11
|
)
|
|
12
12
|
})
|
|
13
13
|
})
|
|
14
|
-
describe(
|
|
15
|
-
it(
|
|
16
|
-
expect(derivativeObject(
|
|
14
|
+
describe("derivativeObject()", () => {
|
|
15
|
+
it("returns expected values for mriqc", () => {
|
|
16
|
+
expect(derivativeObject("ds000102", "mriqc")).toEqual({
|
|
17
17
|
dataladUrl: new URL(
|
|
18
|
-
|
|
18
|
+
"https://github.com/OpenNeuroDerivatives/ds000102-mriqc.git",
|
|
19
19
|
),
|
|
20
20
|
local: false,
|
|
21
|
-
name:
|
|
22
|
-
s3Url: new URL(
|
|
21
|
+
name: "ds000102-mriqc",
|
|
22
|
+
s3Url: new URL("s3://openneuro-derivatives/mriqc/ds000102-mriqc"),
|
|
23
23
|
})
|
|
24
24
|
})
|
|
25
|
-
it(
|
|
26
|
-
expect(derivativeObject(
|
|
25
|
+
it("returns expected values for fmriprep", () => {
|
|
26
|
+
expect(derivativeObject("ds000102", "fmriprep")).toEqual({
|
|
27
27
|
dataladUrl: new URL(
|
|
28
|
-
|
|
28
|
+
"https://github.com/OpenNeuroDerivatives/ds000102-fmriprep.git",
|
|
29
29
|
),
|
|
30
30
|
local: false,
|
|
31
|
-
name:
|
|
32
|
-
s3Url: new URL(
|
|
31
|
+
name: "ds000102-fmriprep",
|
|
32
|
+
s3Url: new URL("s3://openneuro-derivatives/fmriprep/ds000102-fmriprep"),
|
|
33
33
|
})
|
|
34
34
|
})
|
|
35
35
|
})
|