diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c12e7cf72..4ffdee89b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ Details: - Branch: [beta][branch_beta] - Purpose: feature maturation - Suitable environment: development +- Added Cloud Trigger for LiveQuery event beforeUnsubscribe (sadortun) [#7419](https://github.com/parse-community/parse-server/pull/7419) +- Ensure the startup is completed before calling startupCompleted (sadortun) [#7525](https://github.com/parse-community/parse-server/pull/7525) ## 🔥 [Alpha Releases][log_alpha] diff --git a/package.json b/package.json index d80b19f9bb..5c7b6fbddf 100644 --- a/package.json +++ b/package.json @@ -135,7 +135,7 @@ "madge:circular": "node_modules/.bin/madge ./src --circular" }, "engines": { - "node": ">=12.20.0 <16" + "node": ">=12.20.0 <17" }, "bin": { "parse-server": "bin/parse-server" diff --git a/spec/ParseLiveQuery.spec.js b/spec/ParseLiveQuery.spec.js index b8b43f7a3d..cc809374b3 100644 --- a/spec/ParseLiveQuery.spec.js +++ b/spec/ParseLiveQuery.spec.js @@ -683,6 +683,45 @@ describe('ParseLiveQuery', function () { await object.save(); }); + it('can handle select beforeUnsubscribe trigger', async done => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + const user = new Parse.User(); + user.setUsername('username'); + user.setPassword('password'); + await user.signUp(); + + Parse.Cloud.beforeSubscribe(TestObject, req => { + expect(req.requestId).toBe(1); + expect(req.user).toBeDefined(); + }); + + Parse.Cloud.beforeUnsubscribe(TestObject, req => { + expect(req.requestId).toBe(1); + expect(req.user).toBeDefined(); + expect(req.user.get('username')).toBe('username'); + done(); + }); + + const object = new TestObject(); + await object.save(); + + const query = new Parse.Query(TestObject); + query.equalTo('objectId', object.id); + const subscription = await query.subscribe(); + + object.set({ foo: 'bar', yolo: 'abc' }); + await object.save(); + + await subscription.unsubscribe(); + }); + it('LiveQuery with ACL', async () => { await reconfigureServer({ liveQuery: { diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index bf870c92b8..aba077888b 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -1185,6 +1185,23 @@ describe('Parse.Query testing', () => { }); }); + it('multiple notEqualTo queries', function (done) { + const makeBoxedNumber = function (i) { + return new BoxedNumber({ number: i }); + }; + Parse.Object.saveAll([0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(makeBoxedNumber)).then(function () { + const query = new Parse.Query(BoxedNumber); + query.notEqualTo('number', 1); + query.notEqualTo('number', 2); + query.notEqualTo('number', 3); + query.notEqualTo('number', 4); + query.find().then(function (results) { + equal(results.length, 5); + done(); + }); + }); + }); + it('notEqualTo zero queries', done => { const makeBoxedNumber = i => { return new BoxedNumber({ number: i }); diff --git a/src/Adapters/Logger/WinstonLogger.js b/src/Adapters/Logger/WinstonLogger.js index fe28660056..cd4d89d947 100644 --- a/src/Adapters/Logger/WinstonLogger.js +++ b/src/Adapters/Logger/WinstonLogger.js @@ -52,7 +52,7 @@ function configureTransports(options) { colorize: true, name: 'console', silent, - format: format.combine(format.splat(), consoleFormat), + format: format.combine(format.colorize(), format.splat(), consoleFormat), }, options ); diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 20d5c4ba66..2de0371f04 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -1054,12 +1054,14 @@ class DatabaseController { // Modifies query so that it no longer has $relatedTo // Returns a promise that resolves when query is mutated reduceRelationKeys(className: string, query: any, queryOptions: any): ?Promise { - if (query['$or']) { - return Promise.all( - query['$or'].map(aQuery => { - return this.reduceRelationKeys(className, aQuery, queryOptions); - }) - ); + for (const op of ['$or', '$and', '$not']) { + if (query[op]) { + return Promise.all( + query[op].map(aQuery => { + return this.reduceRelationKeys(className, aQuery, queryOptions); + }) + ); + } } if (query['$and']) { return Promise.all( diff --git a/src/LiveQuery/ParseLiveQueryServer.js b/src/LiveQuery/ParseLiveQueryServer.js index d15a2bd70a..ffadd2bb23 100644 --- a/src/LiveQuery/ParseLiveQueryServer.js +++ b/src/LiveQuery/ParseLiveQueryServer.js @@ -846,7 +846,7 @@ class ParseLiveQueryServer { this._handleSubscribe(parseWebsocket, request); } - _handleUnsubscribe(parseWebsocket: any, request: any, notifyClient: boolean = true): any { + async _handleUnsubscribe(parseWebsocket: any, request: any, notifyClient: boolean = true): any { // If we can not find this client, return error to client if (!Object.prototype.hasOwnProperty.call(parseWebsocket, 'clientId')) { Client.pushError( @@ -893,11 +893,38 @@ class ParseLiveQueryServer { return; } + const subscription = subscriptionInfo.subscription; + const className = subscription.className; + const trigger = getTrigger(className, 'beforeUnsubscribe', Parse.applicationId); + if (trigger) { + const auth = await this.getAuthFromClient(client, request.requestId, request.sessionToken); + if (auth && auth.user) { + request.user = auth.user; + } + + request.sessionToken = subscriptionInfo.sessionToken; + request.useMasterKey = client.hasMasterKey; + request.installationId = client.installationId; + + try { + await runTrigger(trigger, `beforeUnsubscribe.${className}`, request, auth); + } catch (error) { + Client.pushError( + parseWebsocket, + error.code || Parse.Error.SCRIPT_FAILED, + error.message || error, + false + ); + logger.error( + `Failed running beforeUnsubscribe for session ${request.sessionToken} with:\n Error: ` + + JSON.stringify(error) + ); + } + } + // Remove subscription from client client.deleteSubscriptionInfo(requestId); // Remove client from subscription - const subscription = subscriptionInfo.subscription; - const className = subscription.className; subscription.deleteClientSubscription(parseWebsocket.clientId, requestId); // If there is no client which is subscribing this subscription, remove it from subscriptions const classSubscriptions = this.subscriptions.get(className); diff --git a/src/Options/index.js b/src/Options/index.js index 31b9e10d41..77edd6549f 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -242,7 +242,7 @@ export interface ParseServerOptions { playgroundPath: ?string; /* Callback when server has started */ serverStartComplete: ?(error: ?Error) => void; - /* Rest representation on Parse.Schema https://docs.parseplatform.org/rest/guide/#adding-a-schema */ + /* Rest representation on Parse.Schema */ schema: ?SchemaOptions; /* Callback when server has closed */ serverCloseComplete: ?() => void; diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index 9fb437cada..935e62d141 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -671,6 +671,42 @@ ParseCloud.onLiveQueryEvent = function (handler) { triggers.addLiveQueryEventHandler(handler, Parse.applicationId); }; +/** + * Registers a before live query subscription function. + * + * **Available in Cloud Code only.** + * + * If you want to use beforeUnsubscribe for a predefined class in the Parse JavaScript SDK (e.g. {@link Parse.User}), you should pass the class itself and not the String for arg1. + * ``` + * Parse.Cloud.beforeUnsubscribe('MyCustomClass', (request) => { + * // code here + * }, (request) => { + * // validation code here + * }); + * + * Parse.Cloud.beforeUnsubscribe(Parse.User, (request) => { + * // code here + * }, { ...validationObject }); + *``` + * + * @method beforeUnsubscribe + * @name Parse.Cloud.beforeUnsubscribe + * @param {(String|Parse.Object)} arg1 The Parse.Object subclass to register the before subscription function for. This can instead be a String that is the className of the subclass. + * @param {Function} func The function to run before a subscription. This function can be async and should take one parameter, a {@link Parse.Cloud.TriggerRequest}. + * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.TriggerRequest}, or a {@link Parse.Cloud.ValidatorObject}. + */ +ParseCloud.beforeUnsubscribe = function (parseClass, handler, validationHandler) { + validateValidator(validationHandler); + const className = triggers.getClassName(parseClass); + triggers.addTrigger( + triggers.Types.beforeUnsubscribe, + className, + handler, + Parse.applicationId, + validationHandler + ); +}; + /** * Registers an after live query server event function. * diff --git a/src/triggers.js b/src/triggers.js index 8320b5fb74..9ab279412d 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -18,6 +18,7 @@ export const Types = { afterDeleteFile: 'afterDeleteFile', beforeConnect: 'beforeConnect', beforeSubscribe: 'beforeSubscribe', + beforeUnsubscribe: 'beforeUnsubscribe', afterEvent: 'afterEvent', };