Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions resources/buildConfigDefinitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const nestedOptionTypes = [
'PagesOptions',
'PagesRoute',
'PasswordPolicyOptions',
'QueryServerOptions',
'RequestComplexityOptions',
'SecurityOptions',
'SchemaOptions',
Expand All @@ -48,6 +49,7 @@ const nestedOptionEnvPrefix = {
PagesRoute: 'PARSE_SERVER_PAGES_ROUTE_',
ParseServerOptions: 'PARSE_SERVER_',
PasswordPolicyOptions: 'PARSE_SERVER_PASSWORD_POLICY_',
QueryServerOptions: 'PARSE_SERVER_QUERY_',
RateLimitOptions: 'PARSE_SERVER_RATE_LIMIT_',
RequestComplexityOptions: 'PARSE_SERVER_REQUEST_COMPLEXITY_',
SchemaOptions: 'PARSE_SERVER_SCHEMA_',
Expand Down
219 changes: 219 additions & 0 deletions spec/ParseQuery.Aggregate.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,104 @@ describe('Parse.Query Aggregate testing', () => {
expect(new Date(results[0].date.iso)).toEqual(obj1.get('date'));
});

it_id('f01a0001-0001-0001-0001-000000000001')(it_exclude_dbs(['postgres']))('rawValues: true converts $date EJSON marker to BSON Date in $match', async () => {
const obj = new TestObject();
await obj.save();
const iso = new Date(obj.createdAt.getTime() + 1).toISOString();
const pipeline = [
{ $match: { objectId: obj.id, createdAt: { $lte: { $date: iso } } } },
{ $count: 'total' },
];
const query = new Parse.Query('TestObject');
const results = await query.aggregate(pipeline, { rawValues: true, useMasterKey: true });
expect(results.length).toBe(1);
expect(results[0].total).toBe(1);
});

it_id('f01a0001-0002-0002-0002-000000000002')(it_exclude_dbs(['postgres']))('rawValues: true deserializes $date at any nesting depth', async () => {
const obj = new TestObject();
await obj.save();
const iso = new Date(obj.createdAt.getTime() + 1).toISOString();
const pipeline = [
{
$match: {
$and: [
{ objectId: obj.id },
{ $or: [{ createdAt: { $lte: { $date: iso } } }] },
],
},
},
{ $count: 'total' },
];
const query = new Parse.Query('TestObject');
const results = await query.aggregate(pipeline, { rawValues: true, useMasterKey: true });
expect(results.length).toBe(1);
expect(results[0].total).toBe(1);
});

it_id('f01a0001-0003-0003-0003-000000000003')(it_exclude_dbs(['postgres']))('rawValues: true does NOT coerce bare ISO strings', async () => {
const obj = new TestObject();
await obj.save();
const iso = new Date(obj.createdAt.getTime() + 1).toISOString();
const pipeline = [
{ $match: { objectId: obj.id, createdAt: { $lte: iso } } },
{ $count: 'total' },
];
const query = new Parse.Query('TestObject');
const results = await query.aggregate(pipeline, { rawValues: true, useMasterKey: true });
// Bare ISO string compared against BSON Date: MongoDB string-vs-date comparison yields no matches.
expect(results.length).toBe(0);
});

it_id('f01a0001-0004-0004-0004-000000000004')(it_exclude_dbs(['postgres']))('rawValues: true does NOT coerce Parse Date encoding `{ __type: "Date", iso }`', async () => {
const obj = new TestObject();
await obj.save();
const iso = new Date(obj.createdAt.getTime() + 1).toISOString();
const pipeline = [
{
$match: {
objectId: obj.id,
createdAt: { $lte: { __type: 'Date', iso } },
},
},
{ $count: 'total' },
];
const query = new Parse.Query('TestObject');
const results = await query.aggregate(pipeline, { rawValues: true, useMasterKey: true });
// Parse Date encoding is not interpreted in rawValues mode; comparison fails silently.
expect(results.length).toBe(0);
});

it_id('f01a0001-0005-0005-0005-000000000005')(it_exclude_dbs(['postgres']))('rawValues: true serializes BSON Date in results as `{ $date: iso }`', async () => {
const obj = new TestObject();
await obj.save();
const iso = new Date(obj.createdAt.getTime() + 1).toISOString();
const pipeline = [
{ $match: { objectId: obj.id, createdAt: { $lte: { $date: iso } } } },
{ $project: { _id: 1, _created_at: 1 } },
];
const query = new Parse.Query('TestObject');
const results = await query.aggregate(pipeline, { rawValues: true, useMasterKey: true });
expect(results.length).toBe(1);
// EJSON-serialized date marker, not Parse `{ __type: 'Date', iso }` encoding.
expect(results[0]._created_at).toEqual(jasmine.objectContaining({ $date: jasmine.any(String) }));
});

it_id('f01a0001-0006-0006-0006-000000000006')(it_exclude_dbs(['postgres']))('rawValues: true deserializes EJSON in `$addFields`', async () => {
const obj = new TestObject();
await obj.save();
const iso = '2026-01-01T00:00:00.000Z';
const pipeline = [
{ $match: { objectId: obj.id } },
{ $addFields: { pinned: { $date: iso } } },
{ $project: { _id: 1, pinned: 1 } },
];
const query = new Parse.Query('TestObject');
const results = await query.aggregate(pipeline, { rawValues: true, useMasterKey: true });
expect(results.length).toBe(1);
expect(results[0].pinned).toEqual(jasmine.objectContaining({ $date: jasmine.any(String) }));
});

it_only_db('postgres')(
'can group by any date field postgres (it does not work if you have dirty data)', // rows in your collection with non date data in the field that is supposed to be a date
done => {
Expand Down Expand Up @@ -1554,4 +1652,125 @@ describe('Parse.Query Aggregate testing', () => {
expect(e.code).toBe(Parse.Error.INVALID_QUERY);
}
});

it_id('f01a0002-0001-0001-0001-000000000001')(it_exclude_dbs(['postgres']))('rawFieldNames: true lets users write _created_at directly', async () => {
const obj = new TestObject();
await obj.save();
const iso = new Date(obj.createdAt.getTime() + 1).toISOString();
const pipeline = [
{
$match: {
_id: obj.id,
_created_at: { $lte: { $date: iso } },
},
},
{ $count: 'total' },
];
const query = new Parse.Query('TestObject');
const results = await query.aggregate(pipeline, {
rawValues: true,
rawFieldNames: true,
useMasterKey: true,
});
expect(results.length).toBe(1);
expect(results[0].total).toBe(1);
});

it_id('f01a0002-0002-0002-0002-000000000002')(it_exclude_dbs(['postgres']))('rawFieldNames: true does NOT rewrite Parse-style names', async () => {
const obj = new TestObject();
await obj.save();
const iso = new Date(obj.createdAt.getTime() + 1).toISOString();
// Using Parse-style `createdAt` under rawFieldNames should query a field that doesn't exist in MongoDB.
const pipeline = [
{ $match: { _id: obj.id, createdAt: { $lte: { $date: iso } } } },
{ $count: 'total' },
];
const query = new Parse.Query('TestObject');
const results = await query.aggregate(pipeline, {
rawValues: true,
rawFieldNames: true,
useMasterKey: true,
});
// `createdAt` is not a MongoDB field name; no documents match.
expect(results.length).toBe(0);
});

it_id('f01a0002-0003-0003-0003-000000000003')(it_exclude_dbs(['postgres']))('rawFieldNames: true returns native field names in results', async () => {
const obj = new TestObject();
await obj.save();
const pipeline = [
{ $match: { _id: obj.id } },
{ $project: { _id: 1, _created_at: 1 } },
];
const query = new Parse.Query('TestObject');
const results = await query.aggregate(pipeline, {
rawValues: true,
rawFieldNames: true,
useMasterKey: true,
});
expect(results.length).toBe(1);
expect(results[0]._id).toBe(obj.id);
expect(Object.prototype.hasOwnProperty.call(results[0], '_created_at')).toBe(true);
expect(Object.prototype.hasOwnProperty.call(results[0], 'objectId')).toBe(false);
expect(Object.prototype.hasOwnProperty.call(results[0], 'createdAt')).toBe(false);
});

it_id('f01a0003-0001-0001-0001-000000000001')(it_exclude_dbs(['postgres']))('server-level rawValues default applies when per-query omits it', async () => {
await reconfigureServer({ query: { aggregationRawValues: true } });
const obj = new TestObject();
await obj.save();
const iso = new Date(obj.createdAt.getTime() + 1).toISOString();
const pipeline = [
{ $match: { objectId: obj.id, createdAt: { $lte: { $date: iso } } } },
{ $count: 'total' },
];
const query = new Parse.Query('TestObject');
// No rawValues in the per-query options — should inherit from the server default.
const results = await query.aggregate(pipeline, { useMasterKey: true });
expect(results.length).toBe(1);
expect(results[0].total).toBe(1);
});

it_id('f01a0003-0002-0002-0002-000000000002')(it_exclude_dbs(['postgres']))('per-query rawValues: false overrides server-level true', async () => {
await reconfigureServer({ query: { aggregationRawValues: true } });
const obj = new TestObject();
await obj.save();
const iso = new Date(obj.createdAt.getTime() + 1).toISOString();
// With server-level rawValues: true, EJSON `{ $date: iso }` would be converted to a BSON Date
// and the $match would succeed. Per-query rawValues: false overrides that, so `{ $date: iso }`
// is NOT deserialized as EJSON and the comparison fails — proving the override works.
const pipeline = [
{ $match: { objectId: obj.id, createdAt: { $lte: { $date: iso } } } },
{ $count: 'total' },
];
const query = new Parse.Query('TestObject');
const results = await query.aggregate(pipeline, {
rawValues: false,
useMasterKey: true,
});
// Under rawValues: false the `{ $date: iso }` is not EJSON-deserialized; comparison yields no match.
expect(results.length).toBe(0);
});

it_id('f01a0003-0003-0003-0003-000000000003')(it_exclude_dbs(['postgres']))('server-level rawFieldNames default applies when per-query omits it', async () => {
await reconfigureServer({
query: { aggregationRawValues: true, aggregationRawFieldNames: true },
});
const obj = new TestObject();
await obj.save();
const iso = new Date(obj.createdAt.getTime() + 1).toISOString();
const pipeline = [
{
$match: {
_id: obj.id,
_created_at: { $lte: { $date: iso } },
},
},
{ $count: 'total' },
];
const query = new Parse.Query('TestObject');
const results = await query.aggregate(pipeline, { useMasterKey: true });
expect(results.length).toBe(1);
expect(results[0].total).toBe(1);
});
});
Loading
Loading