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
31 changes: 31 additions & 0 deletions apps/console/src/__tests__/MSWServer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,35 @@ describe('MSW Server Integration', () => {
expect(record.priority).toBe('high');
}
});

// ── Stable seed-data IDs ──────────────────────────────────────────────
// Seed records carry an explicit `_id`. After kernel bootstrap and
// syncDriverIds(), `id` should equal the seed `_id`, NOT a random
// driver-generated value. This ensures URLs with record IDs remain
// valid across page refreshes.

it('should preserve seed _id as canonical id (stable across refreshes)', async () => {
const driver = getDriver();
const opportunities = await driver!.find('opportunity', { object: 'opportunity' });
expect(opportunities.length).toBeGreaterThan(0);

// Seed data defines _id "101" for the first opportunity.
// After syncDriverIds, id must equal _id (both "101").
const targetOpportunity = opportunities.find((r: any) => r._id === '101');
expect(targetOpportunity).toBeDefined();
expect(targetOpportunity.id).toBe('101');
expect(targetOpportunity._id).toBe('101');
});

it('should fetch a seed record by _id via HTTP', async () => {
// GET /data/opportunity/101 — uses the stable seed _id.
// Response may be wrapped in { success, data: { record } } (HttpDispatcher)
// or returned as { record } (direct protocol).
const res = await fetch('http://localhost/api/v1/data/opportunity/101');
expect(res.ok).toBe(true);
const body = await res.json();
const record = body.data?.record ?? body.record;
expect(record).toBeDefined();
expect(record.name).toBe('ObjectStack Enterprise License');
});
});
15 changes: 12 additions & 3 deletions apps/console/src/mocks/createKernel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,14 +102,23 @@ async function installBrokerShim(kernel: ObjectKernel): Promise<void> {
* generated identity as `id`. Seed data may also carry its own
* `_id` that differs from the driver-assigned `id`.
*
* This helper ensures every record has `_id === id` so that
* protocol lookups via `_id` match the driver-assigned `id`.
* When seed data provides an explicit `_id`, that value is promoted
* to `id` so that record identifiers remain stable across page
* refreshes (the driver would otherwise generate a new timestamp-based
* `id` every time the in-memory kernel reboots).
*
* When no explicit `_id` exists, `_id` is derived from the
* driver-assigned `id` so protocol lookups still work.
*/
function syncDriverIds(driver: InMemoryDriver): void {
const db = (driver as any).db as Record<string, any[]>;
for (const records of Object.values(db)) {
for (const record of records) {
if (record.id) {
if (record._id != null && record._id !== record.id) {
// Seed data carries an explicit _id → promote it to canonical id
record.id = record._id;
} else if (record.id) {
// No explicit seed _id → derive _id from driver-assigned id
record._id = record.id;
}
}
Expand Down
4 changes: 4 additions & 0 deletions packages/data-objectstack/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,10 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
$top: 1,
};
const result = await this.rawFindWithPopulate(resource, findParams);
// Handle array responses (some servers return data as flat arrays)
if (Array.isArray(result)) {
return result[0] || null;
}
const resultObj = result as { records?: T[]; value?: T[] };
const records = resultObj.records || resultObj.value || [];
Comment on lines 296 to 297
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new array-response handling in the findOne() $expand path isn’t covered by existing adapter tests. Please add/extend a unit test (e.g. in src/expand.test.ts) where the raw fetch returns an unwrapped array (or { success: true, data: [...] }) to ensure findOne() returns the first record instead of null.

Suggested change
const resultObj = result as { records?: T[]; value?: T[] };
const records = resultObj.records || resultObj.value || [];
const resultObj = result as { records?: T[]; value?: T[]; data?: T[] };
const records = resultObj.records || resultObj.value || resultObj.data || [];

Copilot uses AI. Check for mistakes.
return records[0] || null;
Expand Down