parse-server icon indicating copy to clipboard operation
parse-server copied to clipboard

feat: add `middleware` option to Parse Server config

Open okobsamoht opened this issue 3 years ago • 15 comments

New Pull Request Checklist

  • [x] I am not disclosing a vulnerability.
  • [ ] I am creating this PR in reference to an issue.

Issue Description

the 'middleware' option only works if parse-server is used as command line binary. when using parse-server as an express middleware the option 'middleware' have no effect.

Approach

i uses the same condition of the middleware in ParseServer.start({...options}, callback) to let parse-server handle the middleware in ParseServer.app({...options})

TODOs before merging

  • [x] Add tests
  • [x] Add changes to documentation (guides, repository pages, in-code descriptions)
  • [x] Add security check

okobsamoht avatar Apr 17 '22 12:04 okobsamoht

Thanks for opening this pull request!

  • ❌ Please check all required checkboxes at the top, otherwise your pull request will be closed.

  • ⚠️ Remember that a security vulnerability must only be reported confidentially, see our Security Policy. If you are not sure whether the issue is a security vulnerability, the safest way is to treat it as such and submit it confidentially to us for evaluation.

for the middleware config option you can see in: https://github.com/parse-community/parse-server/blob/alpha/src/Options/Definitions.js it already exist

okobsamoht avatar Apr 17 '22 22:04 okobsamoht

Right, it already exists, there is even a test case:

https://github.com/parse-community/parse-server/blob/4c29d4d23b67e4abaf25803fe71cae47ce1b5957/spec/index.spec.js#L489-L508

If the test already passes, why is this PR needed?

mtrezza avatar Apr 17 '22 22:04 mtrezza

it works only when i run parse-server like this: parse-server --appId APPLICATION_ID --masterKey MASTER_KEY --databaseURI mongodb://localhost/test --middleware [middleware_path]

but it does not work when i do this:

var ParseServer = require('parse-server').ParseServer;

var app = express();
var api = new ParseServer({ 
   ...options,
   middleware: 'middleware'
});```

app.use('/parse', api);

var port = 1337;
app.listen(port, function() {
  console.log('parse-server-example running on port ' + port + '.');
});

i was struggling on it

okobsamoht avatar Apr 17 '22 22:04 okobsamoht

What's the difference between your example and the test?

mtrezza avatar Apr 18 '22 14:04 mtrezza

the diffrerence between the two is explained here:

  • for using parseserver's internal express app: https://github.com/parse-community/parse-server/blob/8b919613f0babe1bbf524fb391b0d3853e029e68/src/ParseServer.js#L252
  • for starting parseserver inside another express app: https://github.com/parse-community/parse-server/blob/8b919613f0babe1bbf524fb391b0d3853e029e68/src/ParseServer.js#L152

okobsamoht avatar Apr 19 '22 22:04 okobsamoht

the tests calls the parseserver's internal express app : https://github.com/parse-community/parse-server/blob/8b919613f0babe1bbf524fb391b0d3853e029e68/spec/helper.js#L169

the lines of code handling the 'middleware' option is inside that function

so i copy the logic from the ParseServer.start() function: https://github.com/parse-community/parse-server/blob/8b919613f0babe1bbf524fb391b0d3853e029e68/src/ParseServer.js#L257

https://github.com/parse-community/parse-server/blob/8b919613f0babe1bbf524fb391b0d3853e029e68/src/ParseServer.js#L259

to the ParseServer.app() function: https://github.com/parse-community/parse-server/blob/8b919613f0babe1bbf524fb391b0d3853e029e68/src/ParseServer.js#L154

okobsamoht avatar Apr 19 '22 22:04 okobsamoht

Codecov Report

Merging #7942 (8fec581) into alpha (38ba9b4) will not change coverage. The diff coverage is 100.00%.

:exclamation: Current head 8fec581 differs from pull request most recent head 8fb5d69. Consider uploading reports for the commit 8fb5d69 to get more accurate results

@@           Coverage Diff           @@
##            alpha    #7942   +/-   ##
=======================================
  Coverage   94.17%   94.17%           
=======================================
  Files         182      182           
  Lines       13712    13712           
=======================================
  Hits        12913    12913           
  Misses        799      799           
Impacted Files Coverage Δ
src/ParseServer.js 90.42% <100.00%> (+0.37%) :arrow_up:
src/Adapters/Files/GridFSBucketAdapter.js 79.50% <0.00%> (-0.82%) :arrow_down:
src/RestWrite.js 94.42% <0.00%> (ø)
...dapters/Storage/Postgres/PostgresStorageAdapter.js 95.76% <0.00%> (+0.05%) :arrow_up:

Continue to review full report at Codecov.

Legend - Click here to learn more Δ = absolute <relative> (impact), ø = not affected, ? = missing data Powered by Codecov. Last update 38ba9b4...8fb5d69. Read the comment docs.

codecov[bot] avatar Apr 21 '22 00:04 codecov[bot]

I can confirm that I have written a test that fails on alpha but passes on this branch. I have provided it in my review.

Could you please also link the related issue and amend the PR to follow the template. This process ensures that future users experiencing similar issues can easily identify the problem and related fix. 😊

dblythy avatar Apr 28 '22 04:04 dblythy

Looks mostly good - might just have to fix lint and tests

dblythy avatar May 08 '22 15:05 dblythy

@okobsamoht could you take a look at this PR to get this ready for merging?

mtrezza avatar Jun 17 '22 11:06 mtrezza

Come to think of it do you really need config.middleware when using the Parse Server express constructor? Could you just use:


const app = express();
app.use(…custom middleware here)
app.use("/parse", new ParseServer(config));
app.listen(1337)

dblythy avatar Jul 02 '22 21:07 dblythy

Come to think of it do you really need config.middleware when using the Parse Server express constructor? Could you just use:

const app = express();
app.use(…custom middleware here)
app.use("/parse", new ParseServer(config));
app.listen(1337)

Hi @dblythy when i do that, my custom middleware have no access to ParseServer runtime stuffs like Controllers, Adapters, Options. and when i log req.config i always get undefined in my custom middleare.

okobsamoht avatar Jul 09 '22 09:07 okobsamoht

i make this quick setup:

var express = require('express');
var ParseServer = require('parse-server').ParseServer;

var api = new ParseServer({
    appId: "APP_ID",
    appNAme: "APP_NAME",
    javascriptKey: "JAVASCRIPT_KEY",
    masterKey: "MASTER_KEY",
    directAccess: true,
    enforcePrivateUsers: true,
    port: 3000,
    mountPath: '/api',
    cloud:'cloudCode',
    middleware:(req, res, next)=>{
        console.log('parse middleware_________________________________________________________________________________')
        console.log(req.config)
        next()
    }
});

var app = express();

app.use((req, res, next)=>{
    console.log('express middleware___________________________________________________________________________________')
    console.log(req.config)
    next()
})
// parse server
app.use('/api', api);

var httpServer = require('http').createServer(app);
httpServer.listen(3000);

and each time a ran a request here is the result:

express middleware___________________________________________________________________________________
undefined
parse middleware_________________________________________________________________________________
<ref *2> Config {
  applicationId: 'APP_ID',
  appId: 'APP_ID',
  appNAme: 'APP_NAME',
  javascriptKey: 'JAVASCRIPT_KEY',
  masterKey: 'MASTER_KEY',
  directAccess: true,
  enforcePrivateUsers: true,
  port: 3000,
  mountPath: '/api',
  cloud: 'cloudCode',
  middleware: [Function: middleware],
  allowClientClassCreation: true,
  allowCustomObjectId: false,
  cacheMaxSize: 10000,
  cacheTTL: 5000,
  collectionPrefix: '',
  customPages: {},
  databaseURI: 'mongodb://localhost:27017/parse',
  emailVerifyTokenReuseIfValid: false,
  enableAnonymousUsers: true,
  enableExpressErrorHandler: false,
  expireInactiveSessions: true,
  fileUpload: {
    enableForAnonymousUser: false,
    enableForPublic: false,
    enableForAuthenticatedUser: true
  },
  graphQLPath: '/graphql',
  host: '0.0.0.0',
  idempotencyOptions: { ttl: 300, paths: [] },
  logsFolder: './logs/',
  masterKeyIps: [],
  maxUploadSize: '20mb',
  mountGraphQL: false,
  mountPlayground: false,
  objectIdSize: 10,
  pages: {
    enableRouter: false,
    enableLocalization: false,
    localizationJsonPath: undefined,
    localizationFallbackLocale: 'en',
    placeholders: {},
    forceRedirect: false,
    pagesPath: './public',
    pagesEndpoint: 'apps',
    customUrls: {},
    customRoutes: []
  },
  playgroundPath: '/playground',
  preserveFileName: false,
  preventLoginWithUnverifiedEmail: false,
  protectedFields: { _User: { '*': [Array] } },
  requestKeywordDenylist: [
    { key: '_bsontype', value: 'Code' },
    { key: 'constructor' },
    { key: '__proto__' }
  ],
  revokeSessionOnPasswordReset: true,
  scheduledPush: false,
  security: { enableCheck: false, enableCheckLog: false },
  sessionLength: 31536000,
  verifyUserEmails: false,
  jsonLogs: false,
  verbose: false,
  level: undefined,
  serverURL: 'http://localhost:3000/api',
  loggerController: LoggerController {
    options: {
      jsonLogs: false,
      logsFolder: './logs/',
      verbose: false,
      logLevel: undefined,
      silent: undefined,
      maxLogFiles: undefined
    },
    appId: 'APP_ID',
    debug: [Function (anonymous)],
    verbose: [Function (anonymous)],
    silly: [Function (anonymous)],
    [Symbol()]: WinstonLoggerAdapter {}
  },
  filesController: FilesController {
    options: { preserveFileName: false },
    appId: 'APP_ID',
    [Symbol()]: GridFSBucketAdapter {
      _databaseURI: 'mongodb://localhost:27017/parse',
      _algorithm: 'aes-256-gcm',
      _encryptionKey: null,
      _mongoOptions: [Object]
    }
  },
  userController: UserController {
    options: { verifyUserEmails: false },
    appId: 'APP_ID',
    [Symbol()]: undefined
  },
  pushController: PushController {},
  hasPushScheduledSupport: false,
  hasPushSupport: false,
  pushWorker: PushWorker {
    adapter: ParsePushAdapter {
      supportsPushTracking: true,
      validPushTypes: [Array],
      senderMap: {},
      feature: [Object]
    },
    channel: 'APP_ID-parse-server-push',
    subscriber: Consumer {
      _events: [Object: null prototype],
      _eventsCount: 1,
      _maxListeners: undefined,
      emitter: [EventEmitter],
      [Symbol(kCapture)]: false
    }
  },
  pushControllerQueue: PushQueue {
    channel: 'APP_ID-parse-server-push',
    batchSize: 100,
    parsePublisher: Publisher { emitter: [EventEmitter] }
  },
  analyticsController: AnalyticsController {
    options: undefined,
    appId: undefined,
    [Symbol()]: AnalyticsAdapter {}
  },
  cacheController: <ref *1> CacheController {
    options: {},
    appId: 'APP_ID',
    role: SubCache { prefix: 'role', cache: [Circular *1], ttl: undefined },
    user: SubCache { prefix: 'user', cache: [Circular *1], ttl: undefined },
    graphQL: SubCache {
      prefix: 'graphQL',
      cache: [Circular *1],
      ttl: undefined
    },
    [Symbol()]: InMemoryCacheAdapter { cache: [LRUCache] }
  },
  parseGraphQLController: ParseGraphQLController {
    databaseController: DatabaseController {
      adapter: [MongoStorageAdapter],
      options: [Object],
      idempotencyOptions: [Object],
      _transactionalSession: null
    },
    cacheController: <ref *1> CacheController {
      options: {},
      appId: 'APP_ID',
      role: [SubCache],
      user: [SubCache],
      graphQL: [SubCache],
      [Symbol()]: [InMemoryCacheAdapter]
    },
    isMounted: false,
    configCacheKey: 'config'
  },
  liveQueryController: LiveQueryController {
    classNames: Set(0) {},
    liveQueryPublisher: ParseCloudCodePublisher { parsePublisher: [Publisher] }
  },
  database: DatabaseController {
    adapter: MongoStorageAdapter {
      _uri: 'mongodb://localhost:27017/parse',
      _collectionPrefix: '',
      _mongoOptions: [Object],
      _onchange: [Function (anonymous)],
      _maxTimeMS: undefined,
      canSortOnJoinTables: true,
      enableSchemaHooks: false,
      connectionPromise: [Promise],
      client: [MongoClient],
      database: [Db]
    },
    options: [Circular *2],
    idempotencyOptions: { ttl: 300, paths: [] },
    schemaPromise: null,
    _transactionalSession: null
  },
  hooksController: HooksController {
    _applicationId: 'APP_ID',
    _webhookKey: undefined,
    database: DatabaseController {
      adapter: [MongoStorageAdapter],
      options: [Object],
      idempotencyOptions: [Object],
      _transactionalSession: null
    }
  },
  authDataManager: {
    getValidatorForProvider: [Function: getValidatorForProvider],
    setEnableAnonymousUsers: [Function: setEnableAnonymousUsers]
  },
  schemaCache: {
    all: [Function: all],
    get: [Function: get],
    put: [Function: put],
    del: [Function: del],
    clear: [Function: clear]
  },
  _mount: 'http://127.0.0.1:3000/api',
  generateSessionExpiresAt: [Function: bound generateSessionExpiresAt],
  generateEmailVerifyTokenExpiresAt: [Function: bound generateEmailVerifyTokenExpiresAt],
  headers: {
    'user-agent': 'node-XMLHttpRequest, Parse/js1.11.1 (NodeJS 16.15.0)',
    accept: '*/*',
    'content-type': 'text/plain',
    host: '127.0.0.1:3000',
    'content-length': '146',
    connection: 'close'
  },
  ip: '::ffff:127.0.0.1'
}
express middleware___________________________________________________________________________________
undefined
parse middleware_________________________________________________________________________________
<ref *2> Config {
  applicationId: 'APP_ID',
  appId: 'APP_ID',
  appNAme: 'APP_NAME',
  javascriptKey: 'JAVASCRIPT_KEY',
.........................

okobsamoht avatar Jul 09 '22 09:07 okobsamoht

Thanks for the explanation! I think this could be solved by #7869 - exposing the config via config.get. I think using express’ native syntax of app.use for middleware is a bit more intuitive than ParseServer.middleware and it’s easier to see the order of middleware.

dblythy avatar Jul 10 '22 01:07 dblythy