Skip to content

Commit b942cdd

Browse files
Copilothotlong
andcommitted
fix: address all 5 code review findings — RPC backward compat, remove unreachable branches, fix expand normalization
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> Agent-Logs-Url: https://github.com/objectstack-ai/spec/sessions/aae1804c-cfe4-4529-af70-e59031349057
1 parent e0b0a78 commit b942cdd

6 files changed

Lines changed: 140 additions & 33 deletions

File tree

content/docs/references/data/data-engine.mdx

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ Options for DataEngine.aggregate operations
6868
| :--- | :--- | :--- | :--- |
6969
| **method** | `string` || |
7070
| **object** | `string` || |
71-
| **query** | `Object` || QueryAST-aligned options for DataEngine.aggregate operations |
71+
| **query** | `Object` || |
7272

7373

7474
---
@@ -108,7 +108,7 @@ Options for DataEngine.count operations
108108
| :--- | :--- | :--- | :--- |
109109
| **method** | `string` || |
110110
| **object** | `string` || |
111-
| **query** | `Object` | optional | QueryAST-aligned options for DataEngine.count operations |
111+
| **query** | `Object` | optional | |
112112

113113

114114
---
@@ -137,7 +137,7 @@ Options for DataEngine.delete operations
137137
| **method** | `string` || |
138138
| **object** | `string` || |
139139
| **id** | `string \| number` | optional | ID for single delete, or use where in options |
140-
| **options** | `Object` | optional | QueryAST-aligned options for DataEngine.delete operations |
140+
| **options** | `Object` | optional | |
141141

142142

143143
---
@@ -186,7 +186,7 @@ Reference: [__schema0](./__schema0)
186186
| :--- | :--- | :--- | :--- |
187187
| **method** | `string` || |
188188
| **object** | `string` || |
189-
| **query** | `Object` | optional | QueryAST-aligned query options for IDataEngine.find() operations |
189+
| **query** | `Object` | optional | |
190190

191191

192192
---
@@ -199,7 +199,7 @@ Reference: [__schema0](./__schema0)
199199
| :--- | :--- | :--- | :--- |
200200
| **method** | `string` || |
201201
| **object** | `string` || |
202-
| **query** | `Object` | optional | QueryAST-aligned query options for IDataEngine.find() operations |
202+
| **query** | `Object` | optional | |
203203

204204

205205
---
@@ -268,7 +268,7 @@ This schema accepts one of the following structures:
268268
| :--- | :--- | :--- | :--- |
269269
| **method** | `string` || |
270270
| **object** | `string` || |
271-
| **query** | `Object` | optional | QueryAST-aligned query options for IDataEngine.find() operations |
271+
| **query** | `Object` | optional | |
272272

273273
---
274274

@@ -280,7 +280,7 @@ This schema accepts one of the following structures:
280280
| :--- | :--- | :--- | :--- |
281281
| **method** | `string` || |
282282
| **object** | `string` || |
283-
| **query** | `Object` | optional | QueryAST-aligned query options for IDataEngine.find() operations |
283+
| **query** | `Object` | optional | |
284284

285285
---
286286

@@ -307,7 +307,7 @@ This schema accepts one of the following structures:
307307
| **object** | `string` || |
308308
| **data** | `Record<string, any>` || |
309309
| **id** | `string \| number` | optional | ID for single update, or use where in options |
310-
| **options** | `Object` | optional | QueryAST-aligned options for DataEngine.update operations |
310+
| **options** | `Object` | optional | |
311311

312312
---
313313

@@ -320,7 +320,7 @@ This schema accepts one of the following structures:
320320
| **method** | `string` || |
321321
| **object** | `string` || |
322322
| **id** | `string \| number` | optional | ID for single delete, or use where in options |
323-
| **options** | `Object` | optional | QueryAST-aligned options for DataEngine.delete operations |
323+
| **options** | `Object` | optional | |
324324

325325
---
326326

@@ -332,7 +332,7 @@ This schema accepts one of the following structures:
332332
| :--- | :--- | :--- | :--- |
333333
| **method** | `string` || |
334334
| **object** | `string` || |
335-
| **query** | `Object` | optional | QueryAST-aligned options for DataEngine.count operations |
335+
| **query** | `Object` | optional | |
336336

337337
---
338338

@@ -344,7 +344,7 @@ This schema accepts one of the following structures:
344344
| :--- | :--- | :--- | :--- |
345345
| **method** | `string` || |
346346
| **object** | `string` || |
347-
| **query** | `Object` || QueryAST-aligned options for DataEngine.aggregate operations |
347+
| **query** | `Object` || |
348348

349349
---
350350

@@ -447,7 +447,7 @@ Options for DataEngine.update operations
447447
| **object** | `string` || |
448448
| **data** | `Record<string, any>` || |
449449
| **id** | `string \| number` | optional | ID for single update, or use where in options |
450-
| **options** | `Object` | optional | QueryAST-aligned options for DataEngine.update operations |
450+
| **options** | `Object` | optional | |
451451

452452

453453
---

packages/objectql/src/engine.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -890,9 +890,8 @@ export class ObjectQL implements IDataEngine {
890890

891891
// 1. Extract ID from data or where if it's a single update by ID
892892
let id = data.id;
893-
if (!id && options?.where) {
894-
if (typeof options.where === 'string') id = options.where;
895-
else if (options.where.id) id = options.where.id;
893+
if (!id && options?.where && typeof options.where === 'object' && 'id' in options.where) {
894+
id = (options.where as Record<string, unknown>).id;
896895
}
897896

898897
const opCtx: OperationContext = {
@@ -945,9 +944,8 @@ export class ObjectQL implements IDataEngine {
945944

946945
// Extract ID logic similar to update
947946
let id: any = undefined;
948-
if (options?.where) {
949-
if (typeof options.where === 'string') id = options.where;
950-
else if (options.where.id) id = options.where.id;
947+
if (options?.where && typeof options.where === 'object' && 'id' in options.where) {
948+
id = (options.where as Record<string, unknown>).id;
951949
}
952950

953951
const opCtx: OperationContext = {

packages/objectql/src/protocol-data.test.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,12 @@ describe('ObjectStackProtocolImplementation - Data Operations', () => {
7878
query: { populate: ['assignee'], expand: 'project' },
7979
});
8080

81-
// populate names take precedence, but the pre-existing expand string
82-
// prevents the Record from being created (edge case)
81+
// populate names take precedence; the non-object expand string is
82+
// cleaned up first, then populate-derived names create the Record.
8383
const callArgs = mockEngine.find.mock.calls[0][1];
8484
expect(callArgs.populate).toBeUndefined();
8585
expect(callArgs.$expand).toBeUndefined();
86-
// The expand string blocks Record creation then gets cleaned up as non-object
87-
expect(callArgs.expand).toBeUndefined();
86+
expect(callArgs.expand).toEqual({ assignee: { object: 'assignee' } });
8887
});
8988

9089
it('should pass expand Record object through as-is', async () => {

packages/objectql/src/protocol.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -357,14 +357,18 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
357357
}
358358
delete options.populate;
359359
delete options.$expand;
360+
// Clean up non-object expand (e.g. string) BEFORE the Record conversion
361+
// below, so that populate-derived names can create the expand Record even
362+
// when a legacy string expand was also present.
363+
if (typeof options.expand !== 'object' || options.expand === null) {
364+
delete options.expand;
365+
}
360366
// Only set expand if not already an object (advanced usage)
361367
if (expandNames.length > 0 && !options.expand) {
362368
options.expand = {} as Record<string, any>;
363369
for (const rel of expandNames) {
364370
options.expand[rel] = { object: rel };
365371
}
366-
} else if (typeof options.expand !== 'object' || options.expand === null) {
367-
delete options.expand;
368372
}
369373

370374
// Boolean fields

packages/spec/src/data/data-engine.test.ts

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,25 @@ describe('DataEngineFindRequestSchema', () => {
457457

458458
expect(request.query?.where).toBeDefined();
459459
});
460+
461+
it('should accept find with legacy params (backward compat)', () => {
462+
const request = DataEngineFindRequestSchema.parse({
463+
method: 'find',
464+
object: 'account',
465+
query: {
466+
filter: { status: 'active' },
467+
select: ['id', 'name'],
468+
sort: [{ field: 'name', order: 'asc' }],
469+
skip: 10,
470+
populate: ['owner'],
471+
},
472+
});
473+
474+
expect(request.query?.filter).toEqual({ status: 'active' });
475+
expect(request.query?.select).toEqual(['id', 'name']);
476+
expect(request.query?.skip).toBe(10);
477+
expect(request.query?.populate).toEqual(['owner']);
478+
});
460479
});
461480

462481
describe('DataEngineFindOneRequestSchema', () => {
@@ -471,6 +490,19 @@ describe('DataEngineFindOneRequestSchema', () => {
471490

472491
expect(request.method).toBe('findOne');
473492
});
493+
494+
it('should accept find one with legacy params (backward compat)', () => {
495+
const request = DataEngineFindOneRequestSchema.parse({
496+
method: 'findOne',
497+
object: 'account',
498+
query: {
499+
filter: { id: '123' },
500+
select: ['id', 'name'],
501+
},
502+
});
503+
504+
expect(request.query?.filter).toEqual({ id: '123' });
505+
});
474506
});
475507

476508
describe('DataEngineInsertRequestSchema', () => {
@@ -524,7 +556,7 @@ describe('DataEngineUpdateRequestSchema', () => {
524556
expect(request.id).toBe('123');
525557
});
526558

527-
it('should accept update with filter', () => {
559+
it('should accept update with legacy filter (backward compat)', () => {
528560
const request = DataEngineUpdateRequestSchema.parse({
529561
method: 'update',
530562
object: 'account',
@@ -535,6 +567,22 @@ describe('DataEngineUpdateRequestSchema', () => {
535567
},
536568
});
537569

570+
expect(request.options?.filter).toEqual({ category: 'premium' });
571+
expect(request.options?.multi).toBe(true);
572+
});
573+
574+
it('should accept update with new where (preferred)', () => {
575+
const request = DataEngineUpdateRequestSchema.parse({
576+
method: 'update',
577+
object: 'account',
578+
data: { status: 'active' },
579+
options: {
580+
where: { category: 'premium' },
581+
multi: true,
582+
},
583+
});
584+
585+
expect(request.options?.where).toEqual({ category: 'premium' });
538586
expect(request.options?.multi).toBe(true);
539587
});
540588
});
@@ -550,7 +598,7 @@ describe('DataEngineDeleteRequestSchema', () => {
550598
expect(request.id).toBe('123');
551599
});
552600

553-
it('should accept delete with filter', () => {
601+
it('should accept delete with legacy filter (backward compat)', () => {
554602
const request = DataEngineDeleteRequestSchema.parse({
555603
method: 'delete',
556604
object: 'account',
@@ -560,6 +608,21 @@ describe('DataEngineDeleteRequestSchema', () => {
560608
},
561609
});
562610

611+
expect(request.options?.filter).toEqual({ status: 'archived' });
612+
expect(request.options?.multi).toBe(true);
613+
});
614+
615+
it('should accept delete with new where (preferred)', () => {
616+
const request = DataEngineDeleteRequestSchema.parse({
617+
method: 'delete',
618+
object: 'account',
619+
options: {
620+
where: { status: 'archived' },
621+
multi: true,
622+
},
623+
});
624+
625+
expect(request.options?.where).toEqual({ status: 'archived' });
563626
expect(request.options?.multi).toBe(true);
564627
});
565628
});
@@ -574,7 +637,7 @@ describe('DataEngineCountRequestSchema', () => {
574637
expect(request.method).toBe('count');
575638
});
576639

577-
it('should accept count with filter', () => {
640+
it('should accept count with where (preferred)', () => {
578641
const request = DataEngineCountRequestSchema.parse({
579642
method: 'count',
580643
object: 'account',
@@ -585,6 +648,18 @@ describe('DataEngineCountRequestSchema', () => {
585648

586649
expect(request.query?.where).toBeDefined();
587650
});
651+
652+
it('should accept count with legacy filter (backward compat)', () => {
653+
const request = DataEngineCountRequestSchema.parse({
654+
method: 'count',
655+
object: 'account',
656+
query: {
657+
filter: { status: 'active' },
658+
},
659+
});
660+
661+
expect(request.query?.filter).toEqual({ status: 'active' });
662+
});
588663
});
589664

590665
describe('DataEngineAggregateRequestSchema', () => {

packages/spec/src/data/data-engine.zod.ts

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -320,16 +320,47 @@ export const DataEngineContractSchema = z.object({
320320
* separate microservice or plugin.
321321
*/
322322

323+
/**
324+
* RPC backward-compatibility mixin — shared `@deprecated filter` field.
325+
* When both `filter` and `where` are present, the protocol/engine ignores
326+
* `filter` in favor of `where`; only one should be provided.
327+
*/
328+
const RpcLegacyFilterMixin = {
329+
/** @deprecated Use `where` */
330+
filter: DataEngineFilterSchema.optional(),
331+
};
332+
333+
/**
334+
* RPC query options that accept BOTH new (where/fields/orderBy) and
335+
* legacy (filter/select/sort/skip/populate) parameter names.
336+
*
337+
* **Precedence:** When both legacy and new keys are present for the same
338+
* concern, the protocol normalizer uses the new key (`where` > `filter`,
339+
* `fields` > `select`, `orderBy` > `sort`, `offset` > `skip`,
340+
* `expand` > `populate`). Callers should not mix vocabularies.
341+
*/
342+
const RpcQueryOptionsSchema = EngineQueryOptionsSchema.extend({
343+
...RpcLegacyFilterMixin,
344+
/** @deprecated Use `fields` */
345+
select: z.array(z.string()).optional(),
346+
/** @deprecated Use `orderBy` */
347+
sort: DataEngineSortSchema.optional(),
348+
/** @deprecated Use `offset` */
349+
skip: z.number().int().min(0).optional(),
350+
/** @deprecated Use `expand` */
351+
populate: z.array(z.string()).optional(),
352+
});
353+
323354
export const DataEngineFindRequestSchema = z.object({
324355
method: z.literal('find'),
325356
object: z.string(),
326-
query: EngineQueryOptionsSchema.optional()
357+
query: RpcQueryOptionsSchema.optional()
327358
});
328359

329360
export const DataEngineFindOneRequestSchema = z.object({
330361
method: z.literal('findOne'),
331362
object: z.string(),
332-
query: EngineQueryOptionsSchema.optional()
363+
query: RpcQueryOptionsSchema.optional()
333364
});
334365

335366
export const DataEngineInsertRequestSchema = z.object({
@@ -344,26 +375,26 @@ export const DataEngineUpdateRequestSchema = z.object({
344375
object: z.string(),
345376
data: z.record(z.string(), z.unknown()),
346377
id: z.union([z.string(), z.number()]).optional().describe('ID for single update, or use where in options'),
347-
options: EngineUpdateOptionsSchema.optional()
378+
options: EngineUpdateOptionsSchema.extend(RpcLegacyFilterMixin).optional()
348379
});
349380

350381
export const DataEngineDeleteRequestSchema = z.object({
351382
method: z.literal('delete'),
352383
object: z.string(),
353384
id: z.union([z.string(), z.number()]).optional().describe('ID for single delete, or use where in options'),
354-
options: EngineDeleteOptionsSchema.optional()
385+
options: EngineDeleteOptionsSchema.extend(RpcLegacyFilterMixin).optional()
355386
});
356387

357388
export const DataEngineCountRequestSchema = z.object({
358389
method: z.literal('count'),
359390
object: z.string(),
360-
query: EngineCountOptionsSchema.optional()
391+
query: EngineCountOptionsSchema.extend(RpcLegacyFilterMixin).optional()
361392
});
362393

363394
export const DataEngineAggregateRequestSchema = z.object({
364395
method: z.literal('aggregate'),
365396
object: z.string(),
366-
query: EngineAggregateOptionsSchema
397+
query: EngineAggregateOptionsSchema.extend(RpcLegacyFilterMixin)
367398
});
368399

369400
/**

0 commit comments

Comments
 (0)