diff --git a/ROADMAP.md b/ROADMAP.md index 971889298..c404d8b8f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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. | --- diff --git a/apps/studio/src/components/ObjectDataForm.tsx b/apps/studio/src/components/ObjectDataForm.tsx index acc12d118..b3e8cd12a 100644 --- a/apps/studio/src/components/ObjectDataForm.tsx +++ b/apps/studio/src/components/ObjectDataForm.tsx @@ -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; @@ -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); } @@ -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 ( !open && onCancel()}> diff --git a/apps/studio/src/components/ObjectDataTable.tsx b/apps/studio/src/components/ObjectDataTable.tsx index 9d70812ab..8a11aab8c 100644 --- a/apps/studio/src/components/ObjectDataTable.tsx +++ b/apps/studio/src/components/ObjectDataTable.tsx @@ -277,7 +277,7 @@ export function ObjectDataTable({ objectApiName, onEdit }: ObjectDataTableProps) {loading && records.length === 0 ? ( ) : filteredRecords.map(record => ( - + {columns.map(col => ( @@ -302,7 +302,7 @@ export function ObjectDataTable({ objectApiName, onEdit }: ObjectDataTableProps) handleDelete(record.id || record._id)} + onClick={() => handleDelete(record.id)} className="text-destructive focus:text-destructive" > diff --git a/apps/studio/src/mocks/createKernel.ts b/apps/studio/src/mocks/createKernel.ts index df1eb96fb..8c3326c3d 100644 --- a/apps/studio/src/mocks/createKernel.ts +++ b/apps/studio/src/mocks/createKernel.ts @@ -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 @@ -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') { @@ -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.`); @@ -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]; }); diff --git a/apps/studio/src/types.ts b/apps/studio/src/types.ts index a73f76845..fcff8309c 100644 --- a/apps/studio/src/types.ts +++ b/apps/studio/src/types.ts @@ -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; diff --git a/content/docs/guides/standards.mdx b/content/docs/guides/standards.mdx index e0a1fd282..09df382e0 100644 --- a/content/docs/guides/standards.mdx +++ b/content/docs/guides/standards.mdx @@ -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 diff --git a/content/docs/protocol/objectql/index.mdx b/content/docs/protocol/objectql/index.mdx index 3b76fbc76..3eb8a3a68 100644 --- a/content/docs/protocol/objectql/index.mdx +++ b/content/docs/protocol/objectql/index.mdx @@ -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; diff --git a/content/docs/protocol/objectql/query-syntax.mdx b/content/docs/protocol/objectql/query-syntax.mdx index c85c6ccf5..354106e92 100644 --- a/content/docs/protocol/objectql/query-syntax.mdx +++ b/content/docs/protocol/objectql/query-syntax.mdx @@ -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 @@ -594,7 +594,7 @@ const query = { ['exists', { object: 'opportunity', filters: [ - ['account_id', '=', '{{parent._id}}'], + ['account_id', '=', '{{parent.id}}'], ['stage', '=', 'Closed Won'] ] }] @@ -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' // ) ``` @@ -617,7 +617,7 @@ const query = { ['not_exists', { object: 'opportunity', filters: [ - ['account_id', '=', '{{parent._id}}'] + ['account_id', '=', '{{parent.id}}'] ] }] ] @@ -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' }] }); ``` @@ -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 }); ``` diff --git a/content/docs/protocol/objectql/schema.mdx b/content/docs/protocol/objectql/schema.mdx index ddd32c97f..f43145926 100644 --- a/content/docs/protocol/objectql/schema.mdx +++ b/content/docs/protocol/objectql/schema.mdx @@ -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 @@ -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 @@ -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) diff --git a/content/docs/protocol/objectql/security.mdx b/content/docs/protocol/objectql/security.mdx index b655127a3..99f206196 100644 --- a/content/docs/protocol/objectql/security.mdx +++ b/content/docs/protocol/objectql/security.mdx @@ -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 @@ -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' } diff --git a/content/docs/protocol/objectql/types.mdx b/content/docs/protocol/objectql/types.mdx index d56490579..2d2efae40 100644 --- a/content/docs/protocol/objectql/types.mdx +++ b/content/docs/protocol/objectql/types.mdx @@ -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 @@ -617,7 +617,7 @@ parent: **Storage:** ```json { - "_id": "123", + "id": "123", "_type": "account" } ``` @@ -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) diff --git a/content/docs/protocol/objectui/actions.mdx b/content/docs/protocol/objectui/actions.mdx index b524f85c9..0c2ea2f38 100644 --- a/content/docs/protocol/objectui/actions.mdx +++ b/content/docs/protocol/objectui/actions.mdx @@ -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 @@ -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 diff --git a/content/docs/protocol/objectui/layout-dsl.mdx b/content/docs/protocol/objectui/layout-dsl.mdx index 790c4f375..dc8176ea5 100644 --- a/content/docs/protocol/objectui/layout-dsl.mdx +++ b/content/docs/protocol/objectui/layout-dsl.mdx @@ -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 diff --git a/content/docs/references/qa/testing.mdx b/content/docs/references/qa/testing.mdx index d4a6cc7d1..9817bff68 100644 --- a/content/docs/references/qa/testing.mdx +++ b/content/docs/references/qa/testing.mdx @@ -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` | optional | Map result fields to context variables: `{ "newId": "body._id" }` | +| **capture** | `Record` | optional | Map result fields to context variables: `{ "newId": "body.id" }` | --- diff --git a/content/prompts/platform/automation.prompt.md b/content/prompts/platform/automation.prompt.md index 5ec818caf..895c38906 100644 --- a/content/prompts/platform/automation.prompt.md +++ b/content/prompts/platform/automation.prompt.md @@ -85,7 +85,7 @@ export const LeadQualifyFlow: FlowSchema = { label: 'Get Lead Details', config: { object: 'lead', - filter: [['_id', '=', '{leadId}']] + filter: [['id', '=', '{leadId}']] } }, { diff --git a/content/prompts/plugin/data-seed.prompt.md b/content/prompts/plugin/data-seed.prompt.md index 8c2f6cab8..9eab262ed 100644 --- a/content/prompts/plugin/data-seed.prompt.md +++ b/content/prompts/plugin/data-seed.prompt.md @@ -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}`); } } @@ -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(' ') } }); } diff --git a/examples/app-crm/src/actions/handlers/case.handlers.ts b/examples/app-crm/src/actions/handlers/case.handlers.ts index a0d45d37f..c1c672b74 100644 --- a/examples/app-crm/src/actions/handlers/case.handlers.ts +++ b/examples/app-crm/src/actions/handlers/case.handlers.ts @@ -24,7 +24,7 @@ interface ActionContext { /** Escalate a case to the escalation team */ export async function escalateCase(ctx: ActionContext): Promise { 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, @@ -36,7 +36,7 @@ export async function escalateCase(ctx: ActionContext): Promise { /** Close a case with a resolution */ export async function closeCase(ctx: ActionContext): Promise { 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, diff --git a/examples/app-crm/src/actions/handlers/contact.handlers.ts b/examples/app-crm/src/actions/handlers/contact.handlers.ts index 15918901e..3cacbdef0 100644 --- a/examples/app-crm/src/actions/handlers/contact.handlers.ts +++ b/examples/app-crm/src/actions/handlers/contact.handlers.ts @@ -17,7 +17,7 @@ interface ActionContext { user: { id: string; name: string }; engine: { update(object: string, id: string, data: Record): Promise; - insert(object: string, data: Record): Promise<{ _id: string }>; + insert(object: string, data: Record): Promise<{ id: string }>; find(object: string, query: Record): Promise>>; }; params?: Record; @@ -31,11 +31,11 @@ export async function markAsPrimaryContact(ctx: ActionContext): Promise { // 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) */ @@ -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 }; } diff --git a/examples/app-crm/src/actions/handlers/global.handlers.ts b/examples/app-crm/src/actions/handlers/global.handlers.ts index a1ec40564..cbda27750 100644 --- a/examples/app-crm/src/actions/handlers/global.handlers.ts +++ b/examples/app-crm/src/actions/handlers/global.handlers.ts @@ -16,7 +16,7 @@ interface ActionContext { record: Record; user: { id: string; name: string }; engine: { - insert(object: string, data: Record): Promise<{ _id: string }>; + insert(object: string, data: Record): Promise<{ id: string }>; find(object: string, query: Record): Promise>>; }; params?: Record; @@ -43,11 +43,11 @@ export async function logCall(ctx: ActionContext): Promise<{ activityId: string subject: params?.subject ? String(params.subject) : 'Untitled Call', duration_minutes: params?.duration ? Number(params.duration) : 0, notes: params?.notes ? String(params.notes) : '', - related_to_id: record._id as string, + related_to_id: record.id as string, direction: 'outbound', status: 'completed', created_by: user.id, call_date: new Date().toISOString(), }); - return { activityId: activity._id }; + return { activityId: activity.id }; } diff --git a/examples/app-crm/src/actions/handlers/lead.handlers.ts b/examples/app-crm/src/actions/handlers/lead.handlers.ts index 4d426d0e4..d5cdbc76c 100644 --- a/examples/app-crm/src/actions/handlers/lead.handlers.ts +++ b/examples/app-crm/src/actions/handlers/lead.handlers.ts @@ -20,7 +20,7 @@ interface ActionContext { user: { id: string; name: string }; engine: { update(object: string, id: string, data: Record): Promise; - insert(object: string, data: Record): Promise<{ _id: string }>; + insert(object: string, data: Record): Promise<{ id: string }>; find(object: string, query: Record): Promise>>; }; params?: Record; @@ -46,29 +46,29 @@ export async function convertLead(ctx: ActionContext): Promise<{ last_name: record.last_name, email: record.email, phone: record.phone, - account_id: account._id, + account_id: account.id, }); const opportunity = await engine.insert('opportunity', { name: `${record.company} - New Opportunity`, - account_id: account._id, - contact_id: contact._id, + account_id: account.id, + contact_id: contact.id, stage: 'prospecting', amount: record.estimated_value ?? 0, }); - await engine.update('lead', record._id as string, { + await engine.update('lead', record.id as string, { is_converted: true, status: 'converted', - converted_account_id: account._id, - converted_contact_id: contact._id, - converted_opportunity_id: opportunity._id, + converted_account_id: account.id, + converted_contact_id: contact.id, + converted_opportunity_id: opportunity.id, }); return { - accountId: account._id, - contactId: contact._id, - opportunityId: opportunity._id, + accountId: account.id, + contactId: contact.id, + opportunityId: opportunity.id, }; } diff --git a/examples/app-crm/src/actions/handlers/opportunity.handlers.ts b/examples/app-crm/src/actions/handlers/opportunity.handlers.ts index 49d4abdc1..53edd0ead 100644 --- a/examples/app-crm/src/actions/handlers/opportunity.handlers.ts +++ b/examples/app-crm/src/actions/handlers/opportunity.handlers.ts @@ -16,16 +16,16 @@ interface ActionContext { user: { id: string; name: string }; engine: { update(object: string, id: string, data: Record): Promise; - insert(object: string, data: Record): Promise<{ _id: string }>; + insert(object: string, data: Record): Promise<{ id: string }>; find(object: string, query: Record): Promise>>; }; params?: Record; } /** Clone an opportunity record */ -export async function cloneRecord(ctx: ActionContext): Promise<{ _id: string }> { +export async function cloneRecord(ctx: ActionContext): Promise<{ id: string }> { const { record, engine } = ctx; - const { _id, created_at, updated_at, ...fields } = record as Record; + const { id, created_at, updated_at, ...fields } = record as Record; return engine.insert('opportunity', { ...fields, name: `Copy of ${fields.name ?? 'Untitled'}`, diff --git a/examples/app-todo/src/actions/task.handlers.ts b/examples/app-todo/src/actions/task.handlers.ts index 771cee766..a7b3dfc64 100644 --- a/examples/app-todo/src/actions/task.handlers.ts +++ b/examples/app-todo/src/actions/task.handlers.ts @@ -23,7 +23,7 @@ interface ActionContext { /** Data engine for CRUD operations */ engine: { update(object: string, id: string, data: Record): Promise; - insert(object: string, data: Record): Promise<{ _id: string }>; + insert(object: string, data: Record): Promise<{ id: string }>; find(object: string, query: Record): Promise>>; delete(object: string, ids: string[]): Promise; }; @@ -34,7 +34,7 @@ interface ActionContext { /** Mark a single task as complete */ export async function completeTask(ctx: ActionContext): Promise { const { record, engine, user } = ctx; - await engine.update('task', record._id as string, { + await engine.update('task', record.id as string, { status: 'completed', completed_at: new Date().toISOString(), completed_by: user.id, @@ -44,16 +44,16 @@ export async function completeTask(ctx: ActionContext): Promise { /** Mark a task as in-progress */ export async function startTask(ctx: ActionContext): Promise { const { record, engine } = ctx; - await engine.update('task', record._id as string, { + await engine.update('task', record.id as string, { status: 'in_progress', started_at: new Date().toISOString(), }); } /** Clone a task (duplicate with reset status) */ -export async function cloneTask(ctx: ActionContext): Promise<{ _id: string }> { +export async function cloneTask(ctx: ActionContext): Promise<{ id: string }> { const { record, engine } = ctx; - const { _id, created_at, updated_at, completed_at, completed_by, ...fields } = record as Record; + const { id, created_at, updated_at, completed_at, completed_by, ...fields } = record as Record; return engine.insert('task', { ...fields, status: 'not_started', @@ -79,7 +79,7 @@ export async function massCompleteTasks(ctx: ActionContext): Promise { export async function deleteCompletedTasks(ctx: ActionContext): Promise { const { engine } = ctx; const completed = await engine.find('task', { status: 'completed' }); - const ids = completed.map((r) => r._id as string); + const ids = completed.map((r) => r.id as string); if (ids.length > 0) { await engine.delete('task', ids); } @@ -88,7 +88,7 @@ export async function deleteCompletedTasks(ctx: ActionContext): Promise { /** Defer a task by updating its due date (modal form submission handler) */ export async function deferTask(ctx: ActionContext): Promise { const { record, engine, params } = ctx; - await engine.update('task', record._id as string, { + await engine.update('task', record.id as string, { due_date: params?.new_due_date ? String(params.new_due_date) : null, defer_reason: params?.reason ? String(params.reason) : null, status: 'waiting', @@ -98,7 +98,7 @@ export async function deferTask(ctx: ActionContext): Promise { /** Set a reminder on a task (modal form submission handler) */ export async function setReminder(ctx: ActionContext): Promise { const { record, engine, params } = ctx; - await engine.update('task', record._id as string, { + await engine.update('task', record.id as string, { reminder_date: params?.reminder_date ? String(params.reminder_date) : null, has_reminder: true, }); diff --git a/packages/client/src/client.hono.test.ts b/packages/client/src/client.hono.test.ts index 1f8bcbf46..c664fd9fe 100644 --- a/packages/client/src/client.hono.test.ts +++ b/packages/client/src/client.hono.test.ts @@ -37,7 +37,7 @@ describe('ObjectStackClient (with Hono Server)', () => { 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 }; } // Params from HttpDispatcher: { object, id, ...query } if (method === 'get') { diff --git a/packages/client/src/client.msw.test.ts b/packages/client/src/client.msw.test.ts index a2a2d7559..2a4d5cbaf 100644 --- a/packages/client/src/client.msw.test.ts +++ b/packages/client/src/client.msw.test.ts @@ -43,7 +43,7 @@ describe('ObjectStackClient (with MSW Plugin)', () => { 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') { if (protocol) { diff --git a/packages/core/src/qa/http-adapter.ts b/packages/core/src/qa/http-adapter.ts index b7f0fd469..f2b76b1aa 100644 --- a/packages/core/src/qa/http-adapter.ts +++ b/packages/core/src/qa/http-adapter.ts @@ -49,8 +49,8 @@ export class HttpTestAdapter implements TestExecutionAdapter { } private async updateRecord(objectName: string, data: Record, headers: Record) { - const id = data._id || data.id; - if (!id) throw new Error('Update record requires _id or id in payload'); + const id = data.id; + if (!id) throw new Error('Update record requires id in payload'); const response = await fetch(`${this.baseUrl}/api/data/${objectName}/${id}`, { method: 'PUT', headers, @@ -60,8 +60,8 @@ export class HttpTestAdapter implements TestExecutionAdapter { } private async deleteRecord(objectName: string, data: Record, headers: Record) { - const id = data._id || data.id; - if (!id) throw new Error('Delete record requires _id or id in payload'); + const id = data.id; + if (!id) throw new Error('Delete record requires id in payload'); const response = await fetch(`${this.baseUrl}/api/data/${objectName}/${id}`, { method: 'DELETE', headers @@ -70,8 +70,8 @@ export class HttpTestAdapter implements TestExecutionAdapter { } private async readRecord(objectName: string, data: Record, headers: Record) { - const id = data._id || data.id; - if (!id) throw new Error('Read record requires _id or id in payload'); + const id = data.id; + if (!id) throw new Error('Read record requires id in payload'); const response = await fetch(`${this.baseUrl}/api/data/${objectName}/${id}`, { method: 'GET', headers diff --git a/packages/objectql/src/engine.test.ts b/packages/objectql/src/engine.test.ts index 488b52067..01a2579e0 100644 --- a/packages/objectql/src/engine.test.ts +++ b/packages/objectql/src/engine.test.ts @@ -239,20 +239,20 @@ describe('ObjectQL Engine', () => { // Primary find returns tasks with assignee IDs vi.mocked(mockDriver.find) .mockResolvedValueOnce([ - { _id: 't1', title: 'Task 1', assignee: 'u1' }, - { _id: 't2', title: 'Task 2', assignee: 'u2' }, + { id: 't1', title: 'Task 1', assignee: 'u1' }, + { id: 't2', title: 'Task 2', assignee: 'u2' }, ]) // Second call (expand): returns user records .mockResolvedValueOnce([ - { _id: 'u1', name: 'Alice' }, - { _id: 'u2', name: 'Bob' }, + { id: 'u1', name: 'Alice' }, + { id: 'u2', name: 'Bob' }, ]); const result = await engine.find('task', { populate: ['assignee'] }); expect(result).toHaveLength(2); - expect(result[0].assignee).toEqual({ _id: 'u1', name: 'Alice' }); - expect(result[1].assignee).toEqual({ _id: 'u2', name: 'Bob' }); + expect(result[0].assignee).toEqual({ id: 'u1', name: 'Alice' }); + expect(result[1].assignee).toEqual({ id: 'u2', name: 'Bob' }); // Verify the expand query used $in expect(mockDriver.find).toHaveBeenCalledTimes(2); @@ -260,7 +260,7 @@ describe('ObjectQL Engine', () => { 'user', expect.objectContaining({ object: 'user', - where: { _id: { $in: ['u1', 'u2'] } }, + where: { id: { $in: ['u1', 'u2'] } }, }), ); }); @@ -282,14 +282,14 @@ describe('ObjectQL Engine', () => { vi.mocked(mockDriver.find) .mockResolvedValueOnce([ - { _id: 'oi1', order: 'o1' }, + { id: 'oi1', order: 'o1' }, ]) .mockResolvedValueOnce([ - { _id: 'o1', total: 100 }, + { id: 'o1', total: 100 }, ]); const result = await engine.find('order_item', { populate: ['order'] }); - expect(result[0].order).toEqual({ _id: 'o1', total: 100 }); + expect(result[0].order).toEqual({ id: 'o1', total: 100 }); }); it('should skip expand for fields without reference definition', async () => { @@ -301,7 +301,7 @@ describe('ObjectQL Engine', () => { } as any); vi.mocked(mockDriver.find).mockResolvedValueOnce([ - { _id: 't1', title: 'Task 1' }, + { id: 't1', title: 'Task 1' }, ]); const result = await engine.find('task', { populate: ['title'] }); @@ -313,7 +313,7 @@ describe('ObjectQL Engine', () => { vi.mocked(SchemaRegistry.getObject).mockReturnValue(undefined); vi.mocked(mockDriver.find).mockResolvedValueOnce([ - { _id: 't1', assignee: 'u1' }, + { id: 't1', assignee: 'u1' }, ]); const result = await engine.find('task', { populate: ['assignee'] }); @@ -338,16 +338,16 @@ describe('ObjectQL Engine', () => { vi.mocked(mockDriver.find) .mockResolvedValueOnce([ - { _id: 't1', assignee: null }, - { _id: 't2', assignee: 'u1' }, + { id: 't1', assignee: null }, + { id: 't2', assignee: 'u1' }, ]) .mockResolvedValueOnce([ - { _id: 'u1', name: 'Alice' }, + { id: 'u1', name: 'Alice' }, ]); const result = await engine.find('task', { populate: ['assignee'] }); expect(result[0].assignee).toBeNull(); - expect(result[1].assignee).toEqual({ _id: 'u1', name: 'Alice' }); + expect(result[1].assignee).toEqual({ id: 'u1', name: 'Alice' }); }); it('should de-duplicate foreign key IDs in batch query', async () => { @@ -367,13 +367,13 @@ describe('ObjectQL Engine', () => { vi.mocked(mockDriver.find) .mockResolvedValueOnce([ - { _id: 't1', assignee: 'u1' }, - { _id: 't2', assignee: 'u1' }, // Same user - { _id: 't3', assignee: 'u2' }, + { id: 't1', assignee: 'u1' }, + { id: 't2', assignee: 'u1' }, // Same user + { id: 't3', assignee: 'u2' }, ]) .mockResolvedValueOnce([ - { _id: 'u1', name: 'Alice' }, - { _id: 'u2', name: 'Bob' }, + { id: 'u1', name: 'Alice' }, + { id: 'u2', name: 'Bob' }, ]); const result = await engine.find('task', { populate: ['assignee'] }); @@ -382,11 +382,11 @@ describe('ObjectQL Engine', () => { expect(mockDriver.find).toHaveBeenLastCalledWith( 'user', expect.objectContaining({ - where: { _id: { $in: ['u1', 'u2'] } }, + where: { id: { $in: ['u1', 'u2'] } }, }), ); - expect(result[0].assignee).toEqual({ _id: 'u1', name: 'Alice' }); - expect(result[1].assignee).toEqual({ _id: 'u1', name: 'Alice' }); + expect(result[0].assignee).toEqual({ id: 'u1', name: 'Alice' }); + expect(result[1].assignee).toEqual({ id: 'u1', name: 'Alice' }); }); it('should keep raw ID when referenced record not found', async () => { @@ -406,7 +406,7 @@ describe('ObjectQL Engine', () => { vi.mocked(mockDriver.find) .mockResolvedValueOnce([ - { _id: 't1', assignee: 'u_deleted' }, + { id: 't1', assignee: 'u_deleted' }, ]) .mockResolvedValueOnce([]); // No records found @@ -436,15 +436,15 @@ describe('ObjectQL Engine', () => { vi.mocked(mockDriver.find) .mockResolvedValueOnce([ - { _id: 't1', assignee: 'u1', project: 'p1' }, + { id: 't1', assignee: 'u1', project: 'p1' }, ]) - .mockResolvedValueOnce([{ _id: 'u1', name: 'Alice' }]) - .mockResolvedValueOnce([{ _id: 'p1', name: 'Project X' }]); + .mockResolvedValueOnce([{ id: 'u1', name: 'Alice' }]) + .mockResolvedValueOnce([{ id: 'p1', name: 'Project X' }]); const result = await engine.find('task', { populate: ['assignee', 'project'] }); - expect(result[0].assignee).toEqual({ _id: 'u1', name: 'Alice' }); - expect(result[0].project).toEqual({ _id: 'p1', name: 'Project X' }); + expect(result[0].assignee).toEqual({ id: 'u1', name: 'Alice' }); + expect(result[0].project).toEqual({ id: 'p1', name: 'Project X' }); expect(mockDriver.find).toHaveBeenCalledTimes(3); }); @@ -464,15 +464,15 @@ describe('ObjectQL Engine', () => { }); vi.mocked(mockDriver.findOne as any).mockResolvedValueOnce( - { _id: 't1', title: 'Task 1', assignee: 'u1' }, + { id: 't1', title: 'Task 1', assignee: 'u1' }, ); vi.mocked(mockDriver.find).mockResolvedValueOnce([ - { _id: 'u1', name: 'Alice' }, + { id: 'u1', name: 'Alice' }, ]); const result = await engine.findOne('task', { populate: ['assignee'] }); - expect(result.assignee).toEqual({ _id: 'u1', name: 'Alice' }); + expect(result.assignee).toEqual({ id: 'u1', name: 'Alice' }); }); it('should handle already-expanded objects (skip re-expansion)', async () => { @@ -492,14 +492,14 @@ describe('ObjectQL Engine', () => { // Driver returns an already-expanded object vi.mocked(mockDriver.find).mockResolvedValueOnce([ - { _id: 't1', assignee: { _id: 'u1', name: 'Alice' } }, + { id: 't1', assignee: { id: 'u1', name: 'Alice' } }, ]); const result = await engine.find('task', { populate: ['assignee'] }); // No expand query should have been made — the value was already an object expect(mockDriver.find).toHaveBeenCalledTimes(1); - expect(result[0].assignee).toEqual({ _id: 'u1', name: 'Alice' }); + expect(result[0].assignee).toEqual({ id: 'u1', name: 'Alice' }); }); it('should gracefully handle expand errors and keep raw IDs', async () => { @@ -519,7 +519,7 @@ describe('ObjectQL Engine', () => { vi.mocked(mockDriver.find) .mockResolvedValueOnce([ - { _id: 't1', assignee: 'u1' }, + { id: 't1', assignee: 'u1' }, ]) .mockRejectedValueOnce(new Error('Driver connection failed')); @@ -544,17 +544,17 @@ describe('ObjectQL Engine', () => { vi.mocked(mockDriver.find) .mockResolvedValueOnce([ - { _id: 't1', watchers: ['u1', 'u2'] }, + { id: 't1', watchers: ['u1', 'u2'] }, ]) .mockResolvedValueOnce([ - { _id: 'u1', name: 'Alice' }, - { _id: 'u2', name: 'Bob' }, + { id: 'u1', name: 'Alice' }, + { id: 'u2', name: 'Bob' }, ]); const result = await engine.find('task', { populate: ['watchers'] }); expect(result[0].watchers).toEqual([ - { _id: 'u1', name: 'Alice' }, - { _id: 'u2', name: 'Bob' }, + { id: 'u1', name: 'Alice' }, + { id: 'u2', name: 'Bob' }, ]); }); @@ -570,14 +570,14 @@ describe('ObjectQL Engine', () => { }); vi.mocked(mockDriver.find) - .mockResolvedValueOnce([{ _id: 't1', project: 'p1' }]) // find task - .mockResolvedValueOnce([{ _id: 'p1', org: 'o1' }]); // expand project (depth 0) + .mockResolvedValueOnce([{ id: 't1', project: 'p1' }]) // find task + .mockResolvedValueOnce([{ id: 'p1', org: 'o1' }]); // expand project (depth 0) // org should NOT be expanded further — flat populate doesn't create nested expand const result = await engine.find('task', { populate: ['project'] }); // Project expanded, but org inside project remains as raw ID - expect(result[0].project).toEqual({ _id: 'p1', org: 'o1' }); + expect(result[0].project).toEqual({ id: 'p1', org: 'o1' }); expect(mockDriver.find).toHaveBeenCalledTimes(2); // Only primary + 1 expand query }); @@ -588,11 +588,11 @@ describe('ObjectQL Engine', () => { } as any); vi.mocked(mockDriver.find).mockResolvedValueOnce([ - { _id: 't1', title: 'Task 1' }, + { id: 't1', title: 'Task 1' }, ]); const result = await engine.find('task', {}); - expect(result).toEqual([{ _id: 't1', title: 'Task 1' }]); + expect(result).toEqual([{ id: 't1', title: 'Task 1' }]); expect(mockDriver.find).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/objectql/src/engine.ts b/packages/objectql/src/engine.ts index a2ef591ab..72063fe87 100644 --- a/packages/objectql/src/engine.ts +++ b/packages/objectql/src/engine.ts @@ -681,7 +681,7 @@ export class ObjectQL implements IDataEngine { try { const relatedQuery: QueryAST = { object: referenceObject, - where: { _id: { $in: uniqueIds } }, + where: { id: { $in: uniqueIds } }, ...(nestedAST.fields ? { fields: nestedAST.fields } : {}), ...(nestedAST.orderBy ? { orderBy: nestedAST.orderBy } : {}), }; @@ -692,7 +692,7 @@ export class ObjectQL implements IDataEngine { // Build a lookup map: id → record const recordMap = new Map(); for (const rec of relatedRecords) { - const id = rec._id ?? rec.id; + const id = rec.id; if (id != null) recordMap.set(String(id), rec); } @@ -707,7 +707,7 @@ export class ObjectQL implements IDataEngine { // Rebuild map with expanded records recordMap.clear(); for (const rec of expandedRelated) { - const id = rec._id ?? rec.id; + const id = rec.id; if (id != null) recordMap.set(String(id), rec); } } @@ -921,10 +921,9 @@ export class ObjectQL implements IDataEngine { const driver = this.getDriver(object); // 1. Extract ID from data or filter if it's a single update by ID - let id = data.id || data._id; + let id = data.id; if (!id && options?.filter) { if (typeof options.filter === 'string') id = options.filter; - else if (options.filter._id) id = options.filter._id; else if (options.filter.id) id = options.filter.id; } @@ -980,7 +979,6 @@ export class ObjectQL implements IDataEngine { let id: any = undefined; if (options?.filter) { if (typeof options.filter === 'string') id = options.filter; - else if (options.filter._id) id = options.filter._id; else if (options.filter.id) id = options.filter.id; } @@ -1043,7 +1041,7 @@ export class ObjectQL implements IDataEngine { return driver.count(object, ast); } // Fallback to find().length - const res = await this.find(object, { filter: query?.filter, select: ['_id'] }); + const res = await this.find(object, { filter: query?.filter, select: ['id'] }); return res.length; }); @@ -1335,8 +1333,8 @@ export class ObjectRepository { /** Update a single record by ID */ async updateById(id: string | number, data: any): Promise { - return this.engine.update(this.objectName, { ...data, _id: id }, { - filter: { _id: id }, + return this.engine.update(this.objectName, { ...data, id: id }, { + filter: { id: id }, context: this.context, }); } @@ -1351,7 +1349,7 @@ export class ObjectRepository { /** Delete a single record by ID */ async deleteById(id: string | number): Promise { return this.engine.delete(this.objectName, { - filter: { _id: id }, + filter: { id: id }, context: this.context, }); } diff --git a/packages/objectql/src/plugin.ts b/packages/objectql/src/plugin.ts index 040c55cb8..c83cdc1d8 100644 --- a/packages/objectql/src/plugin.ts +++ b/packages/objectql/src/plugin.ts @@ -129,35 +129,35 @@ export class ObjectQLPlugin implements Plugin { } /** - * Register built-in audit hooks for auto-stamping createdBy/modifiedBy + * Register built-in audit hooks for auto-stamping created_by/updated_by * and fetching previousData for update/delete operations. */ private registerAuditHooks(ctx: PluginContext) { if (!this.ql) return; - // Auto-stamp createdBy/modifiedBy on insert + // Auto-stamp created_by/updated_by on insert this.ql.registerHook('beforeInsert', async (hookCtx) => { if (hookCtx.session?.userId && hookCtx.input?.data) { const data = hookCtx.input.data as Record; if (typeof data === 'object' && data !== null) { data.created_by = data.created_by ?? hookCtx.session.userId; - data.modified_by = hookCtx.session.userId; + data.updated_by = hookCtx.session.userId; data.created_at = data.created_at ?? new Date().toISOString(); - data.modified_at = new Date().toISOString(); + data.updated_at = new Date().toISOString(); if (hookCtx.session.tenantId) { - data.space_id = data.space_id ?? hookCtx.session.tenantId; + data.tenant_id = data.tenant_id ?? hookCtx.session.tenantId; } } } }, { object: '*', priority: 10 }); - // Auto-stamp modifiedBy on update + // Auto-stamp updated_by on update this.ql.registerHook('beforeUpdate', async (hookCtx) => { if (hookCtx.session?.userId && hookCtx.input?.data) { const data = hookCtx.input.data as Record; if (typeof data === 'object' && data !== null) { - data.modified_by = hookCtx.session.userId; - data.modified_at = new Date().toISOString(); + data.updated_by = hookCtx.session.userId; + data.updated_at = new Date().toISOString(); } } }, { object: '*', priority: 10 }); @@ -167,7 +167,7 @@ export class ObjectQLPlugin implements Plugin { if (hookCtx.input?.id && !hookCtx.previous) { try { const existing = await this.ql!.findOne(hookCtx.object, { - filter: { _id: hookCtx.input.id } + filter: { id: hookCtx.input.id } }); if (existing) { hookCtx.previous = existing; @@ -183,7 +183,7 @@ export class ObjectQLPlugin implements Plugin { if (hookCtx.input?.id && !hookCtx.previous) { try { const existing = await this.ql!.findOne(hookCtx.object, { - filter: { _id: hookCtx.input.id } + filter: { id: hookCtx.input.id } }); if (existing) { hookCtx.previous = existing; @@ -194,11 +194,11 @@ export class ObjectQLPlugin implements Plugin { } }, { object: '*', priority: 5 }); - ctx.logger.debug('Audit hooks registered (createdBy/modifiedBy, previousData)'); + ctx.logger.debug('Audit hooks registered (created_by/updated_by, previousData)'); } /** - * Register tenant isolation middleware that auto-injects space_id filter + * Register tenant isolation middleware that auto-injects tenant_id filter * for multi-tenant operations. */ private registerTenantMiddleware(ctx: PluginContext) { @@ -210,10 +210,10 @@ export class ObjectQLPlugin implements Plugin { return next(); } - // Read operations: inject space_id filter into AST + // Read operations: inject tenant_id filter into AST if (['find', 'findOne', 'count', 'aggregate'].includes(opCtx.operation)) { if (opCtx.ast) { - const tenantFilter = { space_id: opCtx.context.tenantId }; + const tenantFilter = { tenant_id: opCtx.context.tenantId }; if (opCtx.ast.where) { opCtx.ast.where = { $and: [opCtx.ast.where, tenantFilter] }; } else { diff --git a/packages/objectql/src/protocol-data.test.ts b/packages/objectql/src/protocol-data.test.ts index 5f7382ccd..6950bba01 100644 --- a/packages/objectql/src/protocol-data.test.ts +++ b/packages/objectql/src/protocol-data.test.ts @@ -129,14 +129,14 @@ describe('ObjectStackProtocolImplementation - Data Operations', () => { }); it('should return records and standard response shape', async () => { - mockEngine.find.mockResolvedValue([{ _id: 't1', name: 'Task 1' }]); + mockEngine.find.mockResolvedValue([{ id: 't1', name: 'Task 1' }]); const result = await protocol.findData({ object: 'task', query: {} }); expect(result).toEqual( expect.objectContaining({ object: 'task', - records: [{ _id: 't1', name: 'Task 1' }], + records: [{ id: 't1', name: 'Task 1' }], total: 1, }), ); @@ -149,21 +149,21 @@ describe('ObjectStackProtocolImplementation - Data Operations', () => { describe('getData', () => { it('should convert expand string to populate array', async () => { - mockEngine.findOne.mockResolvedValue({ _id: 'oi_1', name: 'Item 1' }); + mockEngine.findOne.mockResolvedValue({ id: 'oi_1', name: 'Item 1' }); await protocol.getData({ object: 'order_item', id: 'oi_1', expand: 'order,product' }); expect(mockEngine.findOne).toHaveBeenCalledWith( 'order_item', expect.objectContaining({ - filter: { _id: 'oi_1' }, + filter: { id: 'oi_1' }, populate: ['order', 'product'], }), ); }); it('should convert expand array to populate array', async () => { - mockEngine.findOne.mockResolvedValue({ _id: 't1' }); + mockEngine.findOne.mockResolvedValue({ id: 't1' }); await protocol.getData({ object: 'task', id: 't1', expand: ['assignee', 'project'] }); @@ -176,7 +176,7 @@ describe('ObjectStackProtocolImplementation - Data Operations', () => { }); it('should convert select string to array', async () => { - mockEngine.findOne.mockResolvedValue({ _id: 't1', name: 'Test' }); + mockEngine.findOne.mockResolvedValue({ id: 't1', name: 'Test' }); await protocol.getData({ object: 'task', id: 't1', select: 'name,status' }); @@ -189,7 +189,7 @@ describe('ObjectStackProtocolImplementation - Data Operations', () => { }); it('should pass both expand and select together', async () => { - mockEngine.findOne.mockResolvedValue({ _id: 'oi_1' }); + mockEngine.findOne.mockResolvedValue({ id: 'oi_1' }); await protocol.getData({ object: 'order_item', @@ -201,7 +201,7 @@ describe('ObjectStackProtocolImplementation - Data Operations', () => { expect(mockEngine.findOne).toHaveBeenCalledWith( 'order_item', expect.objectContaining({ - filter: { _id: 'oi_1' }, + filter: { id: 'oi_1' }, populate: ['order'], select: ['name', 'total'], }), @@ -209,25 +209,25 @@ describe('ObjectStackProtocolImplementation - Data Operations', () => { }); it('should work without expand or select', async () => { - mockEngine.findOne.mockResolvedValue({ _id: 't1' }); + mockEngine.findOne.mockResolvedValue({ id: 't1' }); await protocol.getData({ object: 'task', id: 't1' }); expect(mockEngine.findOne).toHaveBeenCalledWith( 'task', - { filter: { _id: 't1' } }, + { filter: { id: 't1' } }, ); }); it('should return standard GetDataResponse shape', async () => { - mockEngine.findOne.mockResolvedValue({ _id: 'oi_1', name: 'Item 1' }); + mockEngine.findOne.mockResolvedValue({ id: 'oi_1', name: 'Item 1' }); const result = await protocol.getData({ object: 'order_item', id: 'oi_1' }); expect(result).toEqual({ object: 'order_item', id: 'oi_1', - record: { _id: 'oi_1', name: 'Item 1' }, + record: { id: 'oi_1', name: 'Item 1' }, }); }); diff --git a/packages/objectql/src/protocol.ts b/packages/objectql/src/protocol.ts index 7537ed929..26a4b3152 100644 --- a/packages/objectql/src/protocol.ts +++ b/packages/objectql/src/protocol.ts @@ -246,7 +246,7 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol { // Form View Generation // Simple single-section layout for now const formFields = fieldKeys - .filter(k => k !== 'id' && k !== 'created_at' && k !== 'modified_at' && !fields[k].hidden) + .filter(k => k !== 'id' && k !== 'created_at' && k !== 'updated_at' && !fields[k].hidden) .map(f => ({ field: f, label: fields[f]?.label, @@ -361,7 +361,7 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol { async getData(request: { object: string, id: string, expand?: string | string[], select?: string | string[] }) { const queryOptions: any = { - filter: { _id: request.id } + filter: { id: request.id } }; // Support select for single-record retrieval @@ -393,14 +393,14 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol { const result = await this.engine.insert(request.object, request.data); return { object: request.object, - id: result._id || result.id, + id: result.id, record: result }; } async updateData(request: { object: string, id: string, data: any }) { // Adapt: update(obj, id, data) -> update(obj, data, options) - const result = await this.engine.update(request.object, request.data, { filter: { _id: request.id } }); + const result = await this.engine.update(request.object, request.data, { filter: { id: request.id } }); return { object: request.object, id: request.id, @@ -410,7 +410,7 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol { async deleteData(request: { object: string, id: string }) { // Adapt: delete(obj, id) -> delete(obj, options) - await this.engine.delete(request.object, { filter: { _id: request.id } }); + await this.engine.delete(request.object, { filter: { id: request.id } }); return { object: request.object, id: request.id, @@ -478,13 +478,13 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol { switch (operation) { case 'create': { const created = await this.engine.insert(object, record.data || record); - results.push({ id: created._id || created.id, success: true, record: created }); + results.push({ id: created.id, success: true, record: created }); succeeded++; break; } case 'update': { if (!record.id) throw new Error('Record id is required for update'); - const updated = await this.engine.update(object, record.data || {}, { filter: { _id: record.id } }); + const updated = await this.engine.update(object, record.data || {}, { filter: { id: record.id } }); results.push({ id: record.id, success: true, record: updated }); succeeded++; break; @@ -493,28 +493,28 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol { // Try update first, then create if not found if (record.id) { try { - const existing = await this.engine.findOne(object, { filter: { _id: record.id } }); + const existing = await this.engine.findOne(object, { filter: { id: record.id } }); if (existing) { - const updated = await this.engine.update(object, record.data || {}, { filter: { _id: record.id } }); + const updated = await this.engine.update(object, record.data || {}, { filter: { id: record.id } }); results.push({ id: record.id, success: true, record: updated }); } else { - const created = await this.engine.insert(object, { _id: record.id, ...(record.data || {}) }); - results.push({ id: created._id || created.id, success: true, record: created }); + const created = await this.engine.insert(object, { id: record.id, ...(record.data || {}) }); + results.push({ id: created.id, success: true, record: created }); } } catch { - const created = await this.engine.insert(object, { _id: record.id, ...(record.data || {}) }); - results.push({ id: created._id || created.id, success: true, record: created }); + const created = await this.engine.insert(object, { id: record.id, ...(record.data || {}) }); + results.push({ id: created.id, success: true, record: created }); } } else { const created = await this.engine.insert(object, record.data || record); - results.push({ id: created._id || created.id, success: true, record: created }); + results.push({ id: created.id, success: true, record: created }); } succeeded++; break; } case 'delete': { if (!record.id) throw new Error('Record id is required for delete'); - await this.engine.delete(object, { filter: { _id: record.id } }); + await this.engine.delete(object, { filter: { id: record.id } }); results.push({ id: record.id, success: true }); succeeded++; break; @@ -563,7 +563,7 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol { for (const record of records) { try { - const updated = await this.engine.update(object, record.data, { filter: { _id: record.id } }); + const updated = await this.engine.update(object, record.data, { filter: { id: record.id } }); results.push({ id: record.id, success: true, record: updated }); succeeded++; } catch (err: any) { @@ -765,7 +765,7 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol { async deleteManyData(request: DeleteManyDataRequest): Promise { // This expects deleting by IDs. return this.engine.delete(request.object, { - filter: { _id: { $in: request.ids } }, + filter: { id: { $in: request.ids } }, ...request.options }); } diff --git a/packages/plugins/driver-memory/src/memory-matcher.ts b/packages/plugins/driver-memory/src/memory-matcher.ts index f7a7d1acf..3957e3366 100644 --- a/packages/plugins/driver-memory/src/memory-matcher.ts +++ b/packages/plugins/driver-memory/src/memory-matcher.ts @@ -62,11 +62,6 @@ export function match(record: RecordType, filter: any): boolean { * Access nested properties via dot-notation (e.g. "user.name") */ export function getValueByPath(obj: any, path: string): any { - // Compatibility: Map _id to id if _id is missing - if (path === '_id' && obj._id === undefined && obj.id !== undefined) { - return obj.id; - } - if (!path.includes('.')) return obj[path]; return path.split('.').reduce((o, i) => (o ? o[i] : undefined), obj); } diff --git a/packages/rest/src/rest.test.ts b/packages/rest/src/rest.test.ts index e69d8c40b..12cc1586f 100644 --- a/packages/rest/src/rest.test.ts +++ b/packages/rest/src/rest.test.ts @@ -445,7 +445,7 @@ describe('RestServer', () => { protocol.getData.mockResolvedValue({ object: 'order_item', id: 'oi_123', - record: { _id: 'oi_123', name: 'Item 1' }, + record: { id: 'oi_123', name: 'Item 1' }, }); await getByIdRoute!.handler(mockReq, mockRes); @@ -478,7 +478,7 @@ describe('RestServer', () => { protocol.getData.mockResolvedValue({ object: 'contact', id: 'c_1', - record: { _id: 'c_1' }, + record: { id: 'c_1' }, }); await getByIdRoute!.handler(mockReq, mockRes); diff --git a/packages/runtime/src/http-dispatcher.test.ts b/packages/runtime/src/http-dispatcher.test.ts index 7ae993583..3e2466757 100644 --- a/packages/runtime/src/http-dispatcher.test.ts +++ b/packages/runtime/src/http-dispatcher.test.ts @@ -600,7 +600,7 @@ describe('HttpDispatcher', () => { describe('handleData', () => { it('should pass expand and select to broker for GET /data/:object/:id', async () => { - mockBroker.call.mockResolvedValue({ object: 'order_item', id: 'oi_1', record: { _id: 'oi_1' } }); + mockBroker.call.mockResolvedValue({ object: 'order_item', id: 'oi_1', record: { id: 'oi_1' } }); const result = await dispatcher.handleData( '/order_item/oi_1', 'GET', {}, diff --git a/packages/spec/DEVELOPMENT_PLAN.md b/packages/spec/DEVELOPMENT_PLAN.md index 2b331f267..13a21f471 100644 --- a/packages/spec/DEVELOPMENT_PLAN.md +++ b/packages/spec/DEVELOPMENT_PLAN.md @@ -232,7 +232,7 @@ export const MetricType = z.enum(['counter', 'gauge', 'histogram', 'summary']); ```typescript // Before (snake_case — violates camelCase property key rule) -_id: z.string(), +id: z.string(), created_by: z.string(), created_at: z.string().datetime(), updated_by: z.string(), diff --git a/packages/spec/docs/SYNC_ARCHITECTURE.md b/packages/spec/docs/SYNC_ARCHITECTURE.md index 36b5f217e..44298a91f 100644 --- a/packages/spec/docs/SYNC_ARCHITECTURE.md +++ b/packages/spec/docs/SYNC_ARCHITECTURE.md @@ -258,7 +258,7 @@ const sapConnector: Connector = { direction: 'bidirectional', schedule: '*/15 * * * *', // Every 15 minutes realtimeSync: true, - timestampField: 'last_modified_at', + timestampField: 'updated_at', conflictResolution: 'latest_wins', batchSize: 1000, deleteMode: 'soft_delete' diff --git a/packages/spec/src/data/dataset.zod.ts b/packages/spec/src/data/dataset.zod.ts index 4fd8a6ec6..1b98d4937 100644 --- a/packages/spec/src/data/dataset.zod.ts +++ b/packages/spec/src/data/dataset.zod.ts @@ -34,7 +34,7 @@ export const DatasetSchema = z.object({ * Idempotency Key (The "Upsert" Key) * The field used to check if a record already exists. * Best Practice: Use a natural key like 'code', 'slug', 'username' or 'external_id'. - * Standard: '_id' (internal ID) is rarely used for portable seed data. + * Standard: 'id' is rarely used for portable seed data — prefer natural keys. */ externalId: z.string().default('name').describe('Field match for uniqueness check'), diff --git a/packages/spec/src/data/mapping.test.ts b/packages/spec/src/data/mapping.test.ts index 9fb69bbfb..6e96bf417 100644 --- a/packages/spec/src/data/mapping.test.ts +++ b/packages/spec/src/data/mapping.test.ts @@ -95,7 +95,7 @@ describe('FieldMappingSchema', () => { params: { object: 'account', fromField: 'name', - toField: '_id', + toField: 'id', autoCreate: false } }); @@ -103,7 +103,7 @@ describe('FieldMappingSchema', () => { expect(mapping.transform).toBe('lookup'); expect(mapping.params?.object).toBe('account'); expect(mapping.params?.fromField).toBe('name'); - expect(mapping.params?.toField).toBe('_id'); + expect(mapping.params?.toField).toBe('id'); }); it('should accept map transform', () => { @@ -297,7 +297,7 @@ describe('MappingSchema', () => { fieldMapping: [{ source: 'email', target: 'email' }], extractQuery: { object: 'contact', - fields: ['_id', 'email', 'name'], + fields: ['id', 'email', 'name'], filters: ['status', '=', 'active'] } }); @@ -361,7 +361,7 @@ describe('MappingSchema', () => { params: { object: 'account', fromField: 'name', - toField: '_id' + toField: 'id' } }, { diff --git a/packages/spec/src/data/mapping.zod.ts b/packages/spec/src/data/mapping.zod.ts index 6268e3ede..fca399a23 100644 --- a/packages/spec/src/data/mapping.zod.ts +++ b/packages/spec/src/data/mapping.zod.ts @@ -39,7 +39,7 @@ export const FieldMappingSchema = z.object({ // Lookup object: z.string().optional(), // Lookup Object fromField: z.string().optional(), // Match on (e.g. "name") - toField: z.string().optional(), // Value to take (e.g. "_id") + toField: z.string().optional(), // Value to take (e.g. "id") autoCreate: z.boolean().optional(), // Create if missing // Map diff --git a/packages/spec/src/integration/connector/database.zod.ts b/packages/spec/src/integration/connector/database.zod.ts index b6ee2a165..bf103662b 100644 --- a/packages/spec/src/integration/connector/database.zod.ts +++ b/packages/spec/src/integration/connector/database.zod.ts @@ -277,7 +277,7 @@ export const mongoConnectorExample = { name: 'event', label: 'Event', tableName: 'events', - primaryKey: '_id', + primaryKey: 'id', enabled: true, }, ], diff --git a/packages/spec/src/qa/testing.test.ts b/packages/spec/src/qa/testing.test.ts index c84a81c71..1087d484e 100644 --- a/packages/spec/src/qa/testing.test.ts +++ b/packages/spec/src/qa/testing.test.ts @@ -115,8 +115,8 @@ describe('TestStepSchema', () => { const step = { ...minimalStep, description: 'Creates a new account record', - assertions: [{ field: 'body._id', operator: 'not_null', expectedValue: null }], - capture: { newId: 'body._id' }, + assertions: [{ field: 'body.id', operator: 'not_null', expectedValue: null }], + capture: { newId: 'body.id' }, }; expect(() => TestStepSchema.parse(step)).not.toThrow(); }); diff --git a/packages/spec/src/qa/testing.zod.ts b/packages/spec/src/qa/testing.zod.ts index 6084aae6d..f600216fa 100644 --- a/packages/spec/src/qa/testing.zod.ts +++ b/packages/spec/src/qa/testing.zod.ts @@ -54,7 +54,7 @@ export const TestStepSchema = z.object({ action: TestActionSchema.describe('The action to execute in this step'), assertions: z.array(TestAssertionSchema).optional().describe('Assertions to validate after the action completes'), // Capture outputs to variables for subsequent steps - capture: z.record(z.string(), z.string()).optional().describe('Map result fields to context variables: { "newId": "body._id" }') + capture: z.record(z.string(), z.string()).optional().describe('Map result fields to context variables: { "newId": "body.id" }') }).describe('A single step in a test scenario, consisting of an action and optional assertions'); export const TestScenarioSchema = z.object({