Skip to content

Commit 92fc058

Browse files
Copilothotlong
andcommitted
fix: address code review - recursive isFilterAST validation, type safety, guard against array entries
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent ee6d064 commit 92fc058

5 files changed

Lines changed: 28 additions & 6 deletions

File tree

ROADMAP.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ This strategy ensures rapid iteration while maintaining a clear path to producti
110110
| `defineStack()` strict by default || `strict: true` default since v3.0.2, validates schemas + cross-references |
111111
| `z.any()` elimination in UI protocol || All `filter` fields → `FilterConditionSchema` or `ViewFilterRuleSchema`, all `value` fields → typed unions |
112112
| Filter format unification || MongoDB-style filters use `FilterConditionSchema`, declarative view/tab filters use `ViewFilterRuleSchema``z.array(z.unknown())` eliminated |
113+
| Filter parameter naming (`filter` vs `filters`) || Canonical HTTP param: `filter` (singular). `filters` accepted for backward compat. `HttpFindQueryParamsSchema` added. Client SDK + protocol.ts unified. |
114+
| `isFilterAST()` structural validation || Exported from `data/filter.zod.ts` — validates AST shape (comparison/logical/legacy) instead of naïve `Array.isArray`. `VALID_AST_OPERATORS` constant. |
115+
| GET by ID parameter pollution prevention || `GetDataRequestSchema` allowlists `select`/`expand`. Dispatcher whitelists only safe params. |
116+
| Dispatcher response field mapping || `handleApiEndpoint` uses spec-correct `result.records`/`result.total` instead of `result.data`/`result.count` |
113117
| Seed data → object cross-reference || `validateCrossReferences` detects seed data referencing undefined objects |
114118
| Navigation → object/dashboard/page/report cross-reference || App navigation items validated against defined metadata (recursive group support) |
115119
| Negative validation tests (dashboard, page, report, view) || Missing required fields, invalid enums, type violations, cross-reference errors all covered |

packages/client/src/index.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1455,10 +1455,12 @@ export class ObjectStackClient {
14551455
// Detect AST filter format vs simple key-value map. AST filters use an array structure
14561456
// with [field, operator, value] or [logicOp, ...nodes] shape (see isFilterAST from spec).
14571457
// For complex filter expressions, use .query() which builds a proper QueryAST.
1458-
if (this.isFilterAST(filterValue)) {
1458+
if (this.isFilterAST(filterValue) || Array.isArray(filterValue)) {
1459+
// AST or any array → serialize as JSON in `filter` param
14591460
queryParams.set('filter', JSON.stringify(filterValue));
1460-
} else {
1461-
Object.entries(filterValue).forEach(([k, v]) => {
1461+
} else if (typeof filterValue === 'object' && filterValue !== null) {
1462+
// Plain key-value map → append each as individual query params
1463+
Object.entries(filterValue as Record<string, unknown>).forEach(([k, v]) => {
14621464
if (v !== undefined && v !== null) {
14631465
queryParams.append(k, String(v));
14641466
}

packages/runtime/src/http-dispatcher.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,7 @@ export class HttpDispatcher {
380380
// Spec: Only select/expand are allowlisted query params for GET by ID.
381381
// All other query parameters are discarded to prevent parameter pollution.
382382
const { select, expand } = query || {};
383-
const allowedParams: Record<string, any> = {};
383+
const allowedParams: Record<string, unknown> = {};
384384
if (select != null) allowedParams.select = select;
385385
if (expand != null) allowedParams.expand = expand;
386386
// Spec: broker returns GetDataResponse = { object, id, record }

packages/spec/src/data/filter.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -984,6 +984,22 @@ describe('isFilterAST', () => {
984984
expect(isFilterAST(['and', 'not-an-array'])).toBe(false);
985985
expect(isFilterAST(['or', 123])).toBe(false);
986986
});
987+
988+
it('should recursively validate children of logical nodes', () => {
989+
// Valid: children are valid AST nodes
990+
expect(isFilterAST(['and', ['status', '=', 'active'], ['age', '>', 18]])).toBe(true);
991+
// Invalid: children are arrays but not valid AST nodes
992+
expect(isFilterAST(['and', [1, 2, 3]])).toBe(false);
993+
// Nested valid AST
994+
expect(isFilterAST(['and', ['or', ['a', '=', 1], ['b', '=', 2]], ['c', '>', 3]])).toBe(true);
995+
});
996+
997+
it('should recursively validate legacy flat array children', () => {
998+
// Valid: all children are valid comparison nodes
999+
expect(isFilterAST([['status', '=', 'active'], ['age', '>', 18]])).toBe(true);
1000+
// Invalid: children are arbitrary arrays
1001+
expect(isFilterAST([[1, 2], [3, 4]])).toBe(false);
1002+
});
9871003
});
9881004

9891005
// ============================================================================

packages/spec/src/data/filter.zod.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ export function isFilterAST(filter: unknown): boolean {
381381
if (typeof first === 'string') {
382382
const lower = first.toLowerCase();
383383
if (lower === 'and' || lower === 'or') {
384-
return filter.length >= 2 && filter.slice(1).every((child: unknown) => Array.isArray(child));
384+
return filter.length >= 2 && filter.slice(1).every((child: unknown) => isFilterAST(child));
385385
}
386386

387387
// Comparison node: [field, operator, value]
@@ -391,7 +391,7 @@ export function isFilterAST(filter: unknown): boolean {
391391
}
392392

393393
// Legacy flat array: [[cond], [cond], ...]
394-
if (filter.every((item: unknown) => Array.isArray(item))) {
394+
if (filter.every((item: unknown) => isFilterAST(item))) {
395395
return filter.length > 0;
396396
}
397397

0 commit comments

Comments
 (0)