@openneuro/server 4.22.0 → 4.24.0-alpha.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/Dockerfile +1 -1
- package/package.json +13 -12
- package/src/cache/types.ts +1 -0
- package/src/datalad/__tests__/dataset.spec.ts +5 -2
- package/src/datalad/__tests__/pagination.spec.ts +5 -1
- package/src/datalad/__tests__/snapshots.spec.ts +7 -1
- package/src/datalad/description.ts +6 -0
- package/src/datalad/draft.ts +13 -8
- package/src/graphql/__tests__/__snapshots__/permissions.spec.ts.snap +2 -2
- package/src/graphql/__tests__/comment.spec.ts +5 -1
- package/src/graphql/resolvers/__tests__/dataset-search.spec.ts +11 -15
- package/src/graphql/resolvers/__tests__/dataset.spec.ts +6 -2
- package/src/graphql/resolvers/dataset-search.ts +6 -6
- package/src/graphql/resolvers/dataset.ts +2 -1
- package/src/graphql/schema.ts +2 -0
- package/src/handlers/datalad.ts +4 -1
- package/src/libs/__tests__/dataset.spec.ts +7 -3
- package/src/libs/doi/__tests__/__snapshots__/doi.spec.ts.snap +5 -5
- package/src/libs/email/templates/__tests__/__snapshots__/comment-created.spec.ts.snap +7 -7
- package/src/libs/email/templates/__tests__/__snapshots__/dataset-deleted.spec.ts.snap +3 -3
- package/src/libs/email/templates/__tests__/__snapshots__/owner-unsubscribed.spec.ts.snap +3 -3
- package/src/libs/email/templates/__tests__/__snapshots__/snapshot-created.spec.ts.snap +6 -6
- package/src/libs/email/templates/__tests__/__snapshots__/snapshot-reminder.spec.ts.snap +4 -4
- package/src/types/promiseTimeoutError.ts +6 -0
- package/src/utils/__tests__/promiseTimeout.spec.ts +16 -0
- package/src/utils/promiseTimeout.ts +31 -0
package/Dockerfile
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openneuro/server",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.24.0-alpha.0",
|
|
4
4
|
"description": "Core service for the OpenNeuro platform.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "src/server.js",
|
|
@@ -18,10 +18,10 @@
|
|
|
18
18
|
"@apollo/client": "3.7.2",
|
|
19
19
|
"@apollo/server": "4.9.3",
|
|
20
20
|
"@apollo/utils.keyvadapter": "3.0.0",
|
|
21
|
-
"@elastic/elasticsearch": "
|
|
21
|
+
"@elastic/elasticsearch": "8.13.1",
|
|
22
22
|
"@graphql-tools/schema": "^10.0.0",
|
|
23
23
|
"@keyv/redis": "^2.7.0",
|
|
24
|
-
"@openneuro/search": "^4.
|
|
24
|
+
"@openneuro/search": "^4.24.0-alpha.0",
|
|
25
25
|
"@passport-next/passport-google-oauth2": "^1.0.0",
|
|
26
26
|
"@sentry/node": "^4.5.3",
|
|
27
27
|
"base64url": "^3.0.0",
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"date-fns": "^2.16.1",
|
|
31
31
|
"draft-js": "^0.11.7",
|
|
32
32
|
"draft-js-export-html": "^1.4.1",
|
|
33
|
-
"elastic-apm-node": "
|
|
34
|
-
"express": "4.
|
|
33
|
+
"elastic-apm-node": "4.5.4",
|
|
34
|
+
"express": "4.19.2",
|
|
35
35
|
"graphql": "16.8.1",
|
|
36
36
|
"graphql-bigint": "^1.0.0",
|
|
37
37
|
"graphql-compose": "9.0.10",
|
|
@@ -39,11 +39,12 @@
|
|
|
39
39
|
"graphql-tools": "9.0.0",
|
|
40
40
|
"immutable": "^3.8.2",
|
|
41
41
|
"ioredis": "4.17.3",
|
|
42
|
-
"jsdom": "
|
|
42
|
+
"jsdom": "24.0.0",
|
|
43
43
|
"jsonwebtoken": "^9.0.0",
|
|
44
44
|
"keyv": "^4.5.3",
|
|
45
45
|
"mime-types": "^2.1.19",
|
|
46
|
-
"
|
|
46
|
+
"mongodb-memory-server": "^9.2.0",
|
|
47
|
+
"mongoose": "6.12.8",
|
|
47
48
|
"morgan": "^1.6.1",
|
|
48
49
|
"node-mailjet": "^3.3.5",
|
|
49
50
|
"object-hash": "2.1.1",
|
|
@@ -52,8 +53,8 @@
|
|
|
52
53
|
"passport-jwt": "^4.0.0",
|
|
53
54
|
"passport-oauth2-refresh": "^2.0.0",
|
|
54
55
|
"passport-orcid": "^0.0.3",
|
|
55
|
-
"react": "^
|
|
56
|
-
"react-dom": "^
|
|
56
|
+
"react": "^18.2.0",
|
|
57
|
+
"react-dom": "^18.2.0",
|
|
57
58
|
"redlock": "^4.0.0",
|
|
58
59
|
"request": "^2.83.0",
|
|
59
60
|
"semver": "^5.5.0",
|
|
@@ -76,14 +77,14 @@
|
|
|
76
77
|
"@types/semver": "^5",
|
|
77
78
|
"core-js": "^3.10.1",
|
|
78
79
|
"ioredis-mock": "^8.8.1",
|
|
79
|
-
"nodemon": "
|
|
80
|
+
"nodemon": "3.1.0",
|
|
80
81
|
"ts-node-dev": "1.1.6",
|
|
81
82
|
"tsc-watch": "^4.2.9",
|
|
82
|
-
"vitest": "
|
|
83
|
+
"vitest": "1.5.0",
|
|
83
84
|
"vitest-fetch-mock": "0.2.2"
|
|
84
85
|
},
|
|
85
86
|
"publishConfig": {
|
|
86
87
|
"access": "public"
|
|
87
88
|
},
|
|
88
|
-
"gitHead": "
|
|
89
|
+
"gitHead": "4e271231c20d21e4c8b5ba8851d67cb94b7f739a"
|
|
89
90
|
}
|
package/src/cache/types.ts
CHANGED
|
@@ -3,6 +3,7 @@ import request from "superagent"
|
|
|
3
3
|
import { createDataset, datasetsFilter, testBlacklist } from "../dataset"
|
|
4
4
|
import { getDatasetWorker } from "../../libs/datalad-service"
|
|
5
5
|
import { connect } from "mongoose"
|
|
6
|
+
import { MongoMemoryServer } from "mongodb-memory-server"
|
|
6
7
|
|
|
7
8
|
// Mock requests to Datalad service
|
|
8
9
|
vi.mock("superagent")
|
|
@@ -13,9 +14,11 @@ vi.mock("../../libs/notifications")
|
|
|
13
14
|
|
|
14
15
|
describe("dataset model operations", () => {
|
|
15
16
|
describe("createDataset()", () => {
|
|
16
|
-
|
|
17
|
+
let mongod
|
|
18
|
+
beforeAll(async () => {
|
|
17
19
|
// Setup MongoDB with mongodb-memory-server
|
|
18
|
-
|
|
20
|
+
mongod = await MongoMemoryServer.create()
|
|
21
|
+
connect(mongod.getUri())
|
|
19
22
|
})
|
|
20
23
|
it("resolves to dataset id string", async () => {
|
|
21
24
|
const user = { id: "1234" }
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { vi } from "vitest"
|
|
2
2
|
vi.mock("ioredis")
|
|
3
|
+
import { MongoMemoryServer } from "mongodb-memory-server"
|
|
3
4
|
import * as pagination from "../pagination.js"
|
|
4
5
|
import { connect, Types } from "mongoose"
|
|
5
6
|
import Dataset from "../../models/dataset"
|
|
@@ -37,8 +38,11 @@ describe("pagination model operations", () => {
|
|
|
37
38
|
})
|
|
38
39
|
})
|
|
39
40
|
describe("datasetsConnection()", () => {
|
|
41
|
+
let mongod
|
|
40
42
|
beforeAll(async () => {
|
|
41
|
-
|
|
43
|
+
// Setup MongoDB with mongodb-memory-server
|
|
44
|
+
mongod = await MongoMemoryServer.create()
|
|
45
|
+
connect(mongod.getUri())
|
|
42
46
|
const ds = new Dataset({
|
|
43
47
|
_id: new ObjectID("5bef51a1ed211400c08e5524"),
|
|
44
48
|
id: "ds001001",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { vi } from "vitest"
|
|
2
2
|
vi.mock("ioredis")
|
|
3
|
+
import { MongoMemoryServer } from "mongodb-memory-server"
|
|
3
4
|
import request from "superagent"
|
|
4
5
|
import { createDataset } from "../dataset"
|
|
5
6
|
import { createSnapshot } from "../snapshots"
|
|
@@ -25,10 +26,15 @@ vi.mock("../../libs/notifications.ts")
|
|
|
25
26
|
|
|
26
27
|
describe("snapshot model operations", () => {
|
|
27
28
|
describe("createSnapshot()", () => {
|
|
29
|
+
let mongod
|
|
30
|
+
beforeAll(async () => {
|
|
31
|
+
// Setup MongoDB with mongodb-memory-server
|
|
32
|
+
mongod = await MongoMemoryServer.create()
|
|
33
|
+
connect(mongod.getUri())
|
|
34
|
+
})
|
|
28
35
|
it("posts to the DataLad /datasets/{dsId}/snapshots/{snapshot} endpoint", async () => {
|
|
29
36
|
const user = { id: "1234" }
|
|
30
37
|
const tag = "snapshot"
|
|
31
|
-
await connect(globalThis.__MONGO_URI__)
|
|
32
38
|
const { id: dsId } = await createDataset(user.id, user, {
|
|
33
39
|
affirmedDefaced: true,
|
|
34
40
|
affirmedConsent: true,
|
|
@@ -88,6 +88,12 @@ export const repairDescriptionTypes = (description) => {
|
|
|
88
88
|
newDescription.HowToAcknowledge =
|
|
89
89
|
JSON.stringify(description.HowToAcknowledge) || ""
|
|
90
90
|
}
|
|
91
|
+
if (
|
|
92
|
+
description.hasOwnProperty("DatasetType") &&
|
|
93
|
+
typeof description.DatasetType !== "string"
|
|
94
|
+
) {
|
|
95
|
+
newDescription.DatasetType = "raw"
|
|
96
|
+
}
|
|
91
97
|
return newDescription
|
|
92
98
|
}
|
|
93
99
|
|
package/src/datalad/draft.ts
CHANGED
|
@@ -4,16 +4,21 @@
|
|
|
4
4
|
import request from "superagent"
|
|
5
5
|
import Dataset from "../models/dataset"
|
|
6
6
|
import { getDatasetWorker } from "../libs/datalad-service"
|
|
7
|
+
import CacheItem, { CacheType } from "../cache/item"
|
|
8
|
+
import { redis } from "../libs/redis"
|
|
7
9
|
|
|
8
10
|
export const getDraftRevision = async (datasetId) => {
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
const cache = new CacheItem(redis, CacheType.draftRevision, [datasetId], 10)
|
|
12
|
+
return cache.get(async (_doNotCache): Promise<string> => {
|
|
13
|
+
const draftUrl = `http://${
|
|
14
|
+
getDatasetWorker(
|
|
15
|
+
datasetId,
|
|
16
|
+
)
|
|
17
|
+
}/datasets/${datasetId}/draft`
|
|
18
|
+
const response = await fetch(draftUrl)
|
|
19
|
+
const { hexsha } = await response.json()
|
|
20
|
+
return hexsha
|
|
21
|
+
})
|
|
17
22
|
}
|
|
18
23
|
|
|
19
24
|
export const updateDatasetRevision = (datasetId, gitRef) => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
2
|
|
|
3
|
-
exports[`resolver permissions helpers > checkDatasetAdmin() > resolves to false for anonymous users 1`] = `
|
|
3
|
+
exports[`resolver permissions helpers > checkDatasetAdmin() > resolves to false for anonymous users 1`] = `[Error: You do not have admin access to this dataset.]`;
|
|
4
4
|
|
|
5
|
-
exports[`resolver permissions helpers > checkDatasetWrite() > resolves to false for anonymous users 1`] = `
|
|
5
|
+
exports[`resolver permissions helpers > checkDatasetWrite() > resolves to false for anonymous users 1`] = `[Error: You do not have access to modify this dataset.]`;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { vi } from "vitest"
|
|
2
2
|
import { connect } from "mongoose"
|
|
3
3
|
import { deleteComment, flatten } from "../resolvers/comment"
|
|
4
|
+
import { MongoMemoryServer } from "mongodb-memory-server"
|
|
4
5
|
import Comment from "../../models/comment"
|
|
5
6
|
|
|
6
7
|
vi.mock("ioredis")
|
|
@@ -16,8 +17,11 @@ describe("comment resolver helpers", () => {
|
|
|
16
17
|
user: "5678",
|
|
17
18
|
userInfo: { admin: false },
|
|
18
19
|
}
|
|
20
|
+
let mongod
|
|
19
21
|
beforeAll(async () => {
|
|
20
|
-
|
|
22
|
+
// Setup MongoDB with mongodb-memory-server
|
|
23
|
+
mongod = await MongoMemoryServer.create()
|
|
24
|
+
connect(mongod.getUri())
|
|
21
25
|
const comment = new Comment({
|
|
22
26
|
text: "a",
|
|
23
27
|
createDate: new Date().toISOString(),
|
|
@@ -28,11 +28,9 @@ describe("dataset search resolvers", () => {
|
|
|
28
28
|
describe("elasticRelayConnection()", () => {
|
|
29
29
|
it("returns a relay cursor for empty ApiResponse", async () => {
|
|
30
30
|
const emptyApiResponse = {
|
|
31
|
-
|
|
32
|
-
hits:
|
|
33
|
-
|
|
34
|
-
total: { value: 0 },
|
|
35
|
-
},
|
|
31
|
+
hits: {
|
|
32
|
+
hits: [],
|
|
33
|
+
total: { value: 0 },
|
|
36
34
|
},
|
|
37
35
|
}
|
|
38
36
|
|
|
@@ -60,20 +58,18 @@ describe("dataset search resolvers", () => {
|
|
|
60
58
|
}
|
|
61
59
|
|
|
62
60
|
const expectedApiResponse = {
|
|
63
|
-
|
|
64
|
-
hits:
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
total: { value: 10 },
|
|
71
|
-
},
|
|
61
|
+
hits: {
|
|
62
|
+
hits: [
|
|
63
|
+
{ _source: { id: "testdataset1" } },
|
|
64
|
+
{ _source: { id: "testdataset2" } },
|
|
65
|
+
{ _source: { id: "testdataset3" }, sort: [1] },
|
|
66
|
+
],
|
|
67
|
+
total: { value: 10 },
|
|
72
68
|
},
|
|
73
69
|
}
|
|
74
70
|
|
|
75
71
|
const resultsRelayConnection = {
|
|
76
|
-
edges: expectedApiResponse.
|
|
72
|
+
edges: expectedApiResponse.hits.hits.map((hit) => {
|
|
77
73
|
// This skips the dataset resolver logic and passes this back
|
|
78
74
|
mockResolvers.dataset.mockReturnValueOnce(hit._source)
|
|
79
75
|
return {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { vi } from "vitest"
|
|
2
|
+
import { MongoMemoryServer } from "mongodb-memory-server"
|
|
2
3
|
import { connect } from "mongoose"
|
|
3
4
|
import request from "superagent"
|
|
4
5
|
import * as ds from "../dataset"
|
|
@@ -9,8 +10,11 @@ vi.mock("../../../config.ts")
|
|
|
9
10
|
vi.mock("../../../libs/notifications.ts")
|
|
10
11
|
|
|
11
12
|
describe("dataset resolvers", () => {
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
let mongod
|
|
14
|
+
beforeAll(async () => {
|
|
15
|
+
// Setup MongoDB with mongodb-memory-server
|
|
16
|
+
mongod = await MongoMemoryServer.create()
|
|
17
|
+
connect(mongod.getUri())
|
|
14
18
|
})
|
|
15
19
|
describe("createDataset()", () => {
|
|
16
20
|
it("createDataset mutation succeeds", async () => {
|
|
@@ -37,7 +37,7 @@ export const decodeCursor = (cursor) =>
|
|
|
37
37
|
* @param {import ('@elastic/elasticsearch').ApiResponse} result
|
|
38
38
|
*/
|
|
39
39
|
export const elasticRelayConnection = (
|
|
40
|
-
|
|
40
|
+
body,
|
|
41
41
|
id,
|
|
42
42
|
size,
|
|
43
43
|
childResolvers = { dataset },
|
|
@@ -102,9 +102,9 @@ export const datasetSearchConnection = async (
|
|
|
102
102
|
index: elasticIndex,
|
|
103
103
|
size: first,
|
|
104
104
|
q: `${q} AND public:true`,
|
|
105
|
-
|
|
105
|
+
...requestBody,
|
|
106
106
|
})
|
|
107
|
-
return elasticRelayConnection(
|
|
107
|
+
return elasticRelayConnection(requestBody, searchId, first)
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
export const datasetSearch = {
|
|
@@ -238,13 +238,13 @@ export const advancedDatasetSearchConnection = async (
|
|
|
238
238
|
// Don't include search_after if parsing fails
|
|
239
239
|
}
|
|
240
240
|
}
|
|
241
|
-
|
|
241
|
+
await elasticClient.search({
|
|
242
242
|
index: elasticIndex,
|
|
243
243
|
size: first,
|
|
244
|
-
|
|
244
|
+
...requestBody,
|
|
245
245
|
})
|
|
246
246
|
return elasticRelayConnection(
|
|
247
|
-
|
|
247
|
+
requestBody,
|
|
248
248
|
searchId,
|
|
249
249
|
first,
|
|
250
250
|
undefined,
|
|
@@ -20,11 +20,12 @@ import { getDatasetWorker } from "../../libs/datalad-service"
|
|
|
20
20
|
import { getFileName } from "../../datalad/files"
|
|
21
21
|
import { onBrainlife } from "./brainlife"
|
|
22
22
|
import { derivatives } from "./derivatives"
|
|
23
|
+
import { promiseTimeout } from "../../utils/promiseTimeout"
|
|
23
24
|
import semver from "semver"
|
|
24
25
|
|
|
25
26
|
export const dataset = async (obj, { id }, { user, userInfo }) => {
|
|
26
27
|
await checkDatasetRead(id, user, userInfo)
|
|
27
|
-
return datalad.getDataset(id)
|
|
28
|
+
return promiseTimeout(datalad.getDataset(id), 30000)
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
export const datasets = (parent, args, { user, userInfo }) => {
|
package/src/graphql/schema.ts
CHANGED
|
@@ -571,6 +571,8 @@ export const typeDefs = `
|
|
|
571
571
|
ReferencesAndLinks: [String]
|
|
572
572
|
# The Document Object Identifier of the dataset (not the corresponding paper).
|
|
573
573
|
DatasetDOI: String
|
|
574
|
+
# The BIDS DatasetType field defined as "raw" or "derivative"
|
|
575
|
+
DatasetType: String
|
|
574
576
|
# List of ethics committee approvals of the research protocols and/or protocol identifiers.
|
|
575
577
|
EthicsApprovals: [String]
|
|
576
578
|
}
|
package/src/handlers/datalad.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { Readable } from "node:stream"
|
|
|
3
3
|
import mime from "mime-types"
|
|
4
4
|
import { getFiles } from "../datalad/files"
|
|
5
5
|
import { getDatasetWorker } from "../libs/datalad-service"
|
|
6
|
+
import { getDraftRevision } from "../datalad/draft"
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Handlers for datalad dataset manipulation
|
|
@@ -21,7 +22,9 @@ export const getFile = async (req, res) => {
|
|
|
21
22
|
const worker = getDatasetWorker(datasetId)
|
|
22
23
|
// Find the right tree
|
|
23
24
|
const pathComponents = filename.split(":")
|
|
24
|
-
|
|
25
|
+
// Get the draft commit for cache busting
|
|
26
|
+
const draftCommit = await getDraftRevision(datasetId)
|
|
27
|
+
let tree = snapshotId || draftCommit
|
|
25
28
|
let file
|
|
26
29
|
for (const level of pathComponents) {
|
|
27
30
|
try {
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { vi } from "vitest"
|
|
2
|
-
import {
|
|
2
|
+
import { MongoMemoryServer } from "mongodb-memory-server"
|
|
3
|
+
import { connect, disconnect } from "mongoose"
|
|
3
4
|
import { getAccessionNumber } from "../dataset"
|
|
4
5
|
|
|
5
6
|
vi.mock("ioredis")
|
|
6
7
|
|
|
7
8
|
describe("libs/dataset", () => {
|
|
8
9
|
describe("getAccessionNumber", () => {
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
let mongod
|
|
11
|
+
beforeAll(async () => {
|
|
12
|
+
// Setup MongoDB with mongodb-memory-server
|
|
13
|
+
mongod = await MongoMemoryServer.create()
|
|
14
|
+
await connect(mongod.getUri())
|
|
11
15
|
})
|
|
12
16
|
it('returns strings starting with "ds"', async () => {
|
|
13
17
|
const ds = await getAccessionNumber()
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
2
|
|
|
3
3
|
exports[`DOI minting utils > template() > accepts expected arguments 1`] = `
|
|
4
|
-
"<?xml version
|
|
5
|
-
<resource xmlns:xsi
|
|
6
|
-
<identifier identifierType
|
|
4
|
+
"<?xml version="1.0" encoding="UTF-8"?>
|
|
5
|
+
<resource xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://datacite.org/schema/kernel-4" xsi:schemaLocation="http://datacite.org/schema/kernel-4 http://schema.datacite.org/meta/kernel-4/metadata.xsd">
|
|
6
|
+
<identifier identifierType="DOI">12345</identifier>
|
|
7
7
|
<creators>
|
|
8
8
|
<creator><creatorName>A. User</creatorName></creator><creator><creatorName>B. User</creatorName></creator>
|
|
9
9
|
</creators>
|
|
10
10
|
<titles>
|
|
11
|
-
<title xml:lang
|
|
11
|
+
<title xml:lang="en-us">Test Dataset</title>
|
|
12
12
|
</titles>
|
|
13
13
|
<publisher>Openneuro</publisher>
|
|
14
14
|
<publicationYear>1999</publicationYear>
|
|
15
|
-
<resourceType resourceTypeGeneral
|
|
15
|
+
<resourceType resourceTypeGeneral="Dataset">fMRI</resourceType>
|
|
16
16
|
</resource>"
|
|
17
17
|
`;
|
|
@@ -56,22 +56,22 @@ exports[`email template -> comment created > renders with expected arguments 1`]
|
|
|
56
56
|
</style>
|
|
57
57
|
</head>
|
|
58
58
|
<body>
|
|
59
|
-
<div class
|
|
60
|
-
<img src
|
|
59
|
+
<div class="top-bar">
|
|
60
|
+
<img src="https://openneuro.org/assets/email-header.1cb8bf76.png" />
|
|
61
61
|
</div>
|
|
62
|
-
<div class
|
|
62
|
+
<div class="content">
|
|
63
63
|
<h2>Hi, J. Doe</h2>
|
|
64
64
|
|
|
65
65
|
<p>
|
|
66
66
|
A new new has been posted on a dataset you follow, <b>Not Real Dataset</b>.
|
|
67
67
|
</p>
|
|
68
|
-
<div class
|
|
68
|
+
<div class="comment">
|
|
69
69
|
<p>By: <b>56789</b> on 2063-04-05</p>
|
|
70
70
|
|
|
71
71
|
Test comment, please ignore
|
|
72
72
|
</div>
|
|
73
|
-
<div class
|
|
74
|
-
<a class
|
|
73
|
+
<div class="link-div">
|
|
74
|
+
<a class="dataset-link" href="https://openneuro.org/datasets/ds1245678#comment-12345">Click here to view this comment on OpenNeuro »</a>
|
|
75
75
|
</div>
|
|
76
76
|
|
|
77
77
|
<p>
|
|
@@ -82,7 +82,7 @@ exports[`email template -> comment created > renders with expected arguments 1`]
|
|
|
82
82
|
</div>
|
|
83
83
|
</body>
|
|
84
84
|
<footer>
|
|
85
|
-
If you would like to stop receiving notifications about this dataset, please <a class='link' href
|
|
85
|
+
If you would like to stop receiving notifications about this dataset, please <a class='link' href="https://openneuro.org/datasets/ds1245678">visit the dataset page</a> and click the 'unfollow' icon.
|
|
86
86
|
</footer>
|
|
87
87
|
<html>"
|
|
88
88
|
`;
|
|
@@ -38,10 +38,10 @@ exports[`email template -> comment created > renders with expected arguments 1`]
|
|
|
38
38
|
</style>
|
|
39
39
|
</head>
|
|
40
40
|
<body>
|
|
41
|
-
<div class
|
|
42
|
-
<img src
|
|
41
|
+
<div class="top-bar">
|
|
42
|
+
<img src="https://openneuro.org/assets/email-header.1cb8bf76.png" />
|
|
43
43
|
</div>
|
|
44
|
-
<div class
|
|
44
|
+
<div class="content">
|
|
45
45
|
<h2>Hi, J. Doe</h2>
|
|
46
46
|
|
|
47
47
|
<p>
|
|
@@ -38,10 +38,10 @@ exports[`email template -> comment created > renders with expected arguments 1`]
|
|
|
38
38
|
</style>
|
|
39
39
|
</head>
|
|
40
40
|
<body>
|
|
41
|
-
<div class
|
|
42
|
-
<img src
|
|
41
|
+
<div class="top-bar">
|
|
42
|
+
<img src="https://openneuro.org/assets/email-header.1cb8bf76.png" />
|
|
43
43
|
</div>
|
|
44
|
-
<div class
|
|
44
|
+
<div class="content">
|
|
45
45
|
<h2>Hi, J. Doe</h2>
|
|
46
46
|
|
|
47
47
|
<p>
|
|
@@ -56,10 +56,10 @@ exports[`email template -> comment created > renders with expected arguments 1`]
|
|
|
56
56
|
</style>
|
|
57
57
|
</head>
|
|
58
58
|
<body>
|
|
59
|
-
<div class
|
|
60
|
-
<img src
|
|
59
|
+
<div class="top-bar">
|
|
60
|
+
<img src="https://openneuro.org/assets/email-header.1cb8bf76.png" />
|
|
61
61
|
</div>
|
|
62
|
-
<div class
|
|
62
|
+
<div class="content">
|
|
63
63
|
<h2>Hi, J. Doe</h2>
|
|
64
64
|
|
|
65
65
|
<p>
|
|
@@ -67,12 +67,12 @@ exports[`email template -> comment created > renders with expected arguments 1`]
|
|
|
67
67
|
</p>
|
|
68
68
|
|
|
69
69
|
<div>
|
|
70
|
-
<a class='dataset-link' href
|
|
70
|
+
<a class='dataset-link' href="https://openneuro.org/datasets/ds1245678/versions/1.2.4">Click here to view this snapshot on OpenNeuro »</a>
|
|
71
71
|
</div>
|
|
72
72
|
|
|
73
73
|
<div>
|
|
74
74
|
<h4>Changelog</h4>
|
|
75
|
-
<p class
|
|
75
|
+
<p class="changelog">New changes...</p>
|
|
76
76
|
</div>
|
|
77
77
|
|
|
78
78
|
<p>
|
|
@@ -82,7 +82,7 @@ exports[`email template -> comment created > renders with expected arguments 1`]
|
|
|
82
82
|
</div>
|
|
83
83
|
</body>
|
|
84
84
|
<footer>
|
|
85
|
-
If you would like to stop receiving notifications about this dataset, please <a class='link' href
|
|
85
|
+
If you would like to stop receiving notifications about this dataset, please <a class='link' href="https://openneuro.org/datasets/ds1245678">visit the dataset page</a> and click the 'unfollow' icon.
|
|
86
86
|
</footer>
|
|
87
87
|
<html>"
|
|
88
88
|
`;
|
|
@@ -38,10 +38,10 @@ exports[`email template -> comment created > renders with expected arguments 1`]
|
|
|
38
38
|
</style>
|
|
39
39
|
</head>
|
|
40
40
|
<body>
|
|
41
|
-
<div class
|
|
42
|
-
<img src
|
|
41
|
+
<div class="top-bar">
|
|
42
|
+
<img src="https://openneuro.org/assets/email-header.1cb8bf76.png" />
|
|
43
43
|
</div>
|
|
44
|
-
<div class
|
|
44
|
+
<div class="content">
|
|
45
45
|
<h2>Hi, J. Doe</h2>
|
|
46
46
|
|
|
47
47
|
<p>
|
|
@@ -53,7 +53,7 @@ exports[`email template -> comment created > renders with expected arguments 1`]
|
|
|
53
53
|
The CRN Team
|
|
54
54
|
</p>
|
|
55
55
|
|
|
56
|
-
<a class
|
|
56
|
+
<a class="dataset-link" href="https://openneuro.org/datasets/ds12345678/snapshot">Create a snapshot. »</a>
|
|
57
57
|
</div>
|
|
58
58
|
</body>
|
|
59
59
|
<html>"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { promiseTimeout } from "../promiseTimeout"
|
|
2
|
+
|
|
3
|
+
describe("withTimeout", () => {
|
|
4
|
+
it("rejects with null when the promise exceeds timeout milliseconds", async () => {
|
|
5
|
+
const slowPromise = new Promise((resolve) =>
|
|
6
|
+
setTimeout(() => resolve("Slow Result"), 500)
|
|
7
|
+
)
|
|
8
|
+
expect(await promiseTimeout(slowPromise, 100)).toBeNull()
|
|
9
|
+
})
|
|
10
|
+
it("resolves when the promise returns before timeout", async () => {
|
|
11
|
+
const slowPromise = new Promise((resolve) =>
|
|
12
|
+
setTimeout(() => resolve("Fast Result"), 10)
|
|
13
|
+
)
|
|
14
|
+
expect(await promiseTimeout(slowPromise, 500)).toBe("Fast Result")
|
|
15
|
+
})
|
|
16
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { PromiseTimeoutError } from "../types/promiseTimeoutError"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Add a timeout to a promise and return null if timeout occurs before the promise resolves
|
|
5
|
+
* @param promise Promise to timeout
|
|
6
|
+
* @param timeout Timeout in milliseconds
|
|
7
|
+
*/
|
|
8
|
+
export async function promiseTimeout<T>(
|
|
9
|
+
promise: Promise<T>,
|
|
10
|
+
timeout: number,
|
|
11
|
+
): Promise<T | null> {
|
|
12
|
+
try {
|
|
13
|
+
const result = await Promise.race([
|
|
14
|
+
promise,
|
|
15
|
+
new Promise((_, reject) =>
|
|
16
|
+
setTimeout(
|
|
17
|
+
() => reject(new PromiseTimeoutError("Operation timed out")),
|
|
18
|
+
timeout,
|
|
19
|
+
)
|
|
20
|
+
),
|
|
21
|
+
])
|
|
22
|
+
// @ts-expect-error This does return the original promise except in failure cases where it returns the expected null
|
|
23
|
+
return result
|
|
24
|
+
} catch (error) {
|
|
25
|
+
if (error instanceof PromiseTimeoutError) {
|
|
26
|
+
return null
|
|
27
|
+
} else {
|
|
28
|
+
throw error // Re-throw other errors
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|