Skip to content

Commit 40b8329

Browse files
authored
Merge pull request #1064 from objectstack-ai/copilot/unify-primary-key-to-id
2 parents 5926fd6 + b66d1db commit 40b8329

File tree

55 files changed

+332
-341
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+332
-341
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ const month = String(now.getMonth() + 1).padStart(2, '0');
3636

3737
const mockDataSource = {
3838
find: vi.fn().mockResolvedValue([
39-
{ _id: 'e1', subject: 'Weekly Standup', start: `${year}-${month}-05T09:00:00`, end: `${year}-${month}-05T10:00:00`, type: 'Meeting' },
40-
{ _id: 'e2', subject: 'Client Call', start: `${year}-${month}-06T14:00:00`, end: `${year}-${month}-06T15:00:00`, type: 'Call' }
39+
{ id: 'e1', subject: 'Weekly Standup', start: `${year}-${month}-05T09:00:00`, end: `${year}-${month}-05T10:00:00`, type: 'Meeting' },
40+
{ id: 'e2', subject: 'Client Call', start: `${year}-${month}-06T14:00:00`, end: `${year}-${month}-06T15:00:00`, type: 'Call' }
4141
]),
4242
findOne: vi.fn().mockResolvedValue({}),
4343
create: vi.fn().mockResolvedValue({}),

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

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -97,26 +97,24 @@ describe('MSW Server Integration', () => {
9797
});
9898

9999
// ── 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.
100+
// Seed records carry an explicit `id`. After kernel bootstrap the
101+
// canonical field is `id`; `_id` is kept only as a legacy alias.
102+
// This ensures URLs with record IDs remain valid across page refreshes.
104103

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

110-
// Seed data defines _id "101" for the first opportunity.
111-
// After syncDriverIds, id must equal _id (both "101").
112-
const targetOpportunity = opportunities.find((r: any) => r._id === '101');
109+
// Seed data defines id "101" for the first opportunity.
110+
// The canonical primary key field is `id`.
111+
const targetOpportunity = opportunities.find((r: any) => r.id === '101');
113112
expect(targetOpportunity).toBeDefined();
114113
expect(targetOpportunity.id).toBe('101');
115-
expect(targetOpportunity._id).toBe('101');
116114
});
117115

118-
it('should fetch a seed record by _id via HTTP', async () => {
119-
// GET /data/opportunity/101 — uses the stable seed _id.
116+
it('should fetch a seed record by id via HTTP', async () => {
117+
// GET /data/opportunity/101 — uses the stable seed id.
120118
// Response may be wrapped in { success, data: { record } } (HttpDispatcher)
121119
// or returned as { record } (direct protocol).
122120
const res = await fetch('http://localhost/api/v1/data/opportunity/101');

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ describe('ObjectGallery & ListView Integration', () => {
3636
});
3737

3838
const mockData = [
39-
{ _id: '1', name: 'Product A', category: 'Tech', image: 'http://img/a.jpg' },
40-
{ _id: '2', name: 'Product B', category: 'Home', image: 'http://img/b.jpg' },
39+
{ id: '1', name: 'Product A', category: 'Tech', image: 'http://img/a.jpg' },
40+
{ id: '2', name: 'Product B', category: 'Home', image: 'http://img/b.jpg' },
4141
];
4242

4343
const mockDataSource = {

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ vi.mock('@object-ui/plugin-list', () => ({
3434
{props.schema?.addRecord?.enabled && <div data-testid="schema-addRecord-enabled">addRecord</div>}
3535
{props.schema?.addRecordViaForm && <div data-testid="schema-addRecordViaForm">addRecordViaForm</div>}
3636
{props.schema?.userFilters && <div data-testid="schema-userFilters">{props.schema.userFilters.element}</div>}
37-
<button data-testid="list-row-click" onClick={() => props.onRowClick?.({ _id: 'rec-1', id: 'rec-1', name: 'Test Record' })}>Click Row</button>
37+
<button data-testid="list-row-click" onClick={() => props.onRowClick?.({ id: 'rec-1', name: 'Test Record' })}>Click Row</button>
3838
</div>
3939
);
4040
},
@@ -856,7 +856,7 @@ describe('ObjectView Component', () => {
856856

857857
const dataSourceWithFindOne = {
858858
...mockDataSource,
859-
findOne: vi.fn().mockResolvedValue({ _id: 'rec-1', id: 'rec-1', name: 'Test' }),
859+
findOne: vi.fn().mockResolvedValue({ id: 'rec-1', name: 'Test' }),
860860
};
861861

862862
render(<ObjectView dataSource={dataSourceWithFindOne} objects={objectsWithSplit} onEdit={vi.fn()} />);
@@ -883,7 +883,7 @@ describe('ObjectView Component', () => {
883883

884884
const dataSourceWithFindOne = {
885885
...mockDataSource,
886-
findOne: vi.fn().mockResolvedValue({ _id: 'rec-1', id: 'rec-1', name: 'Test' }),
886+
findOne: vi.fn().mockResolvedValue({ id: 'rec-1', name: 'Test' }),
887887
};
888888

889889
render(<ObjectView dataSource={dataSourceWithFindOne} objects={objectsWithPopover} onEdit={vi.fn()} />);
@@ -911,7 +911,7 @@ describe('ObjectView Component', () => {
911911

912912
const dataSourceWithFindOne = {
913913
...mockDataSource,
914-
findOne: vi.fn().mockResolvedValue({ _id: 'rec-1', id: 'rec-1', name: 'Test' }),
914+
findOne: vi.fn().mockResolvedValue({ id: 'rec-1', name: 'Test' }),
915915
};
916916

917917
render(<ObjectView dataSource={dataSourceWithFindOne} objects={objectsWithDrawer} onEdit={vi.fn()} />);
@@ -993,7 +993,7 @@ describe('ObjectView Component', () => {
993993
mockUseParams.mockReturnValue({ objectName: 'opportunity' });
994994
const dataSourceWithFindOne = {
995995
...mockDataSource,
996-
findOne: vi.fn().mockResolvedValue({ _id: 'rec-1', id: 'rec-1', name: 'Test' }),
996+
findOne: vi.fn().mockResolvedValue({ id: 'rec-1', name: 'Test' }),
997997
};
998998
render(<ObjectView dataSource={dataSourceWithFindOne} objects={objectsWithDrawer} onEdit={vi.fn()} />);
999999
fireEvent.click(screen.getByTestId('list-row-click'));

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*
44
* Validates that the URL-based recordId is passed through as-is to the
55
* findOne API and onEdit callback. The navigation code puts the actual
6-
* record._id into the URL, so no prefix stripping is needed.
6+
* record.id into the URL, so no prefix stripping is needed.
77
*
88
* Related: objectstack-ai/objectui — "Record not found" bug
99
*/
@@ -314,7 +314,6 @@ describe('RecordDetailView — recordId handling', () => {
314314
expect(onEdit).toHaveBeenCalledWith(
315315
expect.objectContaining({
316316
id: 'contact-1772350253615-4',
317-
_id: 'contact-1772350253615-4',
318317
}),
319318
);
320319
});
@@ -339,7 +338,6 @@ describe('RecordDetailView — recordId handling', () => {
339338
expect(onEdit).toHaveBeenCalledWith(
340339
expect.objectContaining({
341340
id: 'plain-id-12345',
342-
_id: 'plain-id-12345',
343341
}),
344342
);
345343
});

apps/console/src/components/ObjectView.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ function DrawerDetailContent({ objectDef, recordId, dataSource, onEdit }: {
208208
]
209209
}}
210210
dataSource={dataSource}
211-
onEdit={() => onEdit({ _id: recordId, id: recordId })}
211+
onEdit={() => onEdit({ id: recordId })}
212212
/>
213213
{/* Discussion panel — collapsible in drawer/overlay mode */}
214214
<div className="mt-6 border-t pt-6">
@@ -466,7 +466,7 @@ export function ObjectView({ dataSource, objects, onEdit }: any) {
466466
// Sync URL-based recordId to overlay state
467467
useEffect(() => {
468468
if (drawerRecordId && !navOverlay.isOpen) {
469-
navOverlay.open({ _id: drawerRecordId, id: drawerRecordId });
469+
navOverlay.open({ id: drawerRecordId });
470470
} else if (!drawerRecordId && navOverlay.isOpen) {
471471
navOverlay.close();
472472
}
@@ -666,7 +666,7 @@ export function ObjectView({ dataSource, objects, onEdit }: any) {
666666
] : [],
667667
onNavigate: (recordId: string | number, mode: 'view' | 'edit') => {
668668
if (mode === 'edit') {
669-
onEdit?.({ _id: recordId, id: recordId });
669+
onEdit?.({ id: recordId });
670670
} else if (mode === 'view') {
671671
if (viewId) {
672672
navigate(`../../record/${encodeURIComponent(String(recordId))}`, { relative: 'path' });
@@ -799,7 +799,7 @@ export function ObjectView({ dataSource, objects, onEdit }: any) {
799799
}
800800
>
801801
{(record: Record<string, unknown>) => {
802-
const recordId = (record._id || record.id) as string;
802+
const recordId = (record.id || record._id) as string;
803803
return (
804804
<DrawerDetailContent
805805
objectDef={objectDef}
@@ -869,7 +869,7 @@ export function ObjectView({ dataSource, objects, onEdit }: any) {
869869
className={navOverlay.mode === 'drawer' ? 'w-[90vw] sm:max-w-2xl p-0 overflow-hidden' : undefined}
870870
>
871871
{(record: Record<string, unknown>) => {
872-
const recordId = (record._id || record.id) as string;
872+
const recordId = (record.id || record._id) as string;
873873
return (
874874
<DrawerDetailContent
875875
objectDef={objectDef}

apps/console/src/components/RecordDetailView.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi
4343
const [childRelatedData, setChildRelatedData] = useState<Record<string, any[]>>({});
4444
const objectDef = objects.find((o: any) => o.name === objectName);
4545

46-
// Use the URL recordId as-is — it contains the actual record _id.
47-
// Navigation code passes `record._id || record.id` directly into the URL
46+
// Use the URL recordId as-is — it contains the actual record id.
47+
// Navigation code passes `record.id || record._id` directly into the URL
4848
// without adding any prefix, so no stripping is needed.
4949
const pureRecordId = recordId;
5050

@@ -470,7 +470,7 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi
470470
schema={detailSchema}
471471
dataSource={dataSource}
472472
onEdit={() => {
473-
onEdit({ _id: pureRecordId, id: pureRecordId });
473+
onEdit({ id: pureRecordId });
474474
}}
475475
/>
476476
</ActionProvider>

apps/console/src/mocks/createKernel.ts

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -117,31 +117,26 @@ async function installBrokerShim(kernel: ObjectKernel): Promise<void> {
117117
}
118118

119119
/**
120-
* Sync `_id` with `id` on all records in the InMemoryDriver.
120+
* Sync `_id` `id` on all records in the InMemoryDriver.
121121
*
122-
* The ObjectQL protocol uses `_id` for record identity lookups
123-
* (e.g. `filter: { _id: id }`), but InMemoryDriver stores the
124-
* generated identity as `id`. Seed data may also carry its own
125-
* `_id` that differs from the driver-assigned `id`.
122+
* The canonical primary key is `id` (per objectstack-ai/spec).
123+
* For backward compatibility with Mongo/Mingo storage layers, we
124+
* keep `_id` in sync as an alias.
126125
*
127-
* When seed data provides an explicit `_id`, that value is promoted
128-
* to `id` so that record identifiers remain stable across page
129-
* refreshes (the driver would otherwise generate a new timestamp-based
130-
* `id` every time the in-memory kernel reboots).
131-
*
132-
* When no explicit `_id` exists, `_id` is derived from the
133-
* driver-assigned `id` so protocol lookups still work.
126+
* When seed data provides an explicit `id`, `_id` is derived from it.
127+
* When legacy seed data only provides `_id`, that value is promoted
128+
* to `id` so lookups work correctly.
134129
*/
135130
function syncDriverIds(driver: InMemoryDriver): void {
136131
const db = (driver as any).db as Record<string, any[]>;
137132
for (const records of Object.values(db)) {
138133
for (const record of records) {
139-
if (record._id != null && record._id !== record.id) {
140-
// Seed data carries an explicit _id → promote it to canonical id
141-
record.id = record._id;
142-
} else if (record.id) {
143-
// No explicit seed _id → derive _id from driver-assigned id
134+
if (record.id != null && record.id !== record._id) {
135+
// Canonical id present → derive _id for backward compat
144136
record._id = record.id;
137+
} else if (record._id != null && !record.id) {
138+
// Legacy seed data with only _id → promote to canonical id
139+
record.id = record._id;
145140
}
146141
}
147142
}

examples/crm/src/data/account.data.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ export const AccountData = {
33
mode: 'upsert' as const,
44
records: [
55
{
6-
_id: "1",
6+
id: "1",
77
name: "ObjectStack HQ",
88
industry: "Technology",
99
type: "Partner",
@@ -22,7 +22,7 @@ export const AccountData = {
2222
created_at: new Date("2023-01-15")
2323
},
2424
{
25-
_id: "2",
25+
id: "2",
2626
name: "Salesforce Tower",
2727
industry: "Technology",
2828
type: "Customer",
@@ -40,7 +40,7 @@ export const AccountData = {
4040
created_at: new Date("2023-02-20")
4141
},
4242
{
43-
_id: "3",
43+
id: "3",
4444
name: "Global Financial Services",
4545
industry: "Finance",
4646
type: "Customer",
@@ -57,7 +57,7 @@ export const AccountData = {
5757
created_at: new Date("2023-03-10")
5858
},
5959
{
60-
_id: "4",
60+
id: "4",
6161
name: "London Consulting Grp",
6262
industry: "Services",
6363
type: "Partner",
@@ -73,7 +73,7 @@ export const AccountData = {
7373
created_at: new Date("2023-04-05")
7474
},
7575
{
76-
_id: "5",
76+
id: "5",
7777
name: "Tokyo E-Commerce",
7878
industry: "Retail",
7979
type: "Vendor",
@@ -89,7 +89,7 @@ export const AccountData = {
8989
created_at: new Date("2023-05-20")
9090
},
9191
{
92-
_id: "6",
92+
id: "6",
9393
name: "Berlin AutoWorks",
9494
industry: "Manufacturing",
9595
type: "Customer",
@@ -107,7 +107,7 @@ export const AccountData = {
107107
created_at: new Date("2023-06-15")
108108
},
109109
{
110-
_id: "7",
110+
id: "7",
111111
name: "Paris Fashion House",
112112
industry: "Retail",
113113
type: "Customer",

examples/crm/src/data/contact.data.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ export const ContactData = {
33
mode: 'upsert' as const,
44
records: [
55
{
6-
_id: "1",
6+
id: "1",
77
name: "Alice Johnson",
88
email: "alice@objectstack.com",
99
phone: "415-555-1001",
@@ -20,7 +20,7 @@ export const ContactData = {
2020
address: "San Francisco, CA"
2121
},
2222
{
23-
_id: "2",
23+
id: "2",
2424
name: "Bob Smith",
2525
email: "bob@salesforce.com",
2626
phone: "415-555-1002",
@@ -37,7 +37,7 @@ export const ContactData = {
3737
address: "San Francisco, CA"
3838
},
3939
{
40-
_id: "3",
40+
id: "3",
4141
name: "Charlie Brown",
4242
email: "charlie@globalfin.com",
4343
phone: "212-555-1003",
@@ -53,7 +53,7 @@ export const ContactData = {
5353
address: "New York, NY"
5454
},
5555
{
56-
_id: "4",
56+
id: "4",
5757
name: "Diana Prince",
5858
email: "diana@lcg.co.uk",
5959
phone: "+44-555-1004",
@@ -70,7 +70,7 @@ export const ContactData = {
7070
do_not_call: true
7171
},
7272
{
73-
_id: "5",
73+
id: "5",
7474
name: "Evan Wright",
7575
email: "evan@berlinauto.de",
7676
phone: "+49-555-1005",
@@ -86,7 +86,7 @@ export const ContactData = {
8686
address: "Berlin, DE"
8787
},
8888
{
89-
_id: "6",
89+
id: "6",
9090
name: "Fiona Gallagher",
9191
email: "fiona@mode.fr",
9292
phone: "+33-555-1006",
@@ -102,7 +102,7 @@ export const ContactData = {
102102
address: "Paris, FR"
103103
},
104104
{
105-
_id: "7",
105+
id: "7",
106106
name: "George Martin",
107107
email: "george@salesforce.com",
108108
phone: "415-555-1007",

0 commit comments

Comments
 (0)