feat: add `middleware` option to Parse Server config
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
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
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?
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
What's the difference between your example and the test?
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
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
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 dataPowered by Codecov. Last update 38ba9b4...8fb5d69. Read the comment docs.
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. 😊
Looks mostly good - might just have to fix lint and tests
@okobsamoht could you take a look at this PR to get this ready for merging?
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)
Come to think of it do you really need
config.middlewarewhen 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.
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',
.........................
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.