Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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: 1 addition & 1 deletion packages/cli/src/commands/explain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ const SCHEMAS: Record<string, SchemaInfo> = {
object: 'project_task',
fields: ['title', 'status', 'assigned_to'],
filters: [{ field: 'status', operator: 'eq', value: 'open' }],
sort: [{ field: 'created_at', direction: 'desc' }],
sort: [{ field: 'created_at', order: 'desc' }],
limit: 50,
}`,
related: ['object', 'field', 'view'],
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ const ${toCamelCase(name)}ListView: UI.View = {
columns: [
{ field: 'name', width: 200 },
],
defaultSort: { field: 'name', direction: 'asc' },
sort: [{ field: 'name', order: 'asc' }],
pageSize: 25,
},
};
Expand Down
19 changes: 17 additions & 2 deletions packages/plugins/plugin-reports/src/report-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,15 @@ function makeFakeEngine() {
async find(object: string, options?: any) {
const rows = ensure(object).filter(r => matches(r, options?.filter ?? options?.where));
if (options?.orderBy?.[0]) {
const { field, direction } = options.orderBy[0];
// Canonical SortNode key only (spec/data/query.zod.ts): the real
// engine strips an unknown `direction:` key and defaults to asc, so
// the mock must too — honoring both keys masks wrong-key sorts.
const { field, order } = options.orderBy[0];
rows.sort((a, b) => {
const av = a[field]; const bv = b[field];
if (av === bv) return 0;
const cmp = av > bv ? 1 : -1;
return direction === 'desc' ? -cmp : cmp;
return order === 'desc' ? -cmp : cmp;
});
}
return rows.slice(0, options?.limit ?? 1000);
Expand Down Expand Up @@ -163,6 +166,18 @@ describe('ReportService', () => {
expect(leads[0].name).toBe('A');
});

it('listReports: most recently updated first', async () => {
// Regression: the query sorted with the non-canonical `direction: 'desc'`
// key, which SortNode strips — so it sorted ascending (oldest first).
engine._tables['sys_saved_report'] = [
{ id: 'r_old', name: 'Old', object_name: 'lead', query_json: '{}', updated_at: '2026-01-01T00:00:00Z' },
{ id: 'r_new', name: 'New', object_name: 'lead', query_json: '{}', updated_at: '2026-03-01T00:00:00Z' },
{ id: 'r_mid', name: 'Mid', object_name: 'lead', query_json: '{}', updated_at: '2026-02-01T00:00:00Z' },
];
const rows = await svc.listReports({ object: 'lead' }, CTX);
expect(rows.map(r => r.id)).toEqual(['r_new', 'r_mid', 'r_old']);
});

it('getReport: returns null on miss', async () => {
expect(await svc.getReport('nope', CTX)).toBeNull();
});
Expand Down
4 changes: 2 additions & 2 deletions packages/plugins/plugin-reports/src/report-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ export class ReportService implements IReportService {
if (filter?.object) f.object_name = filter.object;
if (filter?.ownerId) f.owner_id = filter.ownerId;
const rows = await this.engine.find('sys_saved_report', {
filter: f, limit: 500, orderBy: [{ field: 'updated_at', direction: 'desc' }], context: SYSTEM_CTX,
filter: f, limit: 500, orderBy: [{ field: 'updated_at', order: 'desc' }], context: SYSTEM_CTX,
});
return Array.isArray(rows) ? rows.map(rowFromSaved) : [];
}
Expand Down Expand Up @@ -376,7 +376,7 @@ export class ReportService implements IReportService {
const f: any = {};
if (filter?.reportId) f.report_id = filter.reportId;
const rows = await this.engine.find('sys_report_schedule', {
filter: f, limit: 500, orderBy: [{ field: 'next_run_at', direction: 'asc' }], context: SYSTEM_CTX,
filter: f, limit: 500, orderBy: [{ field: 'next_run_at', order: 'asc' }], context: SYSTEM_CTX,
});
return Array.isArray(rows) ? rows.map(rowFromSchedule) : [];
}
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/plugin-sharing/src/share-link-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ export function registerShareLinkRoutes(
const SYSTEM_CTX = { isSystem: true, roles: [], permissions: [] } as const;
const rows = await engine.find('ai_messages', {
where: { conversation_id: resolved.link.record_id },
sort: [{ field: 'created_at', direction: 'asc' }],
sort: [{ field: 'created_at', order: 'asc' }],
limit: 500,
context: SYSTEM_CTX,
} as any);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ export class SharingRuleService implements ISharingRuleService {
if (orgId) where.organization_id = orgId;
const rows = await this.engine.find('sys_sharing_rule', {
filter: where,
orderBy: [{ field: 'name', direction: 'asc' }],
orderBy: [{ field: 'name', order: 'asc' }],
limit: 1000,
context: SYSTEM_CTX,
});
Expand Down
26 changes: 25 additions & 1 deletion packages/plugins/plugin-sharing/src/sharing-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,20 @@ function makeFakeEngine(schemas: Record<string, any>) {
async find(object: string, options?: any) {
const table = ensure(object);
const filter = options?.filter ?? options?.where;
return table.filter(r => matches(r, filter)).slice(0, options?.limit ?? 1000);
let out = table.filter(r => matches(r, filter));
if (options?.orderBy?.[0]) {
// Canonical SortNode key only (spec/data/query.zod.ts): the real
// engine strips an unknown `direction:` key and defaults to asc, so
// the mock must too — honoring both keys masks wrong-key sorts.
const { field, order } = options.orderBy[0];
out = [...out].sort((a, b) => {
const av = a[field]; const bv = b[field];
if (av === bv) return 0;
const cmp = av > bv ? 1 : -1;
return order === 'desc' ? -cmp : cmp;
});
}
return out.slice(0, options?.limit ?? 1000);
},
async insert(object: string, data: any) {
const row = { ...data };
Expand Down Expand Up @@ -236,6 +249,17 @@ describe('SharingService.grant / listShares / revoke', () => {
expect(rows.length).toBe(2);
});

it('listShares returns the newest grant first', async () => {
// Regression: the query sorted with the non-canonical `direction: 'desc'`
// key, which SortNode strips — so it sorted ascending (oldest first).
engine._tables.sys_record_share = [
{ id: 'shr_old', object_name: 'account', record_id: 'a1', recipient_id: 'bob', created_at: '2026-01-01T00:00:00Z' },
{ id: 'shr_new', object_name: 'account', record_id: 'a1', recipient_id: 'carol', created_at: '2026-02-01T00:00:00Z' },
];
const rows = await svc.listShares('account', 'a1', { userId: 'admin' });
expect(rows.map(r => r.id)).toEqual(['shr_new', 'shr_old']);
});

it('revoke removes the row', async () => {
const r = await svc.grant({ object: 'account', recordId: 'a1', recipientId: 'bob' }, { userId: 'admin' });
await svc.revoke(r.id, { userId: 'admin' });
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/plugin-sharing/src/sharing-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ export class SharingService implements ISharingService {
): Promise<RecordShare[]> {
const rows = await this.engine.find('sys_record_share', {
filter: { object_name: object, record_id: recordId },
orderBy: [{ field: 'created_at', direction: 'desc' }],
orderBy: [{ field: 'created_at', order: 'desc' }],
limit: 500,
context: SYSTEM_CTX,
});
Expand Down
21 changes: 18 additions & 3 deletions packages/services/service-job/src/db-job-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@ function makeFakeEngine() {
? t.filter((r) => Object.entries(opts.where).every(([k, v]) => r[k] === v))
: [...t];
if (opts.orderBy) {
for (const order of [...opts.orderBy].reverse()) {
for (const ord of [...opts.orderBy].reverse()) {
// Canonical SortNode key only (spec/data/query.zod.ts): the real
// engine strips an unknown `direction:` key and defaults to asc,
// so the mock must too — honoring both keys masks wrong-key sorts.
out.sort((a, b) => {
const av = a[order.field], bv = b[order.field];
const av = a[ord.field], bv = b[ord.field];
if (av === bv) return 0;
const cmp = av > bv ? 1 : -1;
return order.direction === 'desc' ? -cmp : cmp;
return ord.order === 'desc' ? -cmp : cmp;
});
}
}
Expand Down Expand Up @@ -118,6 +121,18 @@ describe('DbJobAdapter', () => {
expect(ok[0].jobId).toBe('mix');
});

it('listExecutionsByStatus returns the newest run first', async () => {
// Regression: the query sorted with the non-canonical `direction: 'desc'`
// key, which SortNode strips — so "latest run" returned the OLDEST run.
engine.tables.set('sys_job_run', [
{ id: '1', job_name: 'first', status: 'success', started_at: '2026-01-01T00:00:00Z' },
{ id: '2', job_name: 'third', status: 'success', started_at: '2026-03-01T00:00:00Z' },
{ id: '3', job_name: 'second', status: 'success', started_at: '2026-02-01T00:00:00Z' },
]);
const runs = await adapter.listExecutionsByStatus('success');
expect(runs.map((r) => r.jobId)).toEqual(['third', 'second', 'first']);
});

it('replay tags a synthetic run as replay trigger', async () => {
await adapter.schedule('rj', { type: 'cron', expression: '* * * * *' }, async () => {});
await adapter.replay('rj');
Expand Down
2 changes: 1 addition & 1 deletion packages/services/service-job/src/db-job-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export class DbJobAdapter implements IJobService {
const rows = await this.engine.find(RUN_TABLE, {
where: { status },
limit: limit ?? 50,
orderBy: [{ field: 'started_at', direction: 'desc' }],
orderBy: [{ field: 'started_at', order: 'desc' }],
context: SYSTEM_CTX,
});
return (rows ?? []).map((r: any) => ({
Expand Down
20 changes: 17 additions & 3 deletions packages/services/service-queue/src/db-queue-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,15 @@ function makeFakeEngine() {
const t = tables.get(table) ?? [];
let out = opts.where ? t.filter((r) => matches(r, opts.where)) : [...t];
if (opts.orderBy) {
for (const order of [...opts.orderBy].reverse()) {
for (const ord of [...opts.orderBy].reverse()) {
// Canonical SortNode key only (spec/data/query.zod.ts): the real
// engine strips an unknown `direction:` key and defaults to asc,
// so the mock must too — honoring both keys masks wrong-key sorts.
out.sort((a, b) => {
const av = a[order.field], bv = b[order.field];
const av = a[ord.field], bv = b[ord.field];
if (av === bv) return 0;
const cmp = av > bv ? 1 : -1;
return order.direction === 'desc' ? -cmp : cmp;
return ord.order === 'desc' ? -cmp : cmp;
});
}
}
Expand Down Expand Up @@ -164,6 +167,17 @@ describe('DbQueueAdapter', () => {
expect(failed[0].lastError).toContain('x');
});

it('listFailed returns the newest message first', async () => {
// Regression: the query sorted with the non-canonical `direction: 'desc'`
// key, which SortNode strips — so it sorted ascending (oldest first).
engine.tables.set('sys_job_queue', [
{ id: 'm_old', queue: 'q', status: 'dlq', payload_json: '{}', created_at: '2026-01-01T00:00:00Z' },
{ id: 'm_new', queue: 'q', status: 'dlq', payload_json: '{}', created_at: '2026-02-01T00:00:00Z' },
]);
const failed = await adapter.listFailed('q');
expect(failed.map((f) => f.id)).toEqual(['m_new', 'm_old']);
});

it('replay resets dlq message back to pending and re-processes', async () => {
let attempts = 0;
await adapter.subscribe('replay-q', async () => {
Expand Down
6 changes: 3 additions & 3 deletions packages/services/service-queue/src/db-queue-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ export class DbQueueAdapter implements IQueueService {
where,
limit: options?.limit ?? 100,
offset: options?.offset,
orderBy: [{ field: 'created_at', direction: 'desc' }],
orderBy: [{ field: 'created_at', order: 'desc' }],
context: SYSTEM_CTX,
});
return (rows ?? []).map((r: any) => this.rowToRecord(r));
Expand Down Expand Up @@ -265,8 +265,8 @@ export class DbQueueAdapter implements IQueueService {
where: { queue, status: 'pending' },
limit: max * 3, // over-fetch in case of CAS contention
orderBy: [
{ field: 'priority', direction: 'asc' },
{ field: 'scheduled_for', direction: 'asc' },
{ field: 'priority', order: 'asc' },
{ field: 'scheduled_for', order: 'asc' },
],
context: SYSTEM_CTX,
});
Expand Down
4 changes: 2 additions & 2 deletions packages/spec/src/benchmark.bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ describe('Data Domain — Schema Parse Performance', () => {
object: 'benchmark_object',
fields: ['name', 'status', 'priority'],
filters: [['status', '=', 'active'], 'and', ['priority', '>', 0]],
sort: [{ field: 'priority', direction: 'desc' }],
orderBy: [{ field: 'priority', order: 'desc' }],
top: 25,
skip: 0,
};
Expand Down Expand Up @@ -82,7 +82,7 @@ describe('UI Domain — Schema Parse Performance', () => {
{ field: 'name', width: 200 },
{ field: 'status', width: 100 },
],
defaultSort: { field: 'name', direction: 'asc' },
sort: [{ field: 'name', order: 'asc' }],
pageSize: 25,
},
};
Expand Down
Loading