From db2d4fe0d7907db262ad290ad8fc41f640905967 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 30 May 2026 13:47:40 +1000 Subject: [PATCH 1/4] refactor: Remove internal Parse.Query usage from Auth and LiveQuery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Towards #8787. Auth's session/role resolution and LiveQuery's _clearCachedRoles now always go through RestQuery instead of the Parse JS SDK's Parse.Query. The !config SDK fallback branches in Auth (getAuthForSessionToken, getRolesForUser, getRolesByIds) are removed — all real callers already pass a config, so a config is now required. Threads config into the LiveQuery getAuthForSessionToken calls. Deletes the dead SessionTokenCache (exported but never used in src). Drops the obsolete no-config Auth/Role specs and rewrites the role-based ACL LiveQuery specs to mock the auth seam instead of the removed Parse.Query. --- spec/Auth.spec.js | 37 -------- spec/ParseLiveQueryServer.spec.js | 73 ++++----------- spec/ParseRole.spec.js | 4 - spec/SessionTokenCache.spec.js | 54 ----------- src/Auth.js | 128 ++++++++++---------------- src/LiveQuery/ParseLiveQueryServer.ts | 31 +++++-- src/LiveQuery/SessionTokenCache.js | 50 ---------- 7 files changed, 91 insertions(+), 286 deletions(-) delete mode 100644 spec/SessionTokenCache.spec.js delete mode 100644 src/LiveQuery/SessionTokenCache.js diff --git a/spec/Auth.spec.js b/spec/Auth.spec.js index a055cda5bc..f387662631 100644 --- a/spec/Auth.spec.js +++ b/spec/Auth.spec.js @@ -114,20 +114,6 @@ describe('Auth', () => { expect(session.get('expiresAt') > expiry).toBeTrue(); }); - it('should load auth without a config', async () => { - const user = new Parse.User(); - await user.signUp({ - username: 'hello', - password: 'password', - }); - expect(user.getSessionToken()).not.toBeUndefined(); - const userAuth = await getAuthForSessionToken({ - sessionToken: user.getSessionToken(), - }); - expect(userAuth.user instanceof Parse.User).toBe(true); - expect(userAuth.user.id).toBe(user.id); - }); - it('should load auth with a config', async () => { const user = new Parse.User(); await user.signUp({ @@ -146,29 +132,6 @@ describe('Auth', () => { describe('getRolesForUser', () => { const rolesNumber = 100; - it('should load all roles without config', async () => { - const user = new Parse.User(); - await user.signUp({ - username: 'hello', - password: 'password', - }); - expect(user.getSessionToken()).not.toBeUndefined(); - const userAuth = await getAuthForSessionToken({ - sessionToken: user.getSessionToken(), - }); - const roles = []; - for (let i = 0; i < rolesNumber; i++) { - const acl = new Parse.ACL(); - const role = new Parse.Role('roleloadtest' + i, acl); - role.getUsers().add([user]); - roles.push(role); - } - const savedRoles = await Parse.Object.saveAll(roles); - expect(savedRoles.length).toBe(rolesNumber); - const cloudRoles = await userAuth.getRolesForUser(); - expect(cloudRoles.length).toBe(rolesNumber); - }); - it('should load all roles with config', async () => { const user = new Parse.User(); await user.signUp({ diff --git a/spec/ParseLiveQueryServer.spec.js b/spec/ParseLiveQueryServer.spec.js index 62bf33d327..de1e812c65 100644 --- a/spec/ParseLiveQueryServer.spec.js +++ b/spec/ParseLiveQueryServer.spec.js @@ -1506,34 +1506,16 @@ describe('ParseLiveQueryServer', function () { }; const requestId = 0; - spyOn(Parse, 'Query').and.callFake(function () { - let shouldReturn = false; - return { - equalTo() { - shouldReturn = true; - // Nothing to do here - return this; + // The user has the "liveQueryRead" role, but the ACL only grants read access + // to "otherLiveQueryRead", so it should not match. + spyOn(parseLiveQueryServer, 'getAuthForSessionToken').and.returnValue( + Promise.resolve({ + userId: 'someUserId', + auth: { + getUserRoles: () => Promise.resolve(['role:liveQueryRead']), }, - containedIn() { - shouldReturn = false; - return this; - }, - find() { - if (!shouldReturn) { - return Promise.resolve([]); - } - //Return a role with the name "liveQueryRead" as that is what was set on the ACL - const liveQueryRole = new Parse.Role('liveQueryRead', new Parse.ACL()); - liveQueryRole.id = 'abcdef1234'; - return Promise.resolve([liveQueryRole]); - }, - }; - }); - - parseLiveQueryServer._matchesACL(acl, client, requestId).then(function (isMatched) { - expect(isMatched).toBe(false); - done(); - }); + }) + ); parseLiveQueryServer._matchesACL(acl, client, requestId).then(function (isMatched) { expect(isMatched).toBe(false); @@ -1553,36 +1535,15 @@ describe('ParseLiveQueryServer', function () { }; const requestId = 0; - spyOn(Parse, 'Query').and.callFake(function () { - let shouldReturn = false; - return { - equalTo() { - shouldReturn = true; - // Nothing to do here - return this; - }, - containedIn() { - shouldReturn = false; - return this; - }, - find() { - if (!shouldReturn) { - return Promise.resolve([]); - } - //Return a role with the name "liveQueryRead" as that is what was set on the ACL - const liveQueryRole = new Parse.Role('liveQueryRead', new Parse.ACL()); - liveQueryRole.id = 'abcdef1234'; - return Promise.resolve([liveQueryRole]); - }, - each(callback) { - //Return a role with the name "liveQueryRead" as that is what was set on the ACL - const liveQueryRole = new Parse.Role('liveQueryRead', new Parse.ACL()); - liveQueryRole.id = 'abcdef1234'; - callback(liveQueryRole); - return Promise.resolve(); + // The user has the "liveQueryRead" role, which the ACL grants read access to. + spyOn(parseLiveQueryServer, 'getAuthForSessionToken').and.returnValue( + Promise.resolve({ + userId: 'someUserId', + auth: { + getUserRoles: () => Promise.resolve(['role:liveQueryRead']), }, - }; - }); + }) + ); parseLiveQueryServer._matchesACL(acl, client, requestId).then(function (isMatched) { expect(isMatched).toBe(true); diff --git a/spec/ParseRole.spec.js b/spec/ParseRole.spec.js index 95e6189a6a..a2caf6bbec 100644 --- a/spec/ParseRole.spec.js +++ b/spec/ParseRole.spec.js @@ -201,10 +201,6 @@ describe('Parse Role testing', () => { testLoadRoles(Config.get('test'), done); }); - it('should recursively load roles without config', done => { - testLoadRoles(undefined, done); - }); - it('_Role object should not save without name.', done => { const role = new Parse.Role(); role.save(null, { useMasterKey: true }).then( diff --git a/spec/SessionTokenCache.spec.js b/spec/SessionTokenCache.spec.js deleted file mode 100644 index 6b3c83df62..0000000000 --- a/spec/SessionTokenCache.spec.js +++ /dev/null @@ -1,54 +0,0 @@ -const SessionTokenCache = require('../lib/LiveQuery/SessionTokenCache').SessionTokenCache; - -describe('SessionTokenCache', function () { - beforeEach(function (done) { - const Parse = require('parse/node'); - - spyOn(Parse, 'Query').and.returnValue({ - first: jasmine.createSpy('first').and.returnValue( - Promise.resolve( - new Parse.Object('_Session', { - user: new Parse.User({ id: 'userId' }), - }) - ) - ), - equalTo: function () {}, - }); - - done(); - }); - - it('can get undefined userId', function (done) { - const sessionTokenCache = new SessionTokenCache(); - - sessionTokenCache.getUserId(undefined).then( - () => {}, - error => { - expect(error).not.toBeNull(); - done(); - } - ); - }); - - it('can get existing userId', function (done) { - const sessionTokenCache = new SessionTokenCache(); - const sessionToken = 'sessionToken'; - const userId = 'userId'; - sessionTokenCache.cache.set(sessionToken, userId); - - sessionTokenCache.getUserId(sessionToken).then(userIdFromCache => { - expect(userIdFromCache).toBe(userId); - done(); - }); - }); - - it('can get new userId', function (done) { - const sessionTokenCache = new SessionTokenCache(); - - sessionTokenCache.getUserId('sessionToken').then(userIdFromCache => { - expect(userIdFromCache).toBe('userId'); - expect(sessionTokenCache.cache.size).toBe(1); - done(); - }); - }); -}); diff --git a/src/Auth.js b/src/Auth.js index dd75aaace2..39160a18ab 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -154,32 +154,21 @@ const getAuthForSessionToken = async function ({ } } - let results; - if (config) { - const restOptions = { - limit: 1, - include: 'user', - }; - const RestQuery = require('./RestQuery'); - const query = await RestQuery({ - method: RestQuery.Method.get, - config, - runBeforeFind: false, - auth: master(config), - className: '_Session', - restWhere: { sessionToken }, - restOptions, - }); - results = (await query.execute()).results; - } else { - results = ( - await new Parse.Query(Parse.Session) - .limit(1) - .include('user') - .equalTo('sessionToken', sessionToken) - .find({ useMasterKey: true }) - ).map(obj => obj.toJSON()); - } + const restOptions = { + limit: 1, + include: 'user', + }; + const RestQuery = require('./RestQuery'); + const query = await RestQuery({ + method: RestQuery.Method.get, + config, + runBeforeFind: false, + auth: master(config), + className: '_Session', + restWhere: { sessionToken }, + restOptions, + }); + const results = (await query.execute()).results; if (results.length !== 1 || !results[0]['user']) { throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); @@ -267,29 +256,23 @@ Auth.prototype.getUserRoles = function () { Auth.prototype.getRolesForUser = async function () { //Stack all Parse.Role const results = []; - if (this.config) { - const restWhere = { - users: { - __type: 'Pointer', - className: '_User', - objectId: this.user.id, - }, - }; - const RestQuery = require('./RestQuery'); - const query = await RestQuery({ - method: RestQuery.Method.find, - runBeforeFind: false, - config: this.config, - auth: master(this.config), - className: '_Role', - restWhere, - }); - await query.each(result => results.push(result)); - } else { - await new Parse.Query(Parse.Role) - .equalTo('users', this.user) - .each(result => results.push(result.toJSON()), { useMasterKey: true }); - } + const restWhere = { + users: { + __type: 'Pointer', + className: '_User', + objectId: this.user.id, + }, + }; + const RestQuery = require('./RestQuery'); + const query = await RestQuery({ + method: RestQuery.Method.find, + runBeforeFind: false, + config: this.config, + auth: master(this.config), + className: '_Role', + restWhere, + }); + await query.each(result => results.push(result)); return results; }; @@ -355,37 +338,24 @@ Auth.prototype.clearRoleCache = function (sessionToken) { Auth.prototype.getRolesByIds = async function (ins) { const results = []; // Build an OR query across all parentRoles - if (!this.config) { - await new Parse.Query(Parse.Role) - .containedIn( - 'roles', - ins.map(id => { - const role = new Parse.Object(Parse.Role); - role.id = id; - return role; - }) - ) - .each(result => results.push(result.toJSON()), { useMasterKey: true }); - } else { - const roles = ins.map(id => { - return { - __type: 'Pointer', - className: '_Role', - objectId: id, - }; - }); - const restWhere = { roles: { $in: roles } }; - const RestQuery = require('./RestQuery'); - const query = await RestQuery({ - method: RestQuery.Method.find, - config: this.config, - runBeforeFind: false, - auth: master(this.config), + const roles = ins.map(id => { + return { + __type: 'Pointer', className: '_Role', - restWhere, - }); - await query.each(result => results.push(result)); - } + objectId: id, + }; + }); + const restWhere = { roles: { $in: roles } }; + const RestQuery = require('./RestQuery'); + const query = await RestQuery({ + method: RestQuery.Method.find, + config: this.config, + runBeforeFind: false, + auth: master(this.config), + className: '_Role', + restWhere, + }); + await query.each(result => results.push(result)); return results; }; diff --git a/src/LiveQuery/ParseLiveQueryServer.ts b/src/LiveQuery/ParseLiveQueryServer.ts index f835fe2140..ed230bf259 100644 --- a/src/LiveQuery/ParseLiveQueryServer.ts +++ b/src/LiveQuery/ParseLiveQueryServer.ts @@ -18,7 +18,8 @@ import { resolveError, toJSONwithObjects, } from '../triggers'; -import { getAuthForSessionToken, Auth } from '../Auth'; +import { getAuthForSessionToken, master, Auth } from '../Auth'; +import RestQuery from '../RestQuery'; import { getCacheController, getDatabaseController } from '../Controllers'; import Config from '../Config'; import { LRUCache as LRU } from 'lru-cache'; @@ -606,19 +607,36 @@ class ParseLiveQueryServer { async _clearCachedRoles(userId: string) { try { - const validTokens = await new Parse.Query(Parse.Session) - .equalTo('user', Parse.User.createWithoutData(userId)) - .find({ useMasterKey: true }); + const config = Config.get(this.config.appId); + const query = await RestQuery({ + method: RestQuery.Method.find, + config, + runBeforeFind: false, + auth: master(config), + className: '_Session', + restWhere: { + user: { + __type: 'Pointer', + className: '_User', + objectId: userId, + }, + }, + }); + const { results: validTokens } = await query.execute(); await Promise.all( validTokens.map(async token => { - const sessionToken = token.get('sessionToken'); + const sessionToken = token.sessionToken; const authPromise = this.authCache.get(sessionToken); if (!authPromise) { return; } const [auth1, auth2] = await Promise.all([ authPromise, - getAuthForSessionToken({ cacheController: this.cacheController, sessionToken }), + getAuthForSessionToken({ + cacheController: this.cacheController, + sessionToken, + config: Config.get(this.config.appId), + }), ]); auth1.auth?.clearRoleCache(sessionToken); auth2.auth?.clearRoleCache(sessionToken); @@ -641,6 +659,7 @@ class ParseLiveQueryServer { const authPromise = getAuthForSessionToken({ cacheController: this.cacheController, sessionToken: sessionToken, + config: Config.get(this.config.appId), }) .then(auth => { return { auth, userId: auth && auth.user && auth.user.id }; diff --git a/src/LiveQuery/SessionTokenCache.js b/src/LiveQuery/SessionTokenCache.js deleted file mode 100644 index a7f52b65a0..0000000000 --- a/src/LiveQuery/SessionTokenCache.js +++ /dev/null @@ -1,50 +0,0 @@ -import Parse from 'parse/node'; -import { LRUCache as LRU } from 'lru-cache'; -import logger from '../logger'; - -function userForSessionToken(sessionToken) { - var q = new Parse.Query('_Session'); - q.equalTo('sessionToken', sessionToken); - return q.first({ useMasterKey: true }).then(function (session) { - if (!session) { - return Promise.reject('No session found for session token'); - } - return session.get('user'); - }); -} - -class SessionTokenCache { - cache: Object; - - constructor(timeout: number = 30 * 24 * 60 * 60 * 1000, maxSize: number = 10000) { - this.cache = new LRU({ - max: maxSize, - ttl: timeout, - }); - } - - getUserId(sessionToken: string): any { - if (!sessionToken) { - return Promise.reject('Empty sessionToken'); - } - const userId = this.cache.get(sessionToken); - if (userId) { - logger.verbose('Fetch userId %s of sessionToken %s from Cache', userId, sessionToken); - return Promise.resolve(userId); - } - return userForSessionToken(sessionToken).then( - user => { - logger.verbose('Fetch userId %s of sessionToken %s from Parse', user.id, sessionToken); - const userId = user.id; - this.cache.set(sessionToken, userId); - return Promise.resolve(userId); - }, - error => { - logger.error('Can not fetch userId for sessionToken %j, error %j', sessionToken, error); - return Promise.reject(error); - } - ); - } -} - -export { SessionTokenCache }; From bb2d8aec6bcfb86d8f093de70955a7cdedd474d1 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 30 May 2026 14:01:00 +1000 Subject: [PATCH 2/4] refactor: Isolate Parse.Query SDK usage behind QueryAdapter Introduce src/cloud-code/QueryAdapter.js as the single boundary between parse-server's internal REST/JSON query format and the Parse JS SDK's Parse.Query. Core code (triggers, rest, RestQuery, LiveQuery) now calls inflateQuery/deflateQuery/isQuery/applyQueryToRest instead of constructing or inspecting Parse.Query directly, so the SDK query type lives in one place. Towards #8787. --- src/LiveQuery/ParseLiveQueryServer.ts | 8 ++-- src/LiveQuery/QueryTools.js | 5 +- src/RestQuery.js | 4 +- src/cloud-code/QueryAdapter.js | 68 +++++++++++++++++++++++++++ src/rest.js | 3 +- src/triggers.js | 57 +++------------------- 6 files changed, 85 insertions(+), 60 deletions(-) create mode 100644 src/cloud-code/QueryAdapter.js diff --git a/src/LiveQuery/ParseLiveQueryServer.ts b/src/LiveQuery/ParseLiveQueryServer.ts index ed230bf259..b72e9d0ee2 100644 --- a/src/LiveQuery/ParseLiveQueryServer.ts +++ b/src/LiveQuery/ParseLiveQueryServer.ts @@ -20,6 +20,7 @@ import { } from '../triggers'; import { getAuthForSessionToken, master, Auth } from '../Auth'; import RestQuery from '../RestQuery'; +import { inflateQuery, deflateQuery } from '../cloud-code/QueryAdapter'; import { getCacheController, getDatabaseController } from '../Controllers'; import Config from '../Config'; import { LRUCache as LRU } from 'lru-cache'; @@ -1015,13 +1016,10 @@ class ParseLiveQueryServer { request.user = auth.user; } - const parseQuery = new Parse.Query(className); - parseQuery.withJSON(request.query); - request.query = parseQuery; + request.query = inflateQuery(className, request.query); await runTrigger(trigger, `beforeSubscribe.${className}`, request, auth); - const query = request.query.toJSON(); - request.query = query; + request.query = deflateQuery(request.query); } if (className === '_Session') { diff --git a/src/LiveQuery/QueryTools.js b/src/LiveQuery/QueryTools.js index 37cfbfa47f..cf0b51329d 100644 --- a/src/LiveQuery/QueryTools.js +++ b/src/LiveQuery/QueryTools.js @@ -3,6 +3,7 @@ var Id = require('./Id'); var Parse = require('parse/node'); var vm = require('vm'); var logger = require('../logger').default; +const { isQuery } = require('../cloud-code/QueryAdapter'); var regexTimeout = 0; // IMPORTANT: vmContext is shared across all calls for performance (vm.createContext() is expensive). @@ -97,7 +98,7 @@ function stringify(object): string { * skip, and limit. */ function queryHash(query) { - if (query instanceof Parse.Query) { + if (isQuery(query)) { query = { className: query.className, where: query._where, @@ -169,7 +170,7 @@ function contains(haystack: Array, needle: any): boolean { * queries, we can avoid building a full-blown query tool. */ function matchesQuery(object: any, query: any): boolean { - if (query instanceof Parse.Query) { + if (isQuery(query)) { var className = object.id instanceof Id ? object.id.className : object.className; if (className !== query.className) { return false; diff --git a/src/RestQuery.js b/src/RestQuery.js index 8456db6a3d..8ac8aacace 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -5,6 +5,7 @@ var SchemaController = require('./Controllers/SchemaController'); var Parse = require('parse/node').Parse; var logger = require('./logger').default; const triggers = require('./triggers'); +const { inflateQuery } = require('./cloud-code/QueryAdapter'); const { continueWhile } = require('parse/lib/node/promiseUtils'); const AlwaysSelectedKeys = ['objectId', 'createdAt', 'updatedAt', 'ACL']; const { enforceRoleSecurity } = require('./SharedRest'); @@ -1121,8 +1122,7 @@ _UnsafeRestQuery.prototype.runAfterFindTrigger = function () { const json = Object.assign({}, this.restOptions); json.where = this.restWhere; - const parseQuery = new Parse.Query(this.className); - parseQuery.withJSON(json); + const parseQuery = inflateQuery(this.className, json); // Run afterFind trigger and set the new results return triggers .maybeRunAfterFindTrigger( diff --git a/src/cloud-code/QueryAdapter.js b/src/cloud-code/QueryAdapter.js new file mode 100644 index 0000000000..d650141578 --- /dev/null +++ b/src/cloud-code/QueryAdapter.js @@ -0,0 +1,68 @@ +// QueryAdapter — the single boundary between parse-server's internal, +// SDK-agnostic query format and the Parse JS SDK's `Parse.Query`. +// +// parse-server represents a query internally as plain JSON +// (`{ className, where, ...restOptions }`). Cloud Code triggers +// (`beforeFind`, `afterFind`, `beforeSubscribe`), however, are a public +// contract that hands the handler a real `Parse.Query` instance and may +// receive a modified one back. This module is the only place in `src/` that +// constructs or inspects a `Parse.Query`, so the rest of the codebase stays +// free of a direct SDK dependency on the query type. Towards #8787. + +import Parse from 'parse/node'; + +// Fields that `Parse.Query#toJSON()` may surface and that map onto +// parse-server's `restOptions`. `where` is handled separately as `restWhere`. +const REST_OPTION_KEYS = [ + 'limit', + 'skip', + 'include', + 'excludeKeys', + 'explain', + 'keys', + 'order', + 'hint', + 'comment', +]; + +// Build a `Parse.Query` from parse-server's internal format so it can be +// handed to a Cloud Code trigger. `json` is `{ where, ...restOptions }`; +// omit it to produce an empty query for the class. +export function inflateQuery(className, json) { + const query = new Parse.Query(className); + if (json) { + query.withJSON(json); + } + return query; +} + +// Convert a `Parse.Query` (typically one a trigger returned or mutated) back +// into parse-server's internal JSON format. +export function deflateQuery(query) { + return query.toJSON(); +} + +// Whether a value is a `Parse.Query` instance. Used where a trigger may return +// either a modified query or some other value. +export function isQuery(value) { + return value instanceof Parse.Query; +} + +// Merge the JSON of a (possibly trigger-modified) `Parse.Query` onto existing +// `restWhere` / `restOptions`, preserving the field-by-field override semantics +// the `beforeFind` trigger has always used. +export function applyQueryToRest(query, restWhere, restOptions) { + const jsonQuery = deflateQuery(query); + if (jsonQuery.where) { + restWhere = jsonQuery.where; + } + for (const key of REST_OPTION_KEYS) { + if (jsonQuery[key]) { + restOptions = restOptions || {}; + restOptions[key] = jsonQuery[key]; + } + } + return { restWhere, restOptions }; +} + +export default { inflateQuery, deflateQuery, isQuery, applyQueryToRest }; diff --git a/src/rest.js b/src/rest.js index 7a78f2f8b5..ca805e3288 100644 --- a/src/rest.js +++ b/src/rest.js @@ -12,6 +12,7 @@ var Parse = require('parse/node').Parse; var RestQuery = require('./RestQuery'); var RestWrite = require('./RestWrite'); var triggers = require('./triggers'); +const { inflateQuery } = require('./cloud-code/QueryAdapter'); const Auth = require('./Auth'); const { enforceRoleSecurity } = require('./SharedRest'); const { createSanitizedError } = require('./Error'); @@ -106,7 +107,7 @@ async function runFindTriggers( className, objectsForAfterFind, config, - new Parse.Query(className).withJSON({ where: restWhere, ...restOptions }), + inflateQuery(className, { where: restWhere, ...restOptions }), context, isGet ); diff --git a/src/triggers.js b/src/triggers.js index f66d96f942..4ac2582d5f 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -2,6 +2,7 @@ import Parse from 'parse/node'; import { logger } from './logger'; import Utils from './Utils'; +import { inflateQuery, isQuery, applyQueryToRest } from './cloud-code/QueryAdapter'; export const Types = { beforeLogin: 'beforeLogin', @@ -493,16 +494,12 @@ export function maybeRunAfterFindTrigger( const request = getRequestObject(triggerType, auth, null, null, config, context, isGet); // Convert query parameter to Parse.Query instance - if (query instanceof Parse.Query) { + if (isQuery(query)) { request.query = query; } else if (typeof query === 'object' && query !== null) { - const parseQueryInstance = new Parse.Query(classNameQuery); - if (query.where) { - parseQueryInstance.withJSON(query); - } - request.query = parseQueryInstance; + request.query = inflateQuery(classNameQuery, query); } else { - request.query = new Parse.Query(classNameQuery); + request.query = inflateQuery(classNameQuery); } const { success, error } = getResponseObject( @@ -584,8 +581,7 @@ export function maybeRunQueryTrigger( const json = Object.assign({}, restOptions); json.where = restWhere; - const parseQuery = new Parse.Query(className); - parseQuery.withJSON(json); + const parseQuery = inflateQuery(className, json); let count = false; if (restOptions) { @@ -613,49 +609,10 @@ export function maybeRunQueryTrigger( .then( result => { let queryResult = parseQuery; - if (result && result instanceof Parse.Query) { + if (result && isQuery(result)) { queryResult = result; } - const jsonQuery = queryResult.toJSON(); - if (jsonQuery.where) { - restWhere = jsonQuery.where; - } - if (jsonQuery.limit) { - restOptions = restOptions || {}; - restOptions.limit = jsonQuery.limit; - } - if (jsonQuery.skip) { - restOptions = restOptions || {}; - restOptions.skip = jsonQuery.skip; - } - if (jsonQuery.include) { - restOptions = restOptions || {}; - restOptions.include = jsonQuery.include; - } - if (jsonQuery.excludeKeys) { - restOptions = restOptions || {}; - restOptions.excludeKeys = jsonQuery.excludeKeys; - } - if (jsonQuery.explain) { - restOptions = restOptions || {}; - restOptions.explain = jsonQuery.explain; - } - if (jsonQuery.keys) { - restOptions = restOptions || {}; - restOptions.keys = jsonQuery.keys; - } - if (jsonQuery.order) { - restOptions = restOptions || {}; - restOptions.order = jsonQuery.order; - } - if (jsonQuery.hint) { - restOptions = restOptions || {}; - restOptions.hint = jsonQuery.hint; - } - if (jsonQuery.comment) { - restOptions = restOptions || {}; - restOptions.comment = jsonQuery.comment; - } + ({ restWhere, restOptions } = applyQueryToRest(queryResult, restWhere, restOptions)); if (requestObject.readPreference) { restOptions = restOptions || {}; restOptions.readPreference = requestObject.readPreference; From 0638023d1d8d875af8643bdda5f8a2afa1ed1e06 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 30 May 2026 14:51:47 +1000 Subject: [PATCH 3/4] fix: Preserve falsy beforeFind query overrides in QueryAdapter applyQueryToRest tested option truthiness, so an explicit falsy override from a beforeFind trigger (e.g. query.limit(0)) was silently dropped. Test presence with hasOwnProperty instead. Parse.Query#toJSON only emits keys that were set, so no default values leak in. Adds a regression test. --- spec/CloudCode.spec.js | 13 +++++++++++++ src/cloud-code/QueryAdapter.js | 7 ++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 941d896aae..28e40d9896 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -2673,6 +2673,19 @@ describe('beforeFind hooks', () => { }); }); + it('should preserve a falsy query override from beforeFind (limit 0)', async () => { + Parse.Cloud.beforeFind('MyObject', req => { + req.query.limit(0); + }); + + const obj0 = new Parse.Object('MyObject'); + const obj1 = new Parse.Object('MyObject'); + await Parse.Object.saveAll([obj0, obj1]); + + const results = await new Parse.Query('MyObject').find(); + expect(results.length).toBe(0); + }); + it('should have object found with nested relational data query', async () => { const obj1 = Parse.Object.extend('TestObject'); const obj2 = Parse.Object.extend('TestObject2'); diff --git a/src/cloud-code/QueryAdapter.js b/src/cloud-code/QueryAdapter.js index d650141578..c561ccb987 100644 --- a/src/cloud-code/QueryAdapter.js +++ b/src/cloud-code/QueryAdapter.js @@ -49,15 +49,16 @@ export function isQuery(value) { } // Merge the JSON of a (possibly trigger-modified) `Parse.Query` onto existing -// `restWhere` / `restOptions`, preserving the field-by-field override semantics -// the `beforeFind` trigger has always used. +// `restWhere` / `restOptions` with the field-by-field override semantics the +// `beforeFind` trigger uses. Presence is tested via `hasOwnProperty` so an +// explicit falsy override (e.g. `limit(0)`) is preserved rather than dropped. export function applyQueryToRest(query, restWhere, restOptions) { const jsonQuery = deflateQuery(query); if (jsonQuery.where) { restWhere = jsonQuery.where; } for (const key of REST_OPTION_KEYS) { - if (jsonQuery[key]) { + if (Object.prototype.hasOwnProperty.call(jsonQuery, key)) { restOptions = restOptions || {}; restOptions[key] = jsonQuery[key]; } From 26d962cc435cb813d7a627984a8c6b25805c7e0a Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 30 May 2026 15:31:52 +1000 Subject: [PATCH 4/4] refactor: Isolate Parse.Object conversion behind ObjectAdapter Towards #8787. Make src/cloud-code/ObjectAdapter.js the sole owner of Parse.Object construction and inspection, mirroring QueryAdapter. The SDK pending-ops state-tracking seam is deferred to a follow-up. --- src/Auth.js | 9 ++-- src/Controllers/UserController.js | 4 +- src/LiveQuery/ParseCloudCodePublisher.js | 5 +- src/LiveQuery/ParseLiveQueryServer.ts | 29 +++++++----- src/RestQuery.js | 3 +- src/RestWrite.js | 3 +- src/Routers/FunctionsRouter.js | 3 +- src/Routers/SessionsRouter.js | 5 +- src/Routers/UsersRouter.js | 15 +++--- src/cloud-code/ObjectAdapter.js | 60 ++++++++++++++++++++++++ src/rest.js | 3 +- src/triggers.js | 26 +++++----- 12 files changed, 117 insertions(+), 48 deletions(-) create mode 100644 src/cloud-code/ObjectAdapter.js diff --git a/src/Auth.js b/src/Auth.js index 39160a18ab..3e3f868b70 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -1,6 +1,7 @@ const Parse = require('parse/node'); import { isDeepStrictEqual } from 'util'; import { getRequestObject, resolveError } from './triggers'; +import { inflateObject } from './cloud-code/ObjectAdapter'; import { logger } from './logger'; import { LRUCache as LRU } from 'lru-cache'; import RestQuery from './RestQuery'; @@ -140,7 +141,7 @@ const getAuthForSessionToken = async function ({ cacheController.user.del(sessionToken); throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Session token is expired.'); } - const cachedUser = Parse.Object.fromJSON(userJSON); + const cachedUser = inflateObject(userJSON); renewSessionIfNeeded({ config, sessionToken }); return Promise.resolve( new Auth({ @@ -192,7 +193,7 @@ const getAuthForSessionToken = async function ({ cacheController.user.put(sessionToken, { ...obj, expiresAt: expiresAt?.toISOString() }); } renewSessionIfNeeded({ config, session, sessionToken }); - const userObject = Parse.Object.fromJSON(obj); + const userObject = inflateObject(obj); return new Auth({ config, cacheController, @@ -228,7 +229,7 @@ var getAuthForLegacySessionToken = async function ({ config, sessionToken, insta } obj.className = '_User'; - const userObject = Parse.Object.fromJSON(obj); + const userObject = inflateObject(obj); return new Auth({ config, isMaster: false, @@ -555,7 +556,7 @@ const checkIfUserHasProvidedConfiguredProvidersForLogin = ( const handleAuthDataValidation = async (authData, req, foundUser) => { let user; if (foundUser) { - user = Parse.User.fromJSON({ className: '_User', ...foundUser }); + user = inflateObject({ className: '_User', ...foundUser }); // Find user by session and current objectId; only pass user if it's the current user or master key is provided } else if ( (req.auth && diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index c8b74d2ab4..b8a17b89a4 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -162,7 +162,7 @@ export class UserController extends AdaptableController { if (typeof shouldSendEmail === 'function') { const response = await Promise.resolve( this.config.sendUserEmailVerification({ - user: Parse.Object.fromJSON({ className: '_User', ...fetchedUser }), + user: inflate({ className: '_User', ...fetchedUser }), master: req.auth?.isMaster, }) ); @@ -205,7 +205,7 @@ export class UserController extends AdaptableController { return Promise.resolve(true); } const shouldSend = await this.setEmailVerifyToken(user, { - object: Parse.User.fromJSON(Object.assign({ className: '_User' }, user)), + object: inflate(Object.assign({ className: '_User' }, user)), master, installationId, ip, diff --git a/src/LiveQuery/ParseCloudCodePublisher.js b/src/LiveQuery/ParseCloudCodePublisher.js index 0e0dce1417..0c500aded1 100644 --- a/src/LiveQuery/ParseCloudCodePublisher.js +++ b/src/LiveQuery/ParseCloudCodePublisher.js @@ -1,5 +1,6 @@ import { ParsePubSub } from './ParsePubSub'; import Parse from 'parse/node'; +import { toFullJSON } from '../cloud-code/ObjectAdapter'; import logger from '../logger'; class ParseCloudCodePublisher { @@ -44,10 +45,10 @@ class ParseCloudCodePublisher { ); // We need the full JSON which includes className const message = { - currentParseObject: request.object._toFullJSON(), + currentParseObject: toFullJSON(request.object), }; if (request.original) { - message.originalParseObject = request.original._toFullJSON(); + message.originalParseObject = toFullJSON(request.original); } if (request.classLevelPermissions) { message.classLevelPermissions = request.classLevelPermissions; diff --git a/src/LiveQuery/ParseLiveQueryServer.ts b/src/LiveQuery/ParseLiveQueryServer.ts index b72e9d0ee2..006c27dd5a 100644 --- a/src/LiveQuery/ParseLiveQueryServer.ts +++ b/src/LiveQuery/ParseLiveQueryServer.ts @@ -8,6 +8,11 @@ import logger from '../logger'; import RequestSchema from './RequestSchema'; import { matchesQuery, queryHash } from './QueryTools'; import { ParsePubSub } from './ParsePubSub'; +import { + inflateObject, + hydrateFromFullJSON, + disableSingleInstance, +} from '../cloud-code/ObjectAdapter'; import SchemaController from '../Controllers/SchemaController'; import _ from 'lodash'; import { randomUUID } from 'crypto'; @@ -60,7 +65,7 @@ class ParseLiveQueryServer { logger.verbose('Support key pairs', this.keyPairs); // Initialize Parse - Parse.Object.disableSingleInstance(); + disableSingleInstance(); const serverURL = config.serverURL || Parse.serverURL; Parse.serverURL = serverURL; Parse.initialize(config.appId, Parse.javaScriptKey, config.masterKey); @@ -159,18 +164,18 @@ class ParseLiveQueryServer { // Inflate merged object const currentParseObject = message.currentParseObject; UserRouter.removeHiddenProperties(currentParseObject); - let className = currentParseObject.className; - let parseObject = new Parse.Object(className); - parseObject._finishFetch(currentParseObject); - message.currentParseObject = parseObject; + message.currentParseObject = hydrateFromFullJSON( + currentParseObject.className, + currentParseObject + ); // Inflate original object const originalParseObject = message.originalParseObject; if (originalParseObject) { UserRouter.removeHiddenProperties(originalParseObject); - className = originalParseObject.className; - parseObject = new Parse.Object(className); - parseObject._finishFetch(originalParseObject); - message.originalParseObject = parseObject; + message.originalParseObject = hydrateFromFullJSON( + originalParseObject.className, + originalParseObject + ); } } @@ -246,7 +251,7 @@ class ParseLiveQueryServer { res.user = auth.user; } if (res.object) { - res.object = Parse.Object.fromJSON(res.object); + res.object = inflateObject(res.object); } await runTrigger(trigger, `afterEvent.${className}`, res, auth); } @@ -409,10 +414,10 @@ class ParseLiveQueryServer { const trigger = getTrigger(className, 'afterEvent', Parse.applicationId); if (trigger) { if (res.object) { - res.object = Parse.Object.fromJSON(res.object); + res.object = inflateObject(res.object); } if (res.original) { - res.original = Parse.Object.fromJSON(res.original); + res.original = inflateObject(res.original); } const auth = await this.getAuthFromClient(client, requestId); if (auth && auth.user) { diff --git a/src/RestQuery.js b/src/RestQuery.js index 8ac8aacace..acf5fbbcff 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -6,6 +6,7 @@ var Parse = require('parse/node').Parse; var logger = require('./logger').default; const triggers = require('./triggers'); const { inflateQuery } = require('./cloud-code/QueryAdapter'); +const { isObject } = require('./cloud-code/ObjectAdapter'); const { continueWhile } = require('parse/lib/node/promiseUtils'); const AlwaysSelectedKeys = ['objectId', 'createdAt', 'updatedAt', 'ACL']; const { enforceRoleSecurity } = require('./SharedRest'); @@ -1139,7 +1140,7 @@ _UnsafeRestQuery.prototype.runAfterFindTrigger = function () { // Ensure we properly set the className back if (this.redirectClassName) { this.response.results = results.map(object => { - if (object instanceof Parse.Object) { + if (isObject(object)) { object = object.toJSON(); } object.className = this.redirectClassName; diff --git a/src/RestWrite.js b/src/RestWrite.js index 98c9fd4656..31b451a72a 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -10,6 +10,7 @@ var cryptoUtils = require('./cryptoUtils'); var passwordCrypto = require('./password'); var Parse = require('parse/node'); var triggers = require('./triggers'); +const { inflateObject } = require('./cloud-code/ObjectAdapter'); const util = require('util'); import RestQuery from './RestQuery'; import _ from 'lodash'; @@ -1858,7 +1859,7 @@ RestWrite.prototype.buildParseObjects = function () { originalObject = triggers.inflate(extraData, this.originalData); } - const className = Parse.Object.fromJSON(extraData); + const className = inflateObject(extraData); const readOnlyAttributes = className.constructor.readOnlyAttributes ? className.constructor.readOnlyAttributes() : []; diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js index 8bcf8a5858..ff6a4e3259 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -2,6 +2,7 @@ var Parse = require('parse/node').Parse, triggers = require('../triggers'); +const { inflateObject } = require('../cloud-code/ObjectAdapter'); import PromiseRouter from '../PromiseRouter'; import { promiseEnforceMasterKeyAccess, promiseEnsureIdempotency } from '../middlewares'; @@ -43,7 +44,7 @@ function parseObject(obj, config) { } return Parse.File.fromJSON(obj); } else if (obj && obj.__type == 'Pointer') { - return Parse.Object.fromJSON({ + return inflateObject({ __type: 'Pointer', className: obj.className, objectId: obj.objectId, diff --git a/src/Routers/SessionsRouter.js b/src/Routers/SessionsRouter.js index 7afb540ee6..a5d1e7b2a5 100644 --- a/src/Routers/SessionsRouter.js +++ b/src/Routers/SessionsRouter.js @@ -1,5 +1,6 @@ import ClassesRouter from './ClassesRouter'; import Parse from 'parse/node'; +import { inflateObject } from '../cloud-code/ObjectAdapter'; import rest from '../rest'; import Auth from '../Auth'; import RestWrite from '../RestWrite'; @@ -41,7 +42,7 @@ export class SessionsRouter extends ClassesRouter { : new Auth.Auth({ config: req.config, isMaster: false, - user: Parse.Object.fromJSON({ className: '_User', objectId: userId }), + user: inflateObject({ className: '_User', objectId: userId }), installationId: req.info.installationId, }); const response = await rest.get( @@ -92,7 +93,7 @@ export class SessionsRouter extends ClassesRouter { : new Auth.Auth({ config, isMaster: false, - user: Parse.Object.fromJSON({ className: '_User', objectId: user.id }), + user: inflateObject({ className: '_User', objectId: user.id }), installationId: req.auth.installationId, }); const response = await rest.find( diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index ddbae0da4a..3127b17e96 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -1,6 +1,7 @@ // These methods handle the User-related routes. import Parse from 'parse/node'; +import { inflateObject } from '../cloud-code/ObjectAdapter'; import Config from '../Config'; import AccountLockout from '../AccountLockout'; import ClassesRouter from './ClassesRouter'; @@ -161,7 +162,7 @@ export class UsersRouter extends ClassesRouter { master: req.auth.isMaster, ip: req.config.ip, installationId: req.auth.installationId, - object: Parse.User.fromJSON(Object.assign({ className: '_User' }, user)), + object: inflateObject(Object.assign({ className: '_User' }, user)), createdWith: RestWrite.buildCreatedWith('login', authProvider), }; @@ -303,7 +304,7 @@ export class UsersRouter extends ClassesRouter { await maybeRunTrigger( TriggerTypes.beforeLogin, req.auth, - Parse.User.fromJSON(Object.assign({ className: '_User' }, user)), + inflateObject(Object.assign({ className: '_User' }, user)), null, req.config, req.info.context @@ -336,7 +337,7 @@ export class UsersRouter extends ClassesRouter { await createSession(); - const afterLoginUser = Parse.User.fromJSON(Object.assign({ className: '_User' }, user)); + const afterLoginUser = inflateObject(Object.assign({ className: '_User' }, user)); await maybeRunTrigger( TriggerTypes.afterLogin, { ...req.auth, user: afterLoginUser }, @@ -355,7 +356,7 @@ export class UsersRouter extends ClassesRouter { : new Auth.Auth({ config: req.config, isMaster: false, - user: Parse.Object.fromJSON({ className: '_User', objectId: user.objectId }), + user: inflateObject({ className: '_User', objectId: user.objectId }), installationId: req.info.installationId, }); let filteredUser; @@ -457,7 +458,7 @@ export class UsersRouter extends ClassesRouter { : new Auth.Auth({ config: req.config, isMaster: false, - user: Parse.Object.fromJSON({ className: '_User', objectId: user.objectId }), + user: inflateObject({ className: '_User', objectId: user.objectId }), installationId: req.info.installationId, }); let filteredUser; @@ -729,7 +730,7 @@ export class UsersRouter extends ClassesRouter { // Find the provider used to find the user const provider = Object.keys(authData).find(key => authData[key] && authData[key].id); - parseUser = Parse.User.fromJSON({ className: '_User', ...results[0] }); + parseUser = inflateObject({ className: '_User', ...results[0] }); request = getRequestObject(undefined, req.auth, parseUser, parseUser, req.config); request.isChallenge = true; // Validate authData used to identify the user to avoid brute-force attack on `id` @@ -746,7 +747,7 @@ export class UsersRouter extends ClassesRouter { } if (!parseUser) { - parseUser = user ? Parse.User.fromJSON({ className: '_User', ...user }) : undefined; + parseUser = user ? inflateObject({ className: '_User', ...user }) : undefined; } if (!request) { diff --git a/src/cloud-code/ObjectAdapter.js b/src/cloud-code/ObjectAdapter.js new file mode 100644 index 0000000000..b87a3e4194 --- /dev/null +++ b/src/cloud-code/ObjectAdapter.js @@ -0,0 +1,60 @@ +// ObjectAdapter — the single boundary between parse-server's internal, +// SDK-agnostic object format and the Parse JS SDK's `Parse.Object`. +// +// parse-server represents an object internally as plain JSON (REST format). +// Cloud Code triggers (`beforeSave`, `afterSave`, `afterFind`, ...) and the +// LiveQuery server, however, are a public contract that hands the handler a +// real `Parse.Object` instance. This module is the conversion seam: the only +// place in `src/` that constructs or inspects a `Parse.Object`, so the rest of +// the codebase stays free of a direct SDK dependency on the object type. +// Towards #8787. Mirrors `QueryAdapter`. +// +// Scope note: this covers the *conversion* half only (inflate / deflate / +// type-check / hydrate). The SDK's pending-ops state tracking — the seam that +// flows a `beforeSave` handler's `set`/`unset`/`increment` mutations back into +// the write — is deferred to a follow-up and still reaches into the SDK +// directly (`_getStateIdentifier`, `getObjectStateController`, `_getSaveJSON`, +// `_handleSaveResponse`, `toJSONwithObjects`). + +import Parse from 'parse/node'; + +// Build a `Parse.Object` from parse-server's REST format so it can be handed +// to a Cloud Code trigger. `data` is either a className string or a REST-format +// object; `restObject` is an optional set of fields merged on top. A payload +// with `className: '_User'` yields a `Parse.User`, so this also covers the +// `_User` / `Parse.User.fromJSON` call sites. +export function inflateObject(data, restObject) { + const copy = typeof data === 'object' ? data : { className: data }; + for (const key in restObject) { + copy[key] = restObject[key]; + } + return Parse.Object.fromJSON(copy); +} + +// Whether a value is a `Parse.Object` instance. Used where a trigger may return +// or be handed either a real object or some other value. +export function isObject(value) { + return value instanceof Parse.Object; +} + +// Deflate a `Parse.Object` to its full JSON representation (including the +// `__type`/`className` envelope), for LiveQuery message payloads. +export function toFullJSON(object) { + return object._toFullJSON(); +} + +// Hydrate a `Parse.Object` from a full-JSON attribute payload without going +// through `fromJSON`. Used by the LiveQuery server to rebuild objects received +// from the publisher. +export function hydrateFromFullJSON(className, attributes) { + const object = new Parse.Object(className); + object._finishFetch(attributes); + return object; +} + +// Passthrough for the LiveQuery server's single-instance opt-out. +export function disableSingleInstance() { + Parse.Object.disableSingleInstance(); +} + +export default { inflateObject, isObject, toFullJSON, hydrateFromFullJSON, disableSingleInstance }; diff --git a/src/rest.js b/src/rest.js index ca805e3288..1a3a3ce7b0 100644 --- a/src/rest.js +++ b/src/rest.js @@ -13,6 +13,7 @@ var RestQuery = require('./RestQuery'); var RestWrite = require('./RestWrite'); var triggers = require('./triggers'); const { inflateQuery } = require('./cloud-code/QueryAdapter'); +const { inflateObject } = require('./cloud-code/ObjectAdapter'); const Auth = require('./Auth'); const { enforceRoleSecurity } = require('./SharedRest'); const { createSanitizedError } = require('./Error'); @@ -198,7 +199,7 @@ function del(config, auth, className, objectId, context) { } var cacheAdapter = config.cacheController; cacheAdapter.user.del(firstResult.sessionToken); - inflatedObject = Parse.Object.fromJSON(firstResult); + inflatedObject = inflateObject(firstResult); return triggers.maybeRunTrigger( triggers.Types.beforeDelete, auth, diff --git a/src/triggers.js b/src/triggers.js index 4ac2582d5f..467cc7238e 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -3,6 +3,7 @@ import Parse from 'parse/node'; import { logger } from './logger'; import Utils from './Utils'; import { inflateQuery, isQuery, applyQueryToRest } from './cloud-code/QueryAdapter'; +import { inflateObject, isObject } from './cloud-code/ObjectAdapter'; export const Types = { beforeLogin: 'beforeLogin', @@ -486,7 +487,7 @@ export function maybeRunAfterFindTrigger( const trigger = getTrigger(classNameQuery, triggerType, config.applicationId); if (!trigger) { - if (objectsInput && objectsInput.length > 0 && objectsInput[0] instanceof Parse.Object) { + if (objectsInput && objectsInput.length > 0 && isObject(objectsInput[0])) { return resolve(objectsInput.map(obj => toJSONwithObjects(obj))); } return resolve(objectsInput || []); @@ -516,7 +517,7 @@ export function maybeRunAfterFindTrigger( classNameQuery, 'AfterFind Input (Pre-Transform)', JSON.stringify( - objectsInput.map(o => (o instanceof Parse.Object ? o.id + ':' + o.className : o)) + objectsInput.map(o => (isObject(o) ? o.id + ':' + o.className : o)) ), auth, config.logLevels.triggerBeforeSuccess @@ -524,13 +525,13 @@ export function maybeRunAfterFindTrigger( // Convert plain objects to Parse.Object instances for trigger request.objects = objectsInput.map(currentObject => { - if (currentObject instanceof Parse.Object) { + if (isObject(currentObject)) { return currentObject; } // Preserve the original className if it exists, otherwise use the query className const originalClassName = currentObject.className || classNameQuery; const tempObjectWithClassName = { ...currentObject, className: originalClassName }; - return Parse.Object.fromJSON(tempObjectWithClassName); + return inflateObject(tempObjectWithClassName); }); return Promise.resolve() .then(() => { @@ -626,11 +627,11 @@ export function maybeRunQueryTrigger( restOptions.subqueryReadPreference = requestObject.subqueryReadPreference; } let objects = undefined; - if (result instanceof Parse.Object) { + if (isObject(result)) { objects = [result]; } else if ( Array.isArray(result) && - (!result.length || result.every(obj => obj instanceof Parse.Object)) + (!result.length || result.every(obj => isObject(obj))) ) { objects = result; } @@ -979,15 +980,10 @@ export function maybeRunTrigger( }); } -// Converts a REST-format object to a Parse.Object -// data is either className or an object -export function inflate(data, restObject) { - var copy = typeof data == 'object' ? data : { className: data }; - for (var key in restObject) { - copy[key] = restObject[key]; - } - return Parse.Object.fromJSON(copy); -} +// Converts a REST-format object to a Parse.Object. +// Thin re-export of the ObjectAdapter conversion seam so existing +// `triggers.inflate` callers keep working. +export const inflate = inflateObject; export function runLiveQueryEventHandlers(data, applicationId = Parse.applicationId) { if (!_triggerStore || !_triggerStore[applicationId] || !_triggerStore[applicationId].LiveQuery) {