Skip to content
1 change: 1 addition & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ This strategy ensures rapid iteration while maintaining a clear path to producti
| CI lint rule rejecting new `z.any()` | 🔴 | Planned — eslint or custom lint rule to block `z.any()` additions |
| Dispatcher async `getService` bug fix | ✅ | All `getService`/`getObjectQLService` calls in `http-dispatcher.ts` now properly `await` async service factories. Covers `handleAnalytics`, `handleAuth`, `handleStorage`, `handleAutomation`, `handleMetadata`, `handleUi`, `handlePackages`. All 7 framework adapters (Express, Fastify, Hono, Next.js, SvelteKit, NestJS, Nuxt) updated to use `getServiceAsync()` for auth service resolution. |
| Analytics `getMetadata` → `getMeta` naming fix | ✅ | `handleAnalytics` in `http-dispatcher.ts` called `getMetadata({ request })` which didn't match the `IAnalyticsService` contract (`getMeta(cubeName?: string)`). Renamed to `getMeta()` and aligned call signature. Updated test mocks accordingly. |
| Unified ID/audit/tenant field naming | ✅ | Eliminated `_id`/`modified_at`/`modified_by`/`space_id` from protocol layer. All protocol code uses `id`, `updated_at`, `updated_by`, `tenant_id` per `SystemFieldName`. Storage-layer (NoSQL driver internals) retains `_id` for MongoDB/Mingo compat. |

---

Expand Down
9 changes: 4 additions & 5 deletions apps/studio/src/components/ObjectDataForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ export function ObjectDataForm({ objectApiName, record, onSuccess, onCancel }: O

try {
const dataToSubmit = { ...formData };
delete dataToSubmit._id;
delete dataToSubmit.id;
delete dataToSubmit.created_at;
delete dataToSubmit.updated_at;
Expand All @@ -71,8 +70,8 @@ export function ObjectDataForm({ objectApiName, record, onSuccess, onCancel }: O
});
}

if (record && (record.id || record._id)) {
await client.data.update(objectApiName, record.id || record._id, dataToSubmit);
if (record && record.id) {
await client.data.update(objectApiName, record.id, dataToSubmit);
} else {
await client.data.create(objectApiName, dataToSubmit);
}
Expand Down Expand Up @@ -109,10 +108,10 @@ export function ObjectDataForm({ objectApiName, record, onSuccess, onCancel }: O

const fields = def.fields || {};
const fieldKeys = Object.keys(fields).filter(k => {
return !['created_at', 'updated_at', 'created_by', 'modified_by'].includes(k);
return !['created_at', 'updated_at', 'created_by', 'updated_by'].includes(k);
});

const isEdit = !!(record && (record.id || record._id));
const isEdit = !!(record && record.id);

return (
<Dialog open={true} onOpenChange={(open) => !open && onCancel()}>
Expand Down
4 changes: 2 additions & 2 deletions apps/studio/src/components/ObjectDataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ export function ObjectDataTable({ objectApiName, onEdit }: ObjectDataTableProps)
{loading && records.length === 0 ? (
<TableSkeleton cols={columns.length} />
) : filteredRecords.map(record => (
<TableRow key={record.id || record._id} className="group">
<TableRow key={record.id} className="group">
{columns.map(col => (
<TableCell key={col.name} className="py-2.5">
<CellValue value={record[col.name]} type={col.type} />
Expand All @@ -302,7 +302,7 @@ export function ObjectDataTable({ objectApiName, onEdit }: ObjectDataTableProps)
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => handleDelete(record.id || record._id)}
onClick={() => handleDelete(record.id)}
className="text-destructive focus:text-destructive"
>
<Trash2 className="mr-2 h-4 w-4" />
Expand Down
8 changes: 4 additions & 4 deletions apps/studio/src/mocks/createKernel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export async function createKernel(options: KernelOptions) {
if (method === 'create') {
const res = await ql.insert(params.object, params.data);
const record = { ...params.data, ...res };
return { object: params.object, id: record.id || record._id, record };
return { object: params.object, id: record.id, record };
}
if (method === 'get') {
// Delegate to protocol for proper expand/select support
Expand All @@ -74,7 +74,7 @@ export async function createKernel(options: KernelOptions) {
}
let all = await ql.find(params.object);
if (!all) all = [];
const match = all.find((i: any) => i.id === params.id || i._id === params.id);
const match = all.find((i: any) => i.id === params.id);
return match ? { object: params.object, id: params.id, record: match } : null;
}
if (method === 'update') {
Expand All @@ -85,7 +85,7 @@ export async function createKernel(options: KernelOptions) {
if (all && (all as any).value) all = (all as any).value;
if (!all) all = [];

const existing = all.find((i: any) => i.id === params.id || i._id === params.id);
const existing = all.find((i: any) => i.id === params.id);

if (!existing) {
console.warn(`[BrokerShim] Update failed: Record ${params.id} not found.`);
Expand Down Expand Up @@ -183,7 +183,7 @@ export async function createKernel(options: KernelOptions) {
: String(queryOptions.select).split(',').map((s: string) => s.trim());

all = all.map((item: any) => {
const projected: any = { id: item.id, _id: item._id }; // Always include ID
const projected: any = { id: item.id }; // Always include ID
selectFields.forEach((f: string) => {
if (item[f] !== undefined) projected[f] = item[f];
});
Expand Down
3 changes: 1 addition & 2 deletions apps/studio/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
* Task type definition (todo_task)
*/
export interface Task {
_id: string; // Internal ID
id: string; // External ID usually, but here we might get _id or id depending on driver
id: string;
subject: string;
priority: number;
is_completed: boolean;
Expand Down
2 changes: 1 addition & 1 deletion content/docs/guides/standards.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,7 @@ describe('Account Object', () => {
- [ ] All objects use `ObjectSchema.create()`
- [ ] Field names are `snake_case`
- [ ] Config keys are `camelCase`
- [ ] Lookups don't have `_id` suffix
- [ ] Lookups don't have `_id` suffix (use `id`)
- [ ] All validations have clear messages
- [ ] Documentation is up to date
- [ ] Tests pass
Expand Down
2 changes: 1 addition & 1 deletion content/docs/protocol/objectql/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ const query: Query = {
-- PostgreSQL (with JOIN)
SELECT c.company_name, c.industry, u.name AS "owner.name"
FROM customer c
LEFT JOIN user u ON c.owner_id = u._id
LEFT JOIN user u ON c.owner_id = u.id
WHERE c.industry = 'tech' AND c.annual_revenue > 1000000
ORDER BY c.created_at DESC
LIMIT 10;
Expand Down
18 changes: 9 additions & 9 deletions content/docs/protocol/objectql/query-syntax.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,7 @@ SELECT
o.amount,
a.company_name AS "account.company_name"
FROM opportunity o
LEFT JOIN account a ON o.account_id = a._id
LEFT JOIN account a ON o.account_id = a.id
```

### Multiple Relationships
Expand Down Expand Up @@ -594,7 +594,7 @@ const query = {
['exists', {
object: 'opportunity',
filters: [
['account_id', '=', '{{parent._id}}'],
['account_id', '=', '{{parent.id}}'],
['stage', '=', 'Closed Won']
]
}]
Expand All @@ -603,7 +603,7 @@ const query = {

// SQL: WHERE EXISTS (
// SELECT 1 FROM opportunity
// WHERE opportunity.account_id = account._id
// WHERE opportunity.account_id = account.id
// AND opportunity.stage = 'Closed Won'
// )
```
Expand All @@ -617,7 +617,7 @@ const query = {
['not_exists', {
object: 'opportunity',
filters: [
['account_id', '=', '{{parent._id}}']
['account_id', '=', '{{parent.id}}']
]
}]
]
Expand Down Expand Up @@ -778,17 +778,17 @@ const page2 = await ObjectQL.query({
const result = await ObjectQL.query({
object: 'customer',
limit: 10,
sort: [{ field: '_id', order: 'asc' }]
sort: [{ field: 'id', order: 'asc' }]
});

// Next page (use last _id as cursor)
// Next page (use last id as cursor)
const nextResult = await ObjectQL.query({
object: 'customer',
filters: [
['_id', '>', result[result.length - 1]._id]
['id', '>', result[result.length - 1].id]
],
limit: 10,
sort: [{ field: '_id', order: 'asc' }]
sort: [{ field: 'id', order: 'asc' }]
});
```

Expand All @@ -808,7 +808,7 @@ const customer = await ObjectQL.findOne('customer', '123');
// Equivalent to:
await ObjectQL.query({
object: 'customer',
filters: [['_id', '=', '123']],
filters: [['id', '=', '123']],
limit: 1
});
```
Expand Down
6 changes: 3 additions & 3 deletions content/docs/protocol/objectql/schema.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ label: Project
```

This 2-line definition creates:
- Database table `project` with system fields (`_id`, `created_at`, `updated_at`)
- Database table `project` with system fields (`id`, `created_at`, `updated_at`)
- REST API: `GET/POST/PUT/DELETE /api/project`
- Admin UI: List view + Form
- TypeScript types
Expand Down Expand Up @@ -303,7 +303,7 @@ Every object automatically includes system fields:

```yaml
# Auto-generated (not defined in .object.yml)
_id:
id:
type: id
label: Record ID
read_only: true
Expand Down Expand Up @@ -637,7 +637,7 @@ parent_id:
Query polymorphic fields:
```typescript
const activity = await ObjectQL.findOne('activity', id);
// activity.parent_id = { _id: '123', _type: 'account' }
// activity.parent_id = { id: '123', _type: 'account' }
```

### Computed Fields (Virtual)
Expand Down
4 changes: 2 additions & 2 deletions content/docs/protocol/objectql/security.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ const user = await ObjectQL.findOne('user', '123');

// Result (salary/ssn stripped):
// {
// _id: '123',
// id: '123',
// name: 'John Doe',
// email: 'john@example.com'
// // salary: REMOVED
Expand All @@ -268,7 +268,7 @@ const user = await ObjectQL.findOne('user', '123');

// Result (salary visible):
// {
// _id: '123',
// id: '123',
// name: 'John Doe',
// email: 'john@example.com',
// salary: { amount: 120000, currency: 'USD' }
Expand Down
8 changes: 4 additions & 4 deletions content/docs/protocol/objectql/types.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,7 @@ account_id:
deleteBehavior: set_null # or restrict, cascade
```

**Storage:** Stores `_id` of referenced record
**Storage:** Stores `id` of referenced record

**Query behavior:**
```typescript
Expand Down Expand Up @@ -617,7 +617,7 @@ parent:
**Storage:**
```json
{
"_id": "123",
"id": "123",
"_type": "account"
}
```
Expand All @@ -628,12 +628,12 @@ const activity = await ObjectQL.findOne('activity', id, {
expand: ['parent'] // Resolves based on _type
});

// activity.parent = { _id: '123', _type: 'account', company_name: 'Acme' }
// activity.parent = { id: '123', _type: 'account', company_name: 'Acme' }
```

**Database mapping:**
- PostgreSQL: Two columns `parent_id` + `parent_type`
- MongoDB: `{ _id: String, _type: String }`
- MongoDB: `{ id: String, _type: String }`

**Use cases:**
- Activity feeds (related to Account OR Contact)
Expand Down
4 changes: 2 additions & 2 deletions content/docs/protocol/objectui/actions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,7 @@ listView:
- type: api
label: Send Email
icon: mail
endpoint: /api/customers/{_id}/send-email
endpoint: /api/customers/{id}/send-email
```

### Dropdown Menu Actions
Expand All @@ -541,7 +541,7 @@ actions:
label: Clone Customer
- type: api
label: Export to PDF
endpoint: /api/customers/{_id}/export-pdf
endpoint: /api/customers/{id}/export-pdf
- type: separator
- type: workflow
label: Merge Duplicate
Expand Down
2 changes: 1 addition & 1 deletion content/docs/protocol/objectui/layout-dsl.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -522,7 +522,7 @@ sections:
- type: related_list
label: Contacts
object: contact
relationField: account_id # contact.account_id → account._id
relationField: account_id # contact.account_id → account.id
columns:
- name
- email
Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/qa/testing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ A single step in a test scenario, consisting of an action and optional assertion
| **description** | `string` | optional | Human-readable description of what this step tests |
| **action** | `Object` | ✅ | The action to execute in this step |
| **assertions** | `Object[]` | optional | Assertions to validate after the action completes |
| **capture** | `Record<string, string>` | optional | Map result fields to context variables: `{ "newId": "body._id" }` |
| **capture** | `Record<string, string>` | optional | Map result fields to context variables: `{ "newId": "body.id" }` |


---
Expand Down
2 changes: 1 addition & 1 deletion content/prompts/platform/automation.prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export const LeadQualifyFlow: FlowSchema = {
label: 'Get Lead Details',
config: {
object: 'lead',
filter: [['_id', '=', '{leadId}']]
filter: [['id', '=', '{leadId}']]
}
},
{
Expand Down
4 changes: 2 additions & 2 deletions content/prompts/plugin/data-seed.prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export async function seedRoles() {
await object.insert(role);
console.log(`Created Role: ${role.name}`);
} else {
await object.update(existing._id, role);
await object.update(existing.id, role);
console.log(`Updated Role: ${role.name}`);
}
}
Expand Down Expand Up @@ -103,7 +103,7 @@ export const SplitNamesJob: JobSchema = {
for (const c of contacts) {
const [first, ...rest] = c.full_name.split(' ');
await ctx.broker.call('contact.update', {
id: c._id,
id: c.id,
data: { first_name: first, last_name: rest.join(' ') }
});
}
Expand Down
4 changes: 2 additions & 2 deletions examples/app-crm/src/actions/handlers/case.handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ interface ActionContext {
/** Escalate a case to the escalation team */
export async function escalateCase(ctx: ActionContext): Promise<void> {
const { record, engine, user, params } = ctx;
await engine.update('case', record._id as string, {
await engine.update('case', record.id as string, {
is_escalated: true,
escalation_reason: params?.reason as string,
escalated_by: user.id,
Expand All @@ -36,7 +36,7 @@ export async function escalateCase(ctx: ActionContext): Promise<void> {
/** Close a case with a resolution */
export async function closeCase(ctx: ActionContext): Promise<void> {
const { record, engine, user, params } = ctx;
await engine.update('case', record._id as string, {
await engine.update('case', record.id as string, {
is_closed: true,
resolution: params?.resolution as string,
closed_by: user.id,
Expand Down
10 changes: 5 additions & 5 deletions examples/app-crm/src/actions/handlers/contact.handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ interface ActionContext {
user: { id: string; name: string };
engine: {
update(object: string, id: string, data: Record<string, unknown>): Promise<void>;
insert(object: string, data: Record<string, unknown>): Promise<{ _id: string }>;
insert(object: string, data: Record<string, unknown>): Promise<{ id: string }>;
find(object: string, query: Record<string, unknown>): Promise<Array<Record<string, unknown>>>;
};
params?: Record<string, unknown>;
Expand All @@ -31,11 +31,11 @@ export async function markAsPrimaryContact(ctx: ActionContext): Promise<void> {
// Clear existing primary contacts on the same account
const siblings = await engine.find('contact', { account_id: accountId, is_primary: true });
for (const sibling of siblings) {
await engine.update('contact', sibling._id as string, { is_primary: false });
await engine.update('contact', sibling.id as string, { is_primary: false });
}

// Set current contact as primary
await engine.update('contact', record._id as string, { is_primary: true });
await engine.update('contact', record.id as string, { is_primary: true });
}

/** Send an email to a contact (modal form submission handler) */
Expand All @@ -45,12 +45,12 @@ export async function sendEmail(ctx: ActionContext): Promise<{ activityId: strin
type: 'email',
subject: params?.subject ? String(params.subject) : `Email to ${record.email}`,
body: params?.body ? String(params.body) : '',
contact_id: record._id as string,
contact_id: record.id as string,
account_id: record.account_id as string,
direction: 'outbound',
status: 'sent',
created_by: user.id,
sent_at: new Date().toISOString(),
});
return { activityId: activity._id };
return { activityId: activity.id };
}
Loading