@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openneuro/server",
3
- "version": "4.4.10",
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": "d3e7a6459132b92d678665a081efd666e3088461"
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
  })
@@ -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
- return files.map(addFileUrl(datasetId, hexsha))
84
+ const size = computeTotalSize(files)
85
+ return {
86
+ files: files.map(addFileUrl(datasetId, hexsha)),
87
+ size,
88
+ }
79
89
  }
80
90
  }),
81
91
  )
@@ -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
- snapshot.files = await getFiles(datasetId, tag)
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
- snapshotLock.unlock()
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
 
@@ -6,6 +6,6 @@ const elasticConfig = {
6
6
  maxRetries: 3,
7
7
  }
8
8
 
9
- const elasticClient = new Client(elasticConfig)
9
+ export const elasticClient = new Client(elasticConfig)
10
10
 
11
11
  export default elasticClient
@@ -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 }).exec()
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),
@@ -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 = (
@@ -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
@@ -7,5 +7,8 @@
7
7
  },
8
8
  "include": ["./src"],
9
9
  "files": ["./src/lerna.json"],
10
- "references": [{ "path": "../openneuro-client" }]
10
+ "references": [
11
+ { "path": "../openneuro-client" },
12
+ { "path": "../openneuro-search" }
13
+ ]
11
14
  }
package/jestsetup.js DELETED
File without changes