@openneuro/server 4.19.2 → 4.20.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.
Files changed (39) hide show
  1. package/Dockerfile +1 -1
  2. package/package.json +23 -26
  3. package/src/app.ts +119 -0
  4. package/src/datalad/files.ts +1 -1
  5. package/src/graphql/__tests__/__snapshots__/permissions.spec.js.snap +1 -1
  6. package/src/graphql/__tests__/permissions.spec.js +1 -1
  7. package/src/graphql/permissions.js +7 -9
  8. package/src/graphql/pubsub.js +4 -9
  9. package/src/graphql/resolvers/dataset.js +3 -3
  10. package/src/graphql/resolvers/draft.js +1 -1
  11. package/src/graphql/resolvers/git.ts +1 -1
  12. package/src/graphql/resolvers/{metadata.js → metadata.ts} +26 -4
  13. package/src/graphql/resolvers/mutation.js +3 -3
  14. package/src/graphql/resolvers/{permissions.js → permissions.ts} +16 -8
  15. package/src/graphql/resolvers/query.js +2 -0
  16. package/src/graphql/resolvers/reviewer.ts +1 -1
  17. package/src/graphql/resolvers/snapshots.js +1 -1
  18. package/src/graphql/resolvers/{summary.js → summary.ts} +2 -2
  19. package/src/graphql/resolvers/upload.js +1 -1
  20. package/src/graphql/resolvers/user.js +15 -1
  21. package/src/graphql/schema.js +3 -1
  22. package/src/graphql/utils/file.js +1 -1
  23. package/src/handlers/datalad.js +13 -16
  24. package/src/handlers/doi.js +1 -1
  25. package/src/libs/authentication/__tests__/jwt.spec.js +1 -1
  26. package/src/libs/authentication/{jwt.js → jwt.ts} +25 -7
  27. package/src/libs/authentication/orcid.js +1 -1
  28. package/src/libs/authentication/passport.js +1 -1
  29. package/src/libs/doi/__tests__/__snapshots__/doi.spec.js.snap +1 -1
  30. package/src/libs/email/templates/__tests__/__snapshots__/comment-created.spec.ts.snap +1 -1
  31. package/src/libs/email/templates/__tests__/__snapshots__/dataset-deleted.spec.ts.snap +1 -1
  32. package/src/libs/email/templates/__tests__/__snapshots__/owner-unsubscribed.spec.ts.snap +1 -1
  33. package/src/libs/email/templates/__tests__/__snapshots__/snapshot-created.spec.ts.snap +1 -1
  34. package/src/libs/email/templates/__tests__/__snapshots__/snapshot-reminder.spec.ts.snap +1 -1
  35. package/src/libs/subscription-server.js +2 -1
  36. package/src/models/summary.ts +2 -0
  37. package/src/routes.js +2 -2
  38. package/src/{server.js → server.ts} +5 -7
  39. package/src/app.js +0 -94
package/Dockerfile CHANGED
@@ -4,7 +4,7 @@ FROM openneuro/node AS build
4
4
  WORKDIR /srv/packages/openneuro-server
5
5
  RUN yarn build
6
6
 
7
- FROM node:18.15.0-alpine
7
+ FROM node:18.17.1-alpine
8
8
 
9
9
  WORKDIR /srv
10
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openneuro/server",
3
- "version": "4.19.2",
3
+ "version": "4.20.0-alpha.0",
4
4
  "description": "Core service for the OpenNeuro platform.",
5
5
  "license": "MIT",
6
6
  "main": "src/server.js",
@@ -16,35 +16,37 @@
16
16
  "author": "Squishymedia",
17
17
  "dependencies": {
18
18
  "@apollo/client": "3.7.2",
19
+ "@apollo/server": "4.9.3",
20
+ "@apollo/utils.keyvadapter": "3.0.0",
19
21
  "@elastic/elasticsearch": "7.15.0",
20
- "@openneuro/search": "^4.19.2",
22
+ "@graphql-tools/schema": "^10.0.0",
23
+ "@keyv/redis": "^2.7.0",
24
+ "@openneuro/search": "^4.20.0-alpha.0",
21
25
  "@passport-next/passport-google-oauth2": "^1.0.0",
22
26
  "@sentry/node": "^4.5.3",
23
- "apollo-server": "2.25.4",
24
- "apollo-server-cache-redis": "1.4.0",
25
- "apollo-server-express": "2.25.3",
26
27
  "base64url": "^3.0.0",
27
- "body-parser": "^1.18.2",
28
- "cookie-parser": "^1.4.3",
28
+ "cookie-parser": "^1.4.6",
29
+ "cors": "^2.8.5",
29
30
  "date-fns": "^2.16.1",
30
31
  "draft-js": "^0.11.7",
31
32
  "draft-js-export-html": "^1.4.1",
32
- "elastic-apm-node": "3.43.0",
33
- "express": "^4.17.1",
34
- "graphql": "14.7.0",
33
+ "elastic-apm-node": "3.49.1",
34
+ "express": "4.18.2",
35
+ "graphql": "16.6.0",
35
36
  "graphql-bigint": "^1.0.0",
36
- "graphql-compose": "^7.25.0",
37
+ "graphql-compose": "9.0.10",
37
38
  "graphql-iso-date": "^3.6.1",
38
39
  "graphql-redis-subscriptions": "2.1.0",
39
40
  "graphql-subscriptions": "^1.1.0",
40
- "graphql-tools": "4.0.6",
41
+ "graphql-tools": "9.0.0",
41
42
  "immutable": "^3.8.2",
42
43
  "ioredis": "4.17.3",
43
44
  "jsdom": "^11.6.2",
44
45
  "jsonwebtoken": "^9.0.0",
46
+ "keyv": "^4.5.3",
45
47
  "mime-types": "^2.1.19",
46
48
  "moment": "^2.14.1",
47
- "mongoose": "^7.0.2",
49
+ "mongoose": "^6.11.3",
48
50
  "morgan": "^1.6.1",
49
51
  "node-mailjet": "^3.3.5",
50
52
  "object-hash": "2.1.1",
@@ -59,38 +61,33 @@
59
61
  "request": "^2.83.0",
60
62
  "semver": "^5.5.0",
61
63
  "sitemap": "^2.1.0",
62
- "subscriptions-transport-ws": "0.9.18",
63
64
  "superagent": "^3.8.2",
64
65
  "ts-node": "9.1.1",
65
- "typescript": "4.5.4",
66
+ "typescript": "5.1.6",
66
67
  "underscore": "^1.8.3",
67
68
  "uuid": "^3.0.1",
68
69
  "xmldoc": "^1.1.0"
69
70
  },
70
71
  "devDependencies": {
71
- "@babel/core": "^7.6.4",
72
- "@babel/plugin-proposal-object-rest-spread": "^7.6.2",
73
- "@babel/plugin-proposal-optional-chaining": "^7.6.0",
74
- "@babel/plugin-syntax-object-rest-spread": "^7.0.0",
75
- "@babel/preset-env": "^7.6.3",
76
- "@babel/runtime-corejs3": "^7.13.10",
72
+ "@types/cors": "^2",
77
73
  "@types/draft-js": "^0.10.43",
74
+ "@types/express": "^4.17.17",
75
+ "@types/express-serve-static-core": "^4.17.35",
78
76
  "@types/ioredis": "^4.17.1",
77
+ "@types/ioredis-mock": "^8.2.2",
79
78
  "@types/node-mailjet": "^3",
80
79
  "@types/semver": "^5",
81
- "apollo-link-schema": "^1.2.5",
82
- "babel-eslint": "^10.1.0",
83
80
  "core-js": "^3.10.1",
84
- "ioredis-mock": "^8.2.2",
81
+ "ioredis-mock": "^8.8.1",
85
82
  "nodemon": "^2.0.7",
86
83
  "supertest": "^3.0.0",
87
84
  "ts-node-dev": "1.1.6",
88
85
  "tsc-watch": "^4.2.9",
89
- "vitest": "^0.25.2",
86
+ "vitest": "0.33.0",
90
87
  "vitest-fetch-mock": "^0.2.1"
91
88
  },
92
89
  "publishConfig": {
93
90
  "access": "public"
94
91
  },
95
- "gitHead": "3074eba3525c3083dbe22495418a91ef02360d32"
92
+ "gitHead": "cc6822c27d8e66562e325042fc11a2998cb2eef7"
96
93
  }
package/src/app.ts ADDED
@@ -0,0 +1,119 @@
1
+ /*eslint no-console: ["error", { allow: ["log"] }] */
2
+ /* eslint-disable no-unused-vars*/
3
+
4
+ /**
5
+ * Express app setup
6
+ */
7
+ import { createServer } from 'http'
8
+ import cors from 'cors'
9
+ import express, { urlencoded, json } from 'express'
10
+ import passport from 'passport'
11
+ import config from './config'
12
+ import routes from './routes'
13
+ import morgan from 'morgan'
14
+ import schema from './graphql/schema'
15
+ import { ApolloServer } from '@apollo/server'
16
+ import { ApolloServerPluginLandingPageLocalDefault } from '@apollo/server/plugin/landingPage/default'
17
+ import { expressMiddleware } from '@apollo/server/express4'
18
+ import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer'
19
+ import { KeyvAdapter } from '@apollo/utils.keyvadapter'
20
+ import Keyv from 'keyv'
21
+ import KeyvRedis from '@keyv/redis'
22
+ import cookieParser from 'cookie-parser'
23
+ import * as jwt from './libs/authentication/jwt'
24
+ import * as auth from './libs/authentication/states.js'
25
+ import { sitemapHandler } from './handlers/sitemap.js'
26
+ import { setupPassportAuth } from './libs/authentication/passport.js'
27
+ import { redis } from './libs/redis'
28
+ import { version } from './lerna.json'
29
+ export { Express } from 'express-serve-static-core'
30
+
31
+ interface OpenNeuroRequestContext {
32
+ user: string
33
+ isSuperUser: boolean
34
+ userInfo: {
35
+ id: string
36
+ exp: string
37
+ scopes: string[]
38
+ }
39
+ }
40
+
41
+ export async function expressApolloSetup() {
42
+ const app = express()
43
+
44
+ setupPassportAuth()
45
+
46
+ app.use(passport.initialize())
47
+
48
+ app.use((req, res, next) => {
49
+ res.set(config.headers)
50
+ res.type('application/json')
51
+ next()
52
+ })
53
+ app.use(morgan('short'))
54
+ app.use(cookieParser())
55
+ app.use(urlencoded({ extended: false, limit: '50mb' }))
56
+ app.use(json({ limit: '50mb' }))
57
+
58
+ // routing ---------------------------------------------------------
59
+ app.use('/sitemap.xml', sitemapHandler)
60
+ app.use(config.apiPrefix, routes)
61
+
62
+ const httpServer = createServer(app)
63
+
64
+ // Apollo server setup
65
+ const apolloServer = new ApolloServer<OpenNeuroRequestContext>({
66
+ schema,
67
+ // Always allow introspection - our schema is public
68
+ introspection: true,
69
+ cache: new KeyvAdapter(new Keyv({ store: new KeyvRedis(redis) })),
70
+ plugins: [
71
+ ApolloServerPluginLandingPageLocalDefault(),
72
+ ApolloServerPluginDrainHttpServer({
73
+ httpServer,
74
+ }),
75
+ {
76
+ async requestDidStart() {
77
+ return {
78
+ async willSendResponse(requestContext) {
79
+ const { response } = requestContext
80
+ if (
81
+ response.body.kind === 'single' &&
82
+ 'data' in response.body.singleResult
83
+ ) {
84
+ response.body.singleResult.extensions = {
85
+ ...response.body.singleResult.extensions,
86
+
87
+ openneuro: { version },
88
+ }
89
+ }
90
+ },
91
+ }
92
+ },
93
+ },
94
+ ],
95
+ })
96
+
97
+ await apolloServer.start()
98
+
99
+ // Setup GraphQL middleware
100
+ app.use(
101
+ ['/graphql', '/crn/graphql'],
102
+ cors<cors.CorsRequest>(),
103
+ jwt.authenticate,
104
+ auth.optional,
105
+ expressMiddleware(apolloServer, {
106
+ context: async ({ req }) => {
107
+ if (req.isAuthenticated()) {
108
+ return {
109
+ user: req.user.id,
110
+ isSuperUser: req.user.admin,
111
+ userInfo: req.user,
112
+ }
113
+ }
114
+ },
115
+ }),
116
+ )
117
+
118
+ return app
119
+ }
@@ -12,7 +12,7 @@ export const encodeFilePath = (path: string): string => {
12
12
  }
13
13
 
14
14
  /**
15
- * Convert to from URL compatible path fo filepath
15
+ * Convert from URL compatible path to filepath
16
16
  * @param {String} path
17
17
  */
18
18
  export const decodeFilePath = (path: string): string => {
@@ -1,4 +1,4 @@
1
- // Vitest Snapshot v1
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
3
  exports[`resolver permissions helpers > checkDatasetAdmin() > resolves to false for anonymous users 1`] = `"You do not have admin access to this dataset."`;
4
4
 
@@ -4,7 +4,7 @@ import {
4
4
  states,
5
5
  checkDatasetWrite,
6
6
  checkDatasetAdmin,
7
- } from '../permissions.js'
7
+ } from '../permissions'
8
8
 
9
9
  vi.mock('ioredis')
10
10
 
@@ -1,5 +1,5 @@
1
1
  import config from '../config.js'
2
- import { ApolloError } from 'apollo-server'
2
+ import { GraphQLError } from 'graphql'
3
3
  import Permission from '../models/permission'
4
4
  import Dataset from '../models/dataset'
5
5
  import Deletion from '../models/deletion'
@@ -52,9 +52,9 @@ export const checkPermissionLevel = (permission, state) => {
52
52
  }
53
53
  }
54
54
 
55
- export class DeletedDatasetError extends ApolloError {
55
+ export class DeletedDatasetError extends GraphQLError {
56
56
  constructor(datasetId, reason, redirect = undefined) {
57
- let extension
57
+ let extensions
58
58
  if (redirect) {
59
59
  try {
60
60
  // Validate URL before we attach it to the API response
@@ -65,17 +65,15 @@ export class DeletedDatasetError extends ApolloError {
65
65
  url.pathname.startsWith('/datasets')
66
66
  ) {
67
67
  // Only return a relative path to avoid cross site risks
68
- extension = { redirect: url.pathname }
68
+ extensions = { code: 'DELETED_DATASET', redirect: url.pathname }
69
69
  }
70
70
  } catch (err) {
71
71
  // Do nothing
72
72
  }
73
73
  }
74
- super(
75
- `Dataset ${datasetId} has been deleted. Reason: ${reason}.`,
76
- 'DELETED_DATASET',
77
- extension,
78
- )
74
+ super(`Dataset ${datasetId} has been deleted. Reason: ${reason}.`, {
75
+ extensions,
76
+ })
79
77
  }
80
78
  }
81
79
 
@@ -1,10 +1,5 @@
1
- import Redis from 'ioredis'
2
- import config from '../config.js'
3
- import pubsubFactory from '../libs/redis-pubsub.js'
1
+ async function* asyncIterator(_) {
2
+ yield null
3
+ }
4
4
 
5
- const pubsub = pubsubFactory({
6
- publisher: new Redis(config.redis),
7
- subscriber: new Redis(config.redis),
8
- })
9
-
10
- export default pubsub
5
+ export default { publish: (_, __) => {}, asyncIterator }
@@ -7,11 +7,11 @@ import {
7
7
  checkDatasetRead,
8
8
  checkDatasetWrite,
9
9
  checkDatasetAdmin,
10
- } from '../permissions.js'
10
+ } from '../permissions'
11
11
  import { user } from './user.js'
12
- import { permissions } from './permissions.js'
12
+ import { permissions } from './permissions'
13
13
  import { datasetComments } from './comment.js'
14
- import { metadata } from './metadata.js'
14
+ import { metadata } from './metadata'
15
15
  import { history } from './history.js'
16
16
  import * as dataladAnalytics from '../../datalad/analytics.js'
17
17
  import DatasetModel from '../../models/dataset'
@@ -1,5 +1,5 @@
1
1
  import Summary from '../../models/summary'
2
- import { summary } from './summary.js'
2
+ import { summary } from './summary'
3
3
  import { issues } from './issues.js'
4
4
  import { description } from './description.js'
5
5
  import { readme } from './readme.js'
@@ -1,5 +1,5 @@
1
1
  import { checkDatasetWrite } from '../permissions.js'
2
- import { generateRepoToken } from '../../libs/authentication/jwt.js'
2
+ import { generateRepoToken } from '../../libs/authentication/jwt'
3
3
  import { getDatasetEndpoint } from '../../libs/datalad-service.js'
4
4
 
5
5
  /**
@@ -1,5 +1,7 @@
1
1
  import Snapshot from '../../models/snapshot'
2
- import MetadataModel from '../../models/metadata'
2
+ import { LeanDocument } from 'mongoose'
3
+ import DatasetModel from '../../models/dataset'
4
+ import MetadataModel, { MetadataDocument } from '../../models/metadata'
3
5
  import { latestSnapshot } from './snapshots'
4
6
  import { permissions } from './permissions'
5
7
 
@@ -8,7 +10,11 @@ import { permissions } from './permissions'
8
10
  *
9
11
  * User modified fields are queried from the Metadata model and dynamic metadata is updated from the latest snapshot
10
12
  */
11
- export const metadata = async (dataset, _, context) => {
13
+ export const metadata = async (
14
+ dataset,
15
+ _,
16
+ context,
17
+ ): Promise<LeanDocument<MetadataDocument>> => {
12
18
  const record = await MetadataModel.findOne({
13
19
  datasetId: dataset.id,
14
20
  }).lean()
@@ -24,7 +30,7 @@ export const metadata = async (dataset, _, context) => {
24
30
  for (const user of userPermissions) {
25
31
  if (user.level === 'admin') {
26
32
  const userObj = await user.user
27
- adminUsers.push(userObj.email)
33
+ adminUsers.push(userObj.name)
28
34
  }
29
35
  }
30
36
  const firstSnapshot = await Snapshot.find({ datasetId: dataset.id }).sort({
@@ -44,7 +50,7 @@ export const metadata = async (dataset, _, context) => {
44
50
  adminUsers,
45
51
  firstSnapshotCreatedAt,
46
52
  latestSnapshotCreatedAt: snapshot.created,
47
- subjectAges: summary?.subjectMetadata?.map(s => s.age),
53
+ ages: summary?.subjectMetadata?.map(s => s.age as number),
48
54
  modalities: summary?.modalities || [],
49
55
  dataProcessed: summary?.dataProcessed || null,
50
56
  }
@@ -60,3 +66,19 @@ export const addMetadata = async (obj, { datasetId, metadata }) => {
60
66
  })
61
67
  return result
62
68
  }
69
+
70
+ /**
71
+ * Resolve all public datasets and return metadata
72
+ */
73
+ export async function publicMetadata(
74
+ obj,
75
+ ): Promise<Array<LeanDocument<MetadataDocument>>> {
76
+ const datasets = await DatasetModel.find({
77
+ public: true,
78
+ }).lean()
79
+ const dsMetadata: LeanDocument<MetadataDocument>[] = []
80
+ for (const ds of datasets) {
81
+ dsMetadata.push(await metadata(ds, null, {}))
82
+ }
83
+ return dsMetadata
84
+ }
@@ -18,9 +18,9 @@ import {
18
18
  undoDeprecateSnapshot,
19
19
  } from './snapshots.js'
20
20
  import { removeUser, setAdmin, setBlocked } from './user.js'
21
- import { updateSummary } from './summary.js'
21
+ import { updateSummary } from './summary'
22
22
  import { revalidate, updateValidation } from './validation.js'
23
- import { updatePermissions, removePermissions } from './permissions.js'
23
+ import { updatePermissions, removePermissions } from './permissions'
24
24
  import { followDataset } from './follow.js'
25
25
  import { starDataset } from './stars.js'
26
26
  import { publishDataset } from './publish.js'
@@ -28,7 +28,7 @@ import { updateDescription, updateDescriptionList } from './description.js'
28
28
  import { updateReadme } from './readme.js'
29
29
  import { addComment, editComment, deleteComment } from './comment.js'
30
30
  import { subscribeToNewsletter } from './newsletter'
31
- import { addMetadata } from './metadata.js'
31
+ import { addMetadata } from './metadata'
32
32
  import { prepareUpload, finishUpload } from './upload.js'
33
33
  import { prepareRepoAccess } from './git'
34
34
  import { cacheClear } from './cache'
@@ -1,17 +1,25 @@
1
- import User from '../../models/user'
2
- import Permission from '../../models/permission'
1
+ import User, { UserDocument } from '../../models/user'
2
+ import Permission, { PermissionDocument } from '../../models/permission'
3
3
  import { checkDatasetAdmin } from '../permissions'
4
4
  import { user } from './user'
5
5
  import pubsub from '../pubsub.js'
6
6
 
7
- export const permissions = async ds => {
7
+ interface DatasetPermission {
8
+ id: string
9
+ userPermissions: (PermissionDocument & { user: Promise<UserDocument> })[]
10
+ }
11
+
12
+ export async function permissions(ds): Promise<DatasetPermission> {
8
13
  const permissions = await Permission.find({ datasetId: ds.id }).exec()
9
14
  return {
10
15
  id: ds.id,
11
- userPermissions: permissions.map(userPermission => ({
12
- ...userPermission.toJSON(),
13
- user: user(ds, { id: userPermission.userId }),
14
- })),
16
+ userPermissions: permissions.map(
17
+ userPermission =>
18
+ ({
19
+ ...userPermission.toJSON(),
20
+ user: user(ds, { id: userPermission.userId }),
21
+ } as PermissionDocument & { user: Promise<UserDocument> }),
22
+ ),
15
23
  }
16
24
  }
17
25
 
@@ -53,7 +61,7 @@ export const updatePermissions = async (obj, args, { user, userInfo }) => {
53
61
  }
54
62
 
55
63
  const userPromises = users.map(user => {
56
- return new Promise((resolve, reject) => {
64
+ return new Promise<void>((resolve, reject) => {
57
65
  Permission.updateOne(
58
66
  {
59
67
  datasetId: args.datasetId,
@@ -5,6 +5,7 @@ import { dataset, datasets } from './dataset.js'
5
5
  import { snapshot, participantCount } from './snapshots.js'
6
6
  import { user, users } from './user.js'
7
7
  import { flaggedFiles } from './flaggedFiles'
8
+ import { publicMetadata } from './metadata'
8
9
 
9
10
  const Query = {
10
11
  dataset,
@@ -14,6 +15,7 @@ const Query = {
14
15
  snapshot,
15
16
  participantCount,
16
17
  flaggedFiles,
18
+ publicMetadata,
17
19
  }
18
20
 
19
21
  export default Query
@@ -1,7 +1,7 @@
1
1
  import config from '../../config'
2
2
  import Reviewer from '../../models/reviewer'
3
3
  import { checkDatasetAdmin } from '../permissions.js'
4
- import { generateReviewerToken } from '../../libs/authentication/jwt.js'
4
+ import { generateReviewerToken } from '../../libs/authentication/jwt'
5
5
 
6
6
  /**
7
7
  * Create an anonymous read-only access key
@@ -4,7 +4,7 @@ import { onBrainlife } from './brainlife'
4
4
  import { checkDatasetRead, checkDatasetWrite } from '../permissions.js'
5
5
  import { readme } from './readme.js'
6
6
  import { description } from './description.js'
7
- import { summary } from './summary.js'
7
+ import { summary } from './summary'
8
8
  import { snapshotIssues } from './issues.js'
9
9
  import { getFiles } from '../../datalad/files'
10
10
  import Summary from '../../models/summary'
@@ -1,9 +1,9 @@
1
- import Summary from '../../models/summary'
1
+ import Summary, { SummaryDocument } from '../../models/summary'
2
2
 
3
3
  /**
4
4
  * Summary resolver
5
5
  */
6
- export const summary = async dataset => {
6
+ export async function summary(dataset): Promise<Partial<SummaryDocument>> {
7
7
  const datasetSummary = (
8
8
  await Summary.findOne({
9
9
  id: dataset.revision,
@@ -1,6 +1,6 @@
1
1
  import Upload from '../../models/upload'
2
2
  import { checkDatasetWrite } from '../permissions.js'
3
- import { generateUploadToken } from '../../libs/authentication/jwt.js'
3
+ import { generateUploadToken } from '../../libs/authentication/jwt'
4
4
  import { finishUploadRequest } from '../../datalad/upload.js'
5
5
  import { getDatasetEndpoint } from '../../libs/datalad-service.js'
6
6
 
@@ -45,4 +45,18 @@ export const setBlocked = (obj, { id, blocked }, { userInfo }) => {
45
45
  }
46
46
  }
47
47
 
48
- export default user
48
+ const UserResolvers = {
49
+ id: obj => obj.id,
50
+ provider: obj => obj.provider,
51
+ avatar: obj => obj.avatar,
52
+ orcid: obj => obj.orcid,
53
+ created: obj => obj.created,
54
+ modified: obj => obj.modified,
55
+ lastSeen: obj => obj.lastSeen,
56
+ email: obj => obj.email,
57
+ name: obj => obj.name,
58
+ admin: obj => obj.admin,
59
+ blocked: obj => obj.blocked,
60
+ }
61
+
62
+ export default UserResolvers
@@ -97,6 +97,8 @@ export const typeDefs = `
97
97
  "Get files that have already been deleted, default false."
98
98
  deleted: Boolean = false
99
99
  ): [FlaggedFile]
100
+ # All public dataset metadata
101
+ publicMetadata: [Metadata] @cacheControl(maxAge: 86400, scope: PUBLIC)
100
102
  }
101
103
 
102
104
  type Mutation {
@@ -527,7 +529,7 @@ export const typeDefs = `
527
529
  id: ID!
528
530
  # ID of user who flagged snapshot as deprecated
529
531
  user: String
530
- # Reason for deprecating snaphot
532
+ # Reason for deprecating snapshot
531
533
  reason: String
532
534
  # Timestamp of snapshot deprecation
533
535
  timestamp: Date
@@ -5,7 +5,7 @@ export const filterRemovedAnnexObjects =
5
5
  const removedAnnexObjectKeys = (
6
6
  await BadAnnexObject.find({ datasetId }).exec()
7
7
  ).map(({ annexKey }) => annexKey)
8
- // keep files that havent had their annex objects removed
8
+ // keep files that haven't had their annex objects removed
9
9
  return userInfo?.admin
10
10
  ? files
11
11
  : files.filter(({ key }) => !removedAnnexObjectKeys.includes(key))
@@ -40,23 +40,20 @@ export const getFile = async (req, res) => {
40
40
  const uri = snapshotId
41
41
  ? `http://${worker}/datasets/${datasetId}/snapshots/${snapshotId}/files/${filename}`
42
42
  : `http://${worker}/datasets/${datasetId}/files/${filename}`
43
- return (
44
- fetch(uri)
45
- .then(r => {
46
- // Set the content length (allow clients to catch HTTP issues better)
47
- res.setHeader(
48
- 'Content-Length',
49
- Number(r.headers.get('content-length')),
50
- )
51
- return r.body
52
- })
43
+ return fetch(uri)
44
+ .then(r => {
45
+ // Set the content length (allow clients to catch HTTP issues better)
46
+ res.setHeader('Content-Length', Number(r.headers.get('content-length')))
47
+ return r.body
48
+ })
49
+ .then(stream =>
53
50
  // @ts-expect-error
54
- .then(stream => Readable.fromWeb(stream).pipe(res))
55
- .catch(err => {
56
- console.error(err)
57
- res.status(500).send('Internal error transferring requested file')
58
- })
59
- )
51
+ Readable.fromWeb(stream, { highWaterMark: 4194304 }).pipe(res),
52
+ )
53
+ .catch(err => {
54
+ console.error(err)
55
+ res.status(500).send('Internal error transferring requested file')
56
+ })
60
57
  }
61
58
  }
62
59
 
@@ -42,7 +42,7 @@ export async function createSnapshotDoi(req, res) {
42
42
  }
43
43
  }
44
44
 
45
- // Have seperate function to get Doi that does not require any authorization
45
+ // Have separate function to get Doi that does not require any authorization
46
46
  export async function getDoi(req, res) {
47
47
  const datasetId = req.params.datasetId
48
48
  const snapshotId = req.params.snapshotId
@@ -1,5 +1,5 @@
1
1
  import User from '../../../models/user'
2
- import { addJWT } from '../jwt.js'
2
+ import { addJWT } from '../jwt'
3
3
 
4
4
  vi.mock('ioredis')
5
5
  vi.mock('../../../config.js')
@@ -5,8 +5,26 @@ import { decrypt } from './crypto'
5
5
  import User from '../../models/user'
6
6
  import config from '../../config.js'
7
7
 
8
- export const buildToken = (config, user, expiresIn, options) => {
9
- const fields = {
8
+ interface OpenNeuroTokenProfile {
9
+ sub: string
10
+ email: string
11
+ provider: string
12
+ name: string
13
+ admin: boolean
14
+ iat: number
15
+ exp: number
16
+ // Tokens may be scoped and limited to one dataset
17
+ scopes?: string[]
18
+ dataset?: string
19
+ }
20
+
21
+ export const buildToken = (
22
+ config,
23
+ user,
24
+ expiresIn,
25
+ options?: { scopes?: string[]; dataset?: string },
26
+ ): string => {
27
+ const fields: Omit<OpenNeuroTokenProfile, 'iat' | 'exp'> = {
10
28
  sub: user.id,
11
29
  email: user.email,
12
30
  provider: user.provider,
@@ -15,7 +33,7 @@ export const buildToken = (config, user, expiresIn, options) => {
15
33
  }
16
34
  // Allow extensions of the base token format
17
35
  if (options) {
18
- if ('scopes' in options) {
36
+ if (options && 'scopes' in options) {
19
37
  fields.scopes = options.scopes
20
38
  }
21
39
  if ('dataset' in options) {
@@ -24,7 +42,7 @@ export const buildToken = (config, user, expiresIn, options) => {
24
42
  }
25
43
  return jwt.sign(fields, config.auth.jwt.secret, {
26
44
  expiresIn,
27
- })
45
+ }) as string
28
46
  }
29
47
 
30
48
  // Helper to generate a JWT containing user info
@@ -77,7 +95,7 @@ export function generateReviewerToken(
77
95
  /**
78
96
  * Generate an git repo token
79
97
  *
80
- * Similary to the upload token, this shorter lived token is specific to git access
98
+ * Similarly to the upload token, this shorter lived token is specific to git access
81
99
  */
82
100
  export function generateRepoToken(user, datasetId, expiresIn = 60 * 60 * 24) {
83
101
  const options = {
@@ -111,8 +129,8 @@ export const jwtFromRequest = req => {
111
129
  }
112
130
  }
113
131
 
114
- export const decodeJWT = token => {
115
- return jwt.decode(token)
132
+ export const decodeJWT = (token: string): OpenNeuroTokenProfile => {
133
+ return jwt.decode(token) as OpenNeuroTokenProfile
116
134
  }
117
135
 
118
136
  export const parsedJwtFromRequest = req => {
@@ -1,6 +1,6 @@
1
1
  import passport from 'passport'
2
2
  import User from '../../models/user'
3
- import { parsedJwtFromRequest } from './jwt.js'
3
+ import { parsedJwtFromRequest } from './jwt'
4
4
 
5
5
  export const requestAuth = passport.authenticate('orcid', {
6
6
  session: false,
@@ -6,7 +6,7 @@ import { Strategy as ORCIDStrategy } from 'passport-orcid'
6
6
  import config from '../../config.js'
7
7
  import User from '../../models/user'
8
8
  import { encrypt } from './crypto'
9
- import { addJWT, jwtFromRequest, decodeJWT } from './jwt.js'
9
+ import { addJWT, jwtFromRequest } from './jwt'
10
10
  import orcid from '../orcid.js'
11
11
 
12
12
  const PROVIDERS = {
@@ -1,4 +1,4 @@
1
- // Vitest Snapshot v1
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
3
  exports[`DOI minting utils > template() > accepts expected arguments 1`] = `
4
4
  "<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?>
@@ -1,4 +1,4 @@
1
- // Vitest Snapshot v1
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
3
  exports[`email template -> comment created > renders with expected arguments 1`] = `
4
4
  "<html>
@@ -1,4 +1,4 @@
1
- // Vitest Snapshot v1
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
3
  exports[`email template -> comment created > renders with expected arguments 1`] = `
4
4
  "<html>
@@ -1,4 +1,4 @@
1
- // Vitest Snapshot v1
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
3
  exports[`email template -> comment created > renders with expected arguments 1`] = `
4
4
  "<html>
@@ -1,4 +1,4 @@
1
- // Vitest Snapshot v1
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
3
  exports[`email template -> comment created > renders with expected arguments 1`] = `
4
4
  "<html>
@@ -1,4 +1,4 @@
1
- // Vitest Snapshot v1
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
3
  exports[`email template -> comment created > renders with expected arguments 1`] = `
4
4
  "<html>
@@ -1,4 +1,4 @@
1
- import { execute, subscribe } from 'graphql'
1
+ /*import { execute, subscribe } from 'graphql'
2
2
  import { SubscriptionServer } from 'subscriptions-transport-ws'
3
3
  import schema from '../graphql/schema.js'
4
4
 
@@ -17,3 +17,4 @@ const subscriptionServerFactory = httpserver =>
17
17
  )
18
18
 
19
19
  export default subscriptionServerFactory
20
+ */
@@ -17,6 +17,7 @@ export interface SummaryDocument extends Document {
17
17
  subjectMetadata: Record<string, any>
18
18
  tasks: string[]
19
19
  modalities: string[]
20
+ primaryModality: string
20
21
  secondaryModalities: string[]
21
22
  dataTypes: string[]
22
23
  totalFiles: number
@@ -33,6 +34,7 @@ const summarySchema = new Schema({
33
34
  subjectMetadata: Object,
34
35
  tasks: [String],
35
36
  modalities: [String],
37
+ primaryModality: String,
36
38
  secondaryModalities: [String],
37
39
  dataTypes: [String],
38
40
  totalFiles: Number,
package/src/routes.js CHANGED
@@ -9,7 +9,7 @@ import * as subscriptions from './handlers/subscriptions'
9
9
  import verifyUser from './libs/authentication/verifyUser.js'
10
10
  import * as google from './libs/authentication/google.js'
11
11
  import * as orcid from './libs/authentication/orcid.js'
12
- import * as jwt from './libs/authentication/jwt.js'
12
+ import * as jwt from './libs/authentication/jwt'
13
13
  import * as auth from './libs/authentication/states.js'
14
14
  import * as doi from './handlers/doi'
15
15
  import { sitemapHandler } from './handlers/sitemap.js'
@@ -148,7 +148,7 @@ const routes = [
148
148
  {
149
149
  method: 'get',
150
150
  url: '/auth/orcid',
151
- middlware: [noCache],
151
+ middleware: [noCache],
152
152
  handler: orcid.requestAuth,
153
153
  },
154
154
  {
@@ -7,11 +7,9 @@ apm.start({
7
7
 
8
8
  import { createServer } from 'http'
9
9
  import mongoose from 'mongoose'
10
- import subscriptionServerFactory from './libs/subscription-server.js'
11
10
  import { connect as redisConnect } from './libs/redis'
12
11
  import config from './config'
13
- import createApp from './app'
14
- import { version } from './lerna.json'
12
+ import { expressApolloSetup } from './app'
15
13
 
16
14
  const redisConnectionSetup = async () => {
17
15
  try {
@@ -23,18 +21,18 @@ const redisConnectionSetup = async () => {
23
21
  }
24
22
  }
25
23
 
26
- mongoose.connect(config.mongo.url, {
24
+ void mongoose.connect(config.mongo.url, {
27
25
  dbName: config.mongo.dbName,
28
26
  connectTimeoutMS: config.mongo.connectTimeoutMS,
29
27
  })
30
28
 
31
- redisConnectionSetup().then(() => {
32
- const app = createApp(false)
29
+ void redisConnectionSetup().then(async () => {
30
+ const app = await expressApolloSetup()
33
31
  const server = createServer(app)
34
32
  server.listen(config.port, () => {
35
33
  // eslint-disable-next-line no-console
36
34
  console.log('Server is listening on port ' + config.port)
37
35
  // Setup GraphQL subscription transport
38
- subscriptionServerFactory(server)
36
+ //subscriptionServerFactory(server)
39
37
  })
40
38
  })
package/src/app.js DELETED
@@ -1,94 +0,0 @@
1
- /*eslint no-console: ["error", { allow: ["log"] }] */
2
- /* eslint-disable no-unused-vars*/
3
-
4
- /**
5
- * Express app setup
6
- */
7
- import express from 'express'
8
- import passport from 'passport'
9
- import config from './config'
10
- import routes from './routes'
11
- import morgan from 'morgan'
12
- import schema from './graphql/schema'
13
- import { ApolloServer } from 'apollo-server-express'
14
- import { BaseRedisCache } from 'apollo-server-cache-redis'
15
- import bodyParser from 'body-parser'
16
- import cookieParser from 'cookie-parser'
17
- import * as jwt from './libs/authentication/jwt.js'
18
- import * as auth from './libs/authentication/states.js'
19
- import { sitemapHandler } from './handlers/sitemap.js'
20
- import { setupPassportAuth } from './libs/authentication/passport.js'
21
- import { redis } from './libs/redis'
22
- import { version } from './lerna.json'
23
-
24
- // test flag disables Sentry for tests
25
- export default test => {
26
- const app = express()
27
-
28
- setupPassportAuth()
29
-
30
- app.use(passport.initialize())
31
-
32
- app.use((req, res, next) => {
33
- res.set(config.headers)
34
- res.type('application/json')
35
- next()
36
- })
37
- app.use(morgan('short'))
38
- app.use(cookieParser())
39
- app.use(bodyParser.urlencoded({ extended: false, limit: '50mb' }))
40
- app.use(bodyParser.json({ limit: '50mb' }))
41
-
42
- // routing ---------------------------------------------------------
43
- app.use('/sitemap.xml', sitemapHandler)
44
- app.use(config.apiPrefix, routes)
45
-
46
- // error handling --------------------------------------------------\
47
-
48
- // Apollo engine setup
49
- const engineConfig = {
50
- privateVariables: ['files'],
51
- }
52
-
53
- // Apollo server setup
54
- const apolloServer = new ApolloServer({
55
- schema,
56
- context: ({ req }) => {
57
- if (req.isAuthenticated()) {
58
- return {
59
- user: req.user.id,
60
- isSuperUser: req.user.admin,
61
- userInfo: req.user,
62
- }
63
- }
64
- },
65
- // Always allow introspection - our schema is public
66
- introspection: true,
67
- // Enable authenticated queries in playground
68
- // Note - buggy at the moment
69
- playground: {
70
- settings: {
71
- 'request.credentials': 'same-origin',
72
- },
73
- },
74
- // Enable cache options
75
- tracing: true,
76
- cacheControl: true,
77
- // Don't limit the max size for dataset uploads
78
- uploads: { maxFieldSize: Infinity },
79
- formatResponse: response => {
80
- return { ...response, extensions: { openneuro: { version } } }
81
- },
82
- cache: new BaseRedisCache({
83
- client: redis,
84
- }),
85
- })
86
-
87
- // Setup pre-GraphQL middleware
88
- app.use('/crn/graphql', jwt.authenticate, auth.optional)
89
-
90
- // Inject Apollo Server
91
- apolloServer.applyMiddleware({ app, path: '/crn/graphql' })
92
-
93
- return app
94
- }