Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- **`@objectstack/service-realtime` — `sys_presence` System Object** — Registers the
`sys_presence` system object in the `service-realtime` package as the canonical Presence
domain object. Fields align with the `PresenceStateSchema` protocol definition
(`user_id`, `session_id`, `status`, `last_seen`, `current_location`, `device`,
`custom_status`, `metadata`). `RealtimeServicePlugin` now auto-registers the object
via the `app.com.objectstack.service.realtime` service convention. Added
`SystemObjectName.PRESENCE` constant (`'sys_presence'`) to `@objectstack/spec/system`.
- **`@objectstack/service-ai` — Data Chatbot: Tool Call Loop & Agent Runtime** — Implements
an Airtable Copilot-style data conversation Chatbot with full-stack support:
- `AIService.chatWithTools()` — automatic multi-round LLM ↔ tool call loop with
Expand Down
1 change: 1 addition & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ Objects now declare `namespace: 'sys'` and a short `name` (e.g., `name: 'user'`)
| `SystemObjectName.PERMISSION_SET` | `sys_permission_set` | plugin-security | Security: permission set grouping |
| `SystemObjectName.AUDIT_LOG` | `sys_audit_log` | plugin-audit | Audit: immutable audit trail |
| `SystemObjectName.METADATA` | `sys_metadata` | metadata | System metadata storage |
| `SystemObjectName.PRESENCE` | `sys_presence` | service-realtime | Realtime: user presence state |

**Object Definition Convention:**
- File naming: `sys-{name}.object.ts` (e.g., `sys-user.object.ts`, `sys-role.object.ts`)
Expand Down
1 change: 1 addition & 0 deletions packages/services/service-realtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export { RealtimeServicePlugin } from './realtime-service-plugin.js';
export type { RealtimeServicePluginOptions } from './realtime-service-plugin.js';
export { InMemoryRealtimeAdapter } from './in-memory-realtime-adapter.js';
export type { InMemoryRealtimeAdapterOptions } from './in-memory-realtime-adapter.js';
export { SysPresence } from './objects/index.js';
9 changes: 9 additions & 0 deletions packages/services/service-realtime/src/objects/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

/**
* Realtime Service — System Object Definitions (sys namespace)
*
* Canonical ObjectSchema definitions for realtime-related system objects.
*/

export { SysPresence } from './sys-presence.object.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

import { describe, it, expect } from 'vitest';
import { SysPresence } from './sys-presence.object';

describe('SysPresence object definition', () => {
it('should have correct namespace and name', () => {
expect(SysPresence.namespace).toBe('sys');
expect(SysPresence.name).toBe('presence');
});

it('should auto-derive tableName as sys_presence', () => {
expect(SysPresence.tableName).toBe('sys_presence');
});

it('should be a system object', () => {
expect(SysPresence.isSystem).toBe(true);
});

it('should have label and pluralLabel', () => {
expect(SysPresence.label).toBe('Presence');
expect(SysPresence.pluralLabel).toBe('Presences');
});

it('should define all presence protocol fields', () => {
const fieldKeys = Object.keys(SysPresence.fields);
expect(fieldKeys).toContain('id');
expect(fieldKeys).toContain('created_at');
expect(fieldKeys).toContain('updated_at');
expect(fieldKeys).toContain('user_id');
expect(fieldKeys).toContain('session_id');
expect(fieldKeys).toContain('status');
expect(fieldKeys).toContain('last_seen');
expect(fieldKeys).toContain('current_location');
expect(fieldKeys).toContain('device');
expect(fieldKeys).toContain('custom_status');
expect(fieldKeys).toContain('metadata');
});

it('should have status field with correct options', () => {
const statusField = SysPresence.fields.status;
expect(statusField.type).toBe('select');
expect(statusField.options).toEqual([
{ value: 'online', label: 'Online' },
{ value: 'away', label: 'Away' },
{ value: 'busy', label: 'Busy' },
{ value: 'offline', label: 'Offline' },
]);
});

it('should have device field with correct options', () => {
const deviceField = SysPresence.fields.device;
expect(deviceField.type).toBe('select');
expect(deviceField.options).toEqual([
{ value: 'desktop', label: 'Desktop' },
{ value: 'mobile', label: 'Mobile' },
{ value: 'tablet', label: 'Tablet' },
{ value: 'other', label: 'Other' },
]);
});

it('should have indexes on user_id, session_id, and status', () => {
expect(SysPresence.indexes).toEqual([
{ fields: ['user_id'], unique: false, type: 'btree' },
{ fields: ['session_id'], unique: true, type: 'btree' },
{ fields: ['status'], unique: false, type: 'btree' },
]);
});

it('should have API enabled', () => {
expect(SysPresence.enable?.apiEnabled).toBe(true);
});
});
120 changes: 120 additions & 0 deletions packages/services/service-realtime/src/objects/sys-presence.object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

import { ObjectSchema, Field } from '@objectstack/spec/data';

/**
* sys_presence — System Presence Object
*
* Tracks real-time user presence and activity across the platform.
* Fields align with the PresenceStateSchema protocol definition
* from `@objectstack/spec/api` (websocket.zod.ts).
*
* Owned by `service-realtime` as the canonical Presence domain object.
*
* @namespace sys
* @see PresenceStateSchema in packages/spec/src/api/websocket.zod.ts
*/
export const SysPresence = ObjectSchema.create({
namespace: 'sys',
name: 'presence',
label: 'Presence',
pluralLabel: 'Presences',
icon: 'wifi',
isSystem: true,
description: 'Real-time user presence and activity tracking',
titleFormat: '{user_id} ({status})',
compactLayout: ['user_id', 'status', 'last_seen'],

fields: {
id: Field.text({
label: 'Presence ID',
required: true,
readonly: true,
}),

created_at: Field.datetime({
label: 'Created At',
defaultValue: 'NOW()',
readonly: true,
}),

updated_at: Field.datetime({
label: 'Updated At',
defaultValue: 'NOW()',
readonly: true,
}),

user_id: Field.text({
label: 'User ID',
required: true,
searchable: true,
}),

session_id: Field.text({
label: 'Session ID',
required: true,
}),

status: Field.select({
label: 'Status',
required: true,
defaultValue: 'online',
options: [
{ value: 'online', label: 'Online' },
{ value: 'away', label: 'Away' },
{ value: 'busy', label: 'Busy' },
{ value: 'offline', label: 'Offline' },
],
}),

last_seen: Field.datetime({
label: 'Last Seen',
required: true,
defaultValue: 'NOW()',
}),

current_location: Field.text({
label: 'Current Location',
required: false,
maxLength: 500,
}),

device: Field.select({
label: 'Device',
required: false,
options: [
{ value: 'desktop', label: 'Desktop' },
{ value: 'mobile', label: 'Mobile' },
{ value: 'tablet', label: 'Tablet' },
{ value: 'other', label: 'Other' },
],
}),

custom_status: Field.text({
label: 'Custom Status',
required: false,
maxLength: 255,
}),

metadata: Field.json({
label: 'Metadata',
required: false,
description: 'Arbitrary JSON metadata associated with the presence state (matches PresenceStateSchema.metadata).',
}),
},

indexes: [
{ fields: ['user_id'], unique: false },
{ fields: ['session_id'], unique: true },
{ fields: ['status'], unique: false },
],

enable: {
trackHistory: false,
searchable: false,
apiEnabled: true,
apiMethods: ['get', 'list', 'create', 'update', 'delete'],
trash: false,
mru: false,
},
});
12 changes: 12 additions & 0 deletions packages/services/service-realtime/src/realtime-service-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import type { Plugin, PluginContext } from '@objectstack/core';
import { InMemoryRealtimeAdapter } from './in-memory-realtime-adapter.js';
import type { InMemoryRealtimeAdapterOptions } from './in-memory-realtime-adapter.js';
import { SysPresence } from './objects/index.js';

/**
* Configuration options for the RealtimeServicePlugin.
Expand Down Expand Up @@ -49,6 +50,17 @@ export class RealtimeServicePlugin implements Plugin {
async init(ctx: PluginContext): Promise<void> {
const realtime = new InMemoryRealtimeAdapter(this.options.memory);
ctx.registerService('realtime', realtime);

// Register realtime system objects so ObjectQLPlugin auto-discovers them
ctx.registerService('app.com.objectstack.service.realtime', {
id: 'com.objectstack.service.realtime',
name: 'Realtime Service',
version: '1.0.0',
type: 'plugin',
namespace: 'sys',
objects: [SysPresence],
});

ctx.logger.info('RealtimeServicePlugin: registered in-memory realtime adapter');
}
}
3 changes: 3 additions & 0 deletions packages/spec/src/system/constants/system-names.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ describe('SystemObjectName', () => {
expect(SystemObjectName.PERMISSION_SET).toBe('sys_permission_set');
expect(SystemObjectName.AUDIT_LOG).toBe('sys_audit_log');
expect(SystemObjectName.METADATA).toBe('sys_metadata');
expect(SystemObjectName.PRESENCE).toBe('sys_presence');
});

it('should be readonly (const assertion)', () => {
Expand All @@ -37,6 +38,7 @@ describe('SystemObjectName', () => {
expect(names).toContain('sys_team_member');
expect(names).toContain('sys_role');
expect(names).toContain('sys_audit_log');
expect(names).toContain('sys_presence');
});

it('should have all expected keys', () => {
Expand All @@ -56,6 +58,7 @@ describe('SystemObjectName', () => {
expect(keys).toContain('PERMISSION_SET');
expect(keys).toContain('AUDIT_LOG');
expect(keys).toContain('METADATA');
expect(keys).toContain('PRESENCE');
});
});

Expand Down
2 changes: 2 additions & 0 deletions packages/spec/src/system/constants/system-names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ export const SystemObjectName = {
AUDIT_LOG: 'sys_audit_log',
/** System metadata storage */
METADATA: 'sys_metadata',
/** Realtime: user presence state */
PRESENCE: 'sys_presence',
} as const;

/** Union type of all system object names */
Expand Down
Loading