Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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);
});
});
119 changes: 119 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,119 @@
// 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.textarea({
label: 'Metadata',
required: false,
Comment thread
hotlong marked this conversation as resolved.
Outdated
}),
},

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