@openneuro/server 4.4.10 → 4.6.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/package.json +3 -2
- package/src/datalad/__tests__/files.spec.js +13 -0
- package/src/datalad/description.js +1 -0
- package/src/datalad/files.js +11 -1
- package/src/datalad/snapshots.js +8 -3
- package/src/elasticsearch/elastic-client.js +1 -1
- package/src/elasticsearch/reindex-dataset.ts +41 -0
- package/src/graphql/resolvers/dataset.js +1 -5
- package/src/graphql/resolvers/draft.js +8 -1
- package/src/graphql/resolvers/permissions.js +3 -1
- package/src/graphql/resolvers/snapshots.js +3 -0
- package/src/graphql/schema.js +4 -0
- package/src/graphql/utils/file.js +0 -29
- package/src/models/user.ts +10 -0
- package/tsconfig.json +4 -1
- package/jestsetup.js +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openneuro/server",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.6.0-alpha.0",
|
|
4
4
|
"description": "Core service for the OpenNeuro platform.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "src/server.js",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@apollo/client": "3.4.17",
|
|
20
20
|
"@elastic/elasticsearch": "7.15.0",
|
|
21
|
+
"@openneuro/search": "^4.6.0-alpha.0",
|
|
21
22
|
"@passport-next/passport-google-oauth2": "^1.0.0",
|
|
22
23
|
"@sentry/node": "^4.5.3",
|
|
23
24
|
"apollo-server": "2.25.3",
|
|
@@ -104,5 +105,5 @@
|
|
|
104
105
|
"publishConfig": {
|
|
105
106
|
"access": "public"
|
|
106
107
|
},
|
|
107
|
-
"gitHead": "
|
|
108
|
+
"gitHead": "ce2db27f750c2614e9cf2f0461add04ca8c3cb48"
|
|
108
109
|
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
decodeFilePath,
|
|
4
4
|
fileUrl,
|
|
5
5
|
filterFiles,
|
|
6
|
+
computeTotalSize,
|
|
6
7
|
} from '../files.js'
|
|
7
8
|
|
|
8
9
|
jest.mock('../../config.js')
|
|
@@ -104,4 +105,16 @@ describe('datalad files', () => {
|
|
|
104
105
|
expect(filterFiles('sub-01/func')(mockFiles)).toEqual([mockSub01[1]])
|
|
105
106
|
})
|
|
106
107
|
})
|
|
108
|
+
describe('computeTotalSize()', () => {
|
|
109
|
+
const mockFileSizes = [
|
|
110
|
+
{ filename: 'README', size: 234 },
|
|
111
|
+
{ filename: 'dataset_description.json', size: 432 },
|
|
112
|
+
{ filename: 'sub-01/anat/sub-01_T1w.nii.gz', size: 10858 },
|
|
113
|
+
{
|
|
114
|
+
filename: 'sub-01/func/sub-01_task-onebacktask_run-01_bold.nii.gz',
|
|
115
|
+
size: 1945682,
|
|
116
|
+
},
|
|
117
|
+
]
|
|
118
|
+
expect(computeTotalSize(mockFileSizes)).toBe(1957206)
|
|
119
|
+
})
|
|
107
120
|
})
|
|
@@ -127,6 +127,7 @@ export const description = obj => {
|
|
|
127
127
|
return cache
|
|
128
128
|
.get(() => {
|
|
129
129
|
return getFiles(datasetId, revision)
|
|
130
|
+
.then(response => response.files)
|
|
130
131
|
.then(getDescriptionObject(datasetId))
|
|
131
132
|
.then(uncachedDescription => ({ id: revision, ...uncachedDescription }))
|
|
132
133
|
})
|
package/src/datalad/files.js
CHANGED
|
@@ -51,6 +51,12 @@ export const fileUrl = (datasetId, path, filename) => {
|
|
|
51
51
|
export const filesUrl = datasetId =>
|
|
52
52
|
`http://${getDatasetWorker(datasetId)}/datasets/${datasetId}/files`
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Sum all file sizes for total dataset size
|
|
56
|
+
*/
|
|
57
|
+
export const computeTotalSize = files =>
|
|
58
|
+
files.reduce((size, f) => size + f.size, 0)
|
|
59
|
+
|
|
54
60
|
/**
|
|
55
61
|
* Get files for a specific revision
|
|
56
62
|
* Similar to getDraftFiles but different cache key and fixed revisions
|
|
@@ -75,7 +81,11 @@ export const getFiles = (datasetId, hexsha) => {
|
|
|
75
81
|
const {
|
|
76
82
|
body: { files },
|
|
77
83
|
} = response
|
|
78
|
-
|
|
84
|
+
const size = computeTotalSize(files)
|
|
85
|
+
return {
|
|
86
|
+
files: files.map(addFileUrl(datasetId, hexsha)),
|
|
87
|
+
size,
|
|
88
|
+
}
|
|
79
89
|
}
|
|
80
90
|
}),
|
|
81
91
|
)
|
package/src/datalad/snapshots.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Get snapshots from datalad-service tags
|
|
3
3
|
*/
|
|
4
4
|
import request from 'superagent'
|
|
5
|
+
import { reindexDataset } from '../elasticsearch/reindex-dataset'
|
|
5
6
|
import { redis, redlock } from '../libs/redis'
|
|
6
7
|
import CacheItem, { CacheType } from '../cache/item'
|
|
7
8
|
import config from '../config.js'
|
|
@@ -154,7 +155,9 @@ export const createSnapshot = async (
|
|
|
154
155
|
snapshotChanges,
|
|
155
156
|
)
|
|
156
157
|
snapshot.created = new Date()
|
|
157
|
-
|
|
158
|
+
const { files, size } = await getFiles(datasetId, tag)
|
|
159
|
+
snapshot.files = files
|
|
160
|
+
snapshot.size = size
|
|
158
161
|
|
|
159
162
|
await Promise.all([
|
|
160
163
|
// Update the draft status in datasets collection in case any changes were made (DOI, License)
|
|
@@ -167,15 +170,17 @@ export const createSnapshot = async (
|
|
|
167
170
|
updateDatasetName(datasetId),
|
|
168
171
|
])
|
|
169
172
|
|
|
170
|
-
|
|
173
|
+
await reindexDataset(datasetId)
|
|
174
|
+
|
|
171
175
|
announceNewSnapshot(snapshot, datasetId, user)
|
|
172
176
|
return snapshot
|
|
173
177
|
} catch (err) {
|
|
174
178
|
// delete the keys if any step fails
|
|
175
179
|
// this avoids inconsistent cache state after failures
|
|
176
180
|
snapshotCache.drop()
|
|
177
|
-
snapshotLock.unlock()
|
|
178
181
|
return err
|
|
182
|
+
} finally {
|
|
183
|
+
snapshotLock.unlock()
|
|
179
184
|
}
|
|
180
185
|
}
|
|
181
186
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { indexDataset, queryForIndex, indexingToken } from '@openneuro/search'
|
|
2
|
+
import { elasticClient } from './elastic-client'
|
|
3
|
+
import {
|
|
4
|
+
from,
|
|
5
|
+
ApolloClient,
|
|
6
|
+
InMemoryCache,
|
|
7
|
+
NormalizedCacheObject,
|
|
8
|
+
} from '@apollo/client'
|
|
9
|
+
import { setContext } from '@apollo/client/link/context'
|
|
10
|
+
import { HttpLink } from '@apollo/client/link/http'
|
|
11
|
+
import fetch from 'node-fetch'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Setup SchemaLink based client for querying
|
|
15
|
+
*/
|
|
16
|
+
export const schemaLinkClient = (): ApolloClient<NormalizedCacheObject> => {
|
|
17
|
+
const accessToken = indexingToken()
|
|
18
|
+
const authLink = setContext((_, { headers }) => {
|
|
19
|
+
return {
|
|
20
|
+
headers: {
|
|
21
|
+
...headers,
|
|
22
|
+
Cookie: `accessToken=${accessToken}`,
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
const httpLink = new HttpLink({
|
|
27
|
+
uri: process.env.GRAPHQL_URI,
|
|
28
|
+
fetch,
|
|
29
|
+
})
|
|
30
|
+
return new ApolloClient({
|
|
31
|
+
link: from([authLink, httpLink]),
|
|
32
|
+
cache: new InMemoryCache(),
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const client = schemaLinkClient()
|
|
37
|
+
|
|
38
|
+
export const reindexDataset = async (datasetId: string): Promise<void> => {
|
|
39
|
+
const datasetIndexQueryResult = await queryForIndex(client, datasetId)
|
|
40
|
+
await indexDataset(elasticClient, datasetIndexQueryResult.data.dataset)
|
|
41
|
+
}
|
|
@@ -16,9 +16,7 @@ import { history } from './history.js'
|
|
|
16
16
|
import * as dataladAnalytics from '../../datalad/analytics.js'
|
|
17
17
|
import DatasetModel from '../../models/dataset'
|
|
18
18
|
import Deletion from '../../models/deletion'
|
|
19
|
-
import fetch from 'node-fetch'
|
|
20
19
|
import { reviewers } from './reviewer'
|
|
21
|
-
import { UpdatedFile } from '../utils/file.js'
|
|
22
20
|
import { getDatasetWorker } from '../../libs/datalad-service.js'
|
|
23
21
|
import { getDraftHead } from '../../datalad/dataset.js'
|
|
24
22
|
import { getFileName } from '../../datalad/files.js'
|
|
@@ -152,9 +150,7 @@ export const deleteFiles = async (
|
|
|
152
150
|
) => {
|
|
153
151
|
try {
|
|
154
152
|
await checkDatasetWrite(datasetId, user, userInfo)
|
|
155
|
-
const deletedFiles = await datalad
|
|
156
|
-
.deleteFiles(datasetId, files, userInfo)
|
|
157
|
-
.then(filenames => filenames.map(filename => new UpdatedFile(filename)))
|
|
153
|
+
const deletedFiles = await datalad.deleteFiles(datasetId, files, userInfo)
|
|
158
154
|
pubsub.publish('filesUpdated', {
|
|
159
155
|
datasetId,
|
|
160
156
|
filesUpdated: {
|
|
@@ -10,11 +10,17 @@ import { filterRemovedAnnexObjects } from '../utils/file.js'
|
|
|
10
10
|
// A draft must have a dataset parent
|
|
11
11
|
const draftFiles = async (dataset, args, { userInfo }) => {
|
|
12
12
|
const hexsha = await getDraftRevision(dataset.id)
|
|
13
|
-
const files = await getFiles(dataset.id, hexsha)
|
|
13
|
+
const { files } = await getFiles(dataset.id, hexsha)
|
|
14
14
|
const prefixFiltered = filterFiles('prefix' in args && args.prefix)(files)
|
|
15
15
|
return filterRemovedAnnexObjects(dataset.id, userInfo)(prefixFiltered)
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
const draftSize = async (dataset, args, { userInfo }) => {
|
|
19
|
+
const hexsha = await getDraftRevision(dataset.id)
|
|
20
|
+
const { size } = await getFiles(dataset.id, hexsha)
|
|
21
|
+
return size
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
/**
|
|
19
25
|
* Deprecated mutation to move the draft HEAD reference forward or backward
|
|
20
26
|
*
|
|
@@ -39,6 +45,7 @@ export const revalidate = async (obj, { datasetId }, { user, userInfo }) => {
|
|
|
39
45
|
const draft = {
|
|
40
46
|
id: obj => obj.id,
|
|
41
47
|
files: draftFiles,
|
|
48
|
+
size: draftSize,
|
|
42
49
|
summary,
|
|
43
50
|
issues,
|
|
44
51
|
modified: obj => obj.modified,
|
|
@@ -44,7 +44,9 @@ const publishPermissions = async datasetId => {
|
|
|
44
44
|
export const updatePermissions = async (obj, args, { user, userInfo }) => {
|
|
45
45
|
await checkDatasetAdmin(args.datasetId, user, userInfo)
|
|
46
46
|
// get all users the the email specified by permissions arg
|
|
47
|
-
const users = await User.find({ email: args.userEmail })
|
|
47
|
+
const users = await User.find({ email: args.userEmail })
|
|
48
|
+
.collation({ locale: 'en', strength: 2 })
|
|
49
|
+
.exec()
|
|
48
50
|
|
|
49
51
|
if (!users.length) {
|
|
50
52
|
throw new Error('A user with that email address does not exist')
|
|
@@ -29,8 +29,11 @@ export const snapshot = (obj, { datasetId, tag }, context) => {
|
|
|
29
29
|
summary: () => summary({ id: datasetId, revision: snapshot.hexsha }),
|
|
30
30
|
files: ({ prefix }) =>
|
|
31
31
|
getFiles(datasetId, snapshot.hexsha)
|
|
32
|
+
.then(response => response.files)
|
|
32
33
|
.then(filterFiles(prefix))
|
|
33
34
|
.then(filterRemovedAnnexObjects(datasetId, context.userInfo)),
|
|
35
|
+
size: () =>
|
|
36
|
+
getFiles(datasetId, snapshot.hexsha).then(response => response.size),
|
|
34
37
|
deprecated: () => deprecated({ datasetId, tag }),
|
|
35
38
|
related: () => related(datasetId),
|
|
36
39
|
onBrainlife: () => onBrainlife(snapshot),
|
package/src/graphql/schema.js
CHANGED
|
@@ -430,6 +430,8 @@ export const typeDefs = `
|
|
|
430
430
|
uploads: [UploadMetadata]
|
|
431
431
|
# Git commit hash
|
|
432
432
|
head: String
|
|
433
|
+
# Total size in bytes of this draft
|
|
434
|
+
size: Int
|
|
433
435
|
}
|
|
434
436
|
|
|
435
437
|
# Tagged snapshot of a draft
|
|
@@ -461,6 +463,8 @@ export const typeDefs = `
|
|
|
461
463
|
related: [RelatedObject]
|
|
462
464
|
# Is the snapshot available for analysis on Brainlife?
|
|
463
465
|
onBrainlife: Boolean @cacheControl(maxAge: 10080, scope: PUBLIC)
|
|
466
|
+
# Total size in bytes of this snapshot
|
|
467
|
+
size: Int
|
|
464
468
|
}
|
|
465
469
|
|
|
466
470
|
# RelatedObject nature of relationship
|
|
@@ -1,34 +1,5 @@
|
|
|
1
1
|
import BadAnnexObject from '../../models/badAnnexObject'
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Generates unique id for untracked files.
|
|
5
|
-
* @param {string} filepath - filepath ('/' delimiters)
|
|
6
|
-
* @param {number|string} [size] - of file
|
|
7
|
-
*/
|
|
8
|
-
export const generateFileId = (filepath, size) => `${filepath}:${size}`
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Creates a file object with an ApolloGQL cache-safe id.
|
|
12
|
-
* @class
|
|
13
|
-
* @param {string} filepath ':' delimited
|
|
14
|
-
* @param {string|number} [size]
|
|
15
|
-
*/
|
|
16
|
-
export function UpdatedFile(filepath, size) {
|
|
17
|
-
/**
|
|
18
|
-
* unique id
|
|
19
|
-
* @id UpdatedFile#id
|
|
20
|
-
* @type {string}
|
|
21
|
-
*/
|
|
22
|
-
this.id = generateFileId(filepath, size)
|
|
23
|
-
/**
|
|
24
|
-
* filename with '/' delimiters
|
|
25
|
-
* @filename UpdatedFile#filename
|
|
26
|
-
* @type {string}
|
|
27
|
-
*/
|
|
28
|
-
this.filename = filepath
|
|
29
|
-
if (size) this.size = size
|
|
30
|
-
}
|
|
31
|
-
|
|
32
3
|
export const filterRemovedAnnexObjects =
|
|
33
4
|
(datasetId, userInfo) => async files => {
|
|
34
5
|
const removedAnnexObjectKeys = (
|
package/src/models/user.ts
CHANGED
|
@@ -29,6 +29,16 @@ const userSchema = new Schema({
|
|
|
29
29
|
})
|
|
30
30
|
|
|
31
31
|
userSchema.index({ id: 1, provider: 1 }, { unique: true })
|
|
32
|
+
// Allow case insensitive email queries
|
|
33
|
+
userSchema.index(
|
|
34
|
+
{ email: 1 },
|
|
35
|
+
{
|
|
36
|
+
collation: {
|
|
37
|
+
locale: 'en',
|
|
38
|
+
strength: 2,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
)
|
|
32
42
|
|
|
33
43
|
const User = model<UserDocument>('User', userSchema)
|
|
34
44
|
|
package/tsconfig.json
CHANGED
package/jestsetup.js
DELETED
|
File without changes
|