Skip to content

Commit 45f0f7e

Browse files
Copilothotlong
andcommitted
fix: preserve seed data _id as stable record id across page refreshes
Fixes: record not found when refreshing detail page Root cause: syncDriverIds() unconditionally set _id = id, overwriting seed data's stable _id values with driver-generated timestamp-based ids. On page refresh, the in-memory kernel reboots with new generated ids, making old URL record ids invalid. Changes: - syncDriverIds now promotes seed _id to canonical id when present - findOne $expand path handles array results defensively - Added tests verifying stable seed ids and HTTP fetch by _id Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent ecb1d00 commit 45f0f7e

3 files changed

Lines changed: 45 additions & 3 deletions

File tree

apps/console/src/__tests__/MSWServer.test.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,33 @@ describe('MSW Server Integration', () => {
9595
expect(record.priority).toBe('high');
9696
}
9797
});
98+
99+
// ── Stable seed-data IDs ──────────────────────────────────────────────
100+
// Seed records carry an explicit `_id`. After kernel bootstrap and
101+
// syncDriverIds(), `id` should equal the seed `_id`, NOT a random
102+
// driver-generated value. This ensures URLs with record IDs remain
103+
// valid across page refreshes.
104+
105+
it('should preserve seed _id as canonical id (stable across refreshes)', async () => {
106+
const driver = getDriver();
107+
const opportunities = await driver!.find('opportunity', { object: 'opportunity' });
108+
expect(opportunities.length).toBeGreaterThan(0);
109+
110+
// Seed data defines _id "101" for the first opportunity.
111+
// After syncDriverIds, id must equal _id (both "101").
112+
const first = opportunities.find((r: any) => r._id === '101');
113+
expect(first).toBeDefined();
114+
expect(first.id).toBe('101');
115+
expect(first._id).toBe('101');
116+
});
117+
118+
it('should fetch a seed record by _id via HTTP', async () => {
119+
// GET /data/opportunity/101 — uses the stable seed _id
120+
const res = await fetch('http://localhost/api/v1/data/opportunity/101');
121+
expect(res.ok).toBe(true);
122+
const body = await res.json();
123+
const record = body.data?.record ?? body.record;
124+
expect(record).toBeDefined();
125+
expect(record.name).toBe('ObjectStack Enterprise License');
126+
});
98127
});

apps/console/src/mocks/createKernel.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,14 +102,23 @@ async function installBrokerShim(kernel: ObjectKernel): Promise<void> {
102102
* generated identity as `id`. Seed data may also carry its own
103103
* `_id` that differs from the driver-assigned `id`.
104104
*
105-
* This helper ensures every record has `_id === id` so that
106-
* protocol lookups via `_id` match the driver-assigned `id`.
105+
* When seed data provides an explicit `_id`, that value is promoted
106+
* to `id` so that record identifiers remain stable across page
107+
* refreshes (the driver would otherwise generate a new timestamp-based
108+
* `id` every time the in-memory kernel reboots).
109+
*
110+
* When no explicit `_id` exists, `_id` is derived from the
111+
* driver-assigned `id` so protocol lookups still work.
107112
*/
108113
function syncDriverIds(driver: InMemoryDriver): void {
109114
const db = (driver as any).db as Record<string, any[]>;
110115
for (const records of Object.values(db)) {
111116
for (const record of records) {
112-
if (record.id) {
117+
if (record._id != null && record._id !== record.id) {
118+
// Seed data carries an explicit _id → promote it to canonical id
119+
record.id = record._id;
120+
} else if (record.id) {
121+
// No explicit seed _id → derive _id from driver-assigned id
113122
record._id = record.id;
114123
}
115124
}

packages/data-objectstack/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,10 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
289289
$top: 1,
290290
};
291291
const result = await this.rawFindWithPopulate(resource, findParams);
292+
// Handle array responses (some servers return data as flat arrays)
293+
if (Array.isArray(result)) {
294+
return result[0] || null;
295+
}
292296
const resultObj = result as { records?: T[]; value?: T[] };
293297
const records = resultObj.records || resultObj.value || [];
294298
return records[0] || null;

0 commit comments

Comments
 (0)