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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- **Studio system objects visibility** — Studio now auto-registers all system objects (sys_user,
sys_role, sys_audit_log, etc.) from plugin-auth, plugin-security, and plugin-audit at kernel
initialization. The sidebar "System" group dynamically lists all `isSystem=true` objects
with a collapsible "System Objects" section. A filter toggle on the Data group allows
showing/hiding system objects from the main object list.
- **ObjectSchema `namespace` property** — New optional `namespace` field on `ObjectSchema` for logical domain
classification (e.g., `'sys'`, `'crm'`). When set, `tableName` is auto-derived as `{namespace}_{name}` by
`ObjectSchema.create()` unless an explicit `tableName` is provided. This decouples the logical object name
Expand All @@ -24,6 +29,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
(`{namespace}_{name}` fallback when no explicit `tableName` is set).

### Changed
- **ObjectFilterSchema `includeSystem` default** — Changed from `false` to `true`. Studio
ObjectManager now includes system objects by default. Users can toggle visibility via the
Data group filter control.
- **System object naming convention** — All system objects now use `namespace: 'sys'` with short `name`
(e.g., `name: 'user'` instead of `name: 'sys_user'`). The `sys_` prefix is auto-derived via
`tableName` = `{namespace}_{name}`. File naming follows `sys-{name}.object.ts` pattern.
Expand Down
1 change: 1 addition & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,7 @@ Final polish and advanced features.
### 8.3 Studio IDE

- [x] Object Designer Protocol — field editor, relationship mapper, ER diagram, object manager schemas defined (`studio/object-designer.zod.ts`)
- [x] System Objects Visibility — Studio sidebar dynamically lists all system objects (sys_user, sys_role, sys_audit_log, etc.) under a "System Objects" collapsible group. Mock kernel auto-registers auth/security/audit system objects. Data group filter toggle to show/hide system objects.
- [ ] Object Designer Runtime — visual field editor with inline editing, drag-reorder, type-aware property panels
- [ ] Relationship Mapper — visual lookup/master-detail/tree creation with drag-to-connect
- [ ] ER Diagram — interactive entity-relationship diagram with force/hierarchy/grid layouts, minimap, zoom, export (PNG/SVG)
Expand Down
1 change: 1 addition & 0 deletions apps/studio/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
| No component-level tests | Regression risk | 🟡 Medium |
| Data refresh via `setTimeout` hack | Race conditions | 🟡 Medium |
| Sidebar groups hardcoded (not reading from plugins) | Plugin contributions ignored | 🟡 Medium |
| System objects not visible in Studio | Cannot manage users, roles, audit logs | ✅ Fixed |

### Protocol Coverage Gap

Expand Down
112 changes: 107 additions & 5 deletions apps/studio/src/components/app-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
AppWindow,
Layers,
Eye,
EyeOff,
FileCode,
Palette,
CheckSquare,
Expand All @@ -30,9 +31,10 @@ import {
Anchor,
UserCog,
ChevronRight,
Settings,
type LucideIcon,
} from "lucide-react"
import { useState, useEffect, useCallback } from "react"
import { useState, useEffect, useCallback, useMemo } from "react"
import { useClient } from '@objectstack/client-react';
import type { InstalledPackage } from '@objectstack/spec/kernel';

Expand Down Expand Up @@ -128,6 +130,15 @@ const PROTOCOL_GROUPS: ProtocolGroup[] = [
/** Types that are internal / should be hidden from the sidebar */
const HIDDEN_TYPES = new Set(['plugin', 'plugins', 'kind', 'app', 'apps', 'package']);

/** System object name prefix — objects with this prefix are grouped under "System" */
const SYSTEM_OBJECT_PREFIX = 'sys_';

/** Check if an object item is a system object */
function isSystemObject(item: any): boolean {
const name = item.name || item.id || '';
return item.isSystem === true || name.startsWith(SYSTEM_OBJECT_PREFIX);
}

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SYSTEM_OBJECT_PREFIX is set to sys_, but object names coming from the registry are stored as FQNs like sys__user (see SchemaRegistry.registerObject storing name: fqn). This means the name.startsWith(SYSTEM_OBJECT_PREFIX) branch in isSystemObject() will never match for current system objects; rely on namespace/FQN parsing (e.g., parseFQN(name).namespace === 'sys') or update the prefix to sys__ to keep the fallback accurate.

Copilot uses AI. Check for mistakes.

/** Icon mapping for package types */
const PKG_TYPE_ICONS: Record<string, LucideIcon> = {
app: AppWindow, plugin: Layers, driver: Database, server: Globe,
Expand Down Expand Up @@ -161,6 +172,9 @@ export function AppSidebar({
// Track which metadata *types* are expanded (show individual items)
const [expandedTypes, setExpandedTypes] = useState<Set<string>>(new Set(['object', 'objects']));

// Toggle to show/hide system objects in the Data protocol group
const [showSystemInData, setShowSystemInData] = useState(true);

const toggleTypeExpanded = (type: string) => {
setExpandedTypes(prev => {
const next = new Set(prev);
Expand Down Expand Up @@ -216,12 +230,35 @@ export function AppSidebar({
label.toLowerCase().includes(searchQuery.toLowerCase()) ||
name.toLowerCase().includes(searchQuery.toLowerCase());

// Extract system objects from loaded metadata (object/objects types)
const systemObjects = useMemo(() => {
const objectTypes = ['object', 'objects'];
const sysItems: any[] = [];
for (const type of objectTypes) {
const items = metaItems[type] || [];
sysItems.push(...items.filter(isSystemObject));
}
return sysItems;
}, [metaItems]);

// Filter system objects out of the Data protocol group when toggled off
const filteredMetaItems = useMemo(() => {
if (showSystemInData) return metaItems;
const result = { ...metaItems };
for (const type of ['object', 'objects']) {
if (result[type]) {
result[type] = result[type].filter((item: any) => !isSystemObject(item));
}
}
return result;
}, [metaItems, showSystemInData]);

// Compute visible groups: only show groups that have at least one type with items
const visibleGroups = PROTOCOL_GROUPS.map(group => {
const visibleTypes = group.types.filter(t =>
metaTypes.includes(t) && !HIDDEN_TYPES.has(t) && (metaItems[t]?.length ?? 0) > 0
metaTypes.includes(t) && !HIDDEN_TYPES.has(t) && (filteredMetaItems[t]?.length ?? 0) > 0
);
const totalItems = visibleTypes.reduce((sum, t) => sum + (metaItems[t]?.length ?? 0), 0);
const totalItems = visibleTypes.reduce((sum, t) => sum + (filteredMetaItems[t]?.length ?? 0), 0);
return { ...group, visibleTypes, totalItems };
}).filter(g => g.totalItems > 0);

Expand Down Expand Up @@ -328,11 +365,23 @@ export function AppSidebar({
<group.icon className="mr-1.5 h-3.5 w-3.5" />
<span className="flex-1 min-w-0 truncate">{group.label}</span>
<span className="shrink-0 text-xs tabular-nums text-sidebar-foreground/50">{group.totalItems}</span>
{/* System objects filter toggle for Data group */}
{group.key === 'data' && systemObjects.length > 0 && (
<button
title={showSystemInData ? 'Hide system objects' : 'Show system objects'}
onClick={(e) => { e.stopPropagation(); setShowSystemInData(!showSystemInData); }}
className="ml-1 shrink-0 rounded p-0.5 text-sidebar-foreground/50 hover:text-sidebar-foreground hover:bg-sidebar-accent transition-colors"
>
{showSystemInData
? <Eye className="h-3 w-3" />
: <EyeOff className="h-3 w-3" />}
</button>
Comment on lines +379 to +389

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The system-object filter toggle button relies on title only. For accessibility, add an aria-label (and consider type="button" to avoid accidental form submission if this component is ever rendered inside a <form>).

Copilot uses AI. Check for mistakes.
)}
</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{group.visibleTypes.map(type => {
const items = metaItems[type] || [];
const items = filteredMetaItems[type] || [];
const TypeIcon = getTypeIcon(type);
const typeLabel = getTypeLabel(type);
const isObjectType = type === 'object' || type === 'objects';
Expand Down Expand Up @@ -408,9 +457,62 @@ export function AppSidebar({

{/* ── System ── */}
<SidebarGroup>
<SidebarGroupLabel>System</SidebarGroupLabel>
<SidebarGroupLabel>
<Settings className="mr-1.5 h-3.5 w-3.5" />
<span className="flex-1 min-w-0 truncate">System</span>
{systemObjects.length > 0 && (
<span className="shrink-0 text-xs tabular-nums text-sidebar-foreground/50">{systemObjects.length}</span>
)}
</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{/* Dynamic system objects */}
{systemObjects.length > 0 && (
<Collapsible
open={expandedTypes.has('_system_objects') || !!searchQuery}
onOpenChange={() => toggleTypeExpanded('_system_objects')}

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Collapsible is controlled via open={expandedTypes.has('_system_objects') || !!searchQuery}, but onOpenChange ignores the boolean argument and always toggles. This can leave expandedTypes out of sync (especially when searchQuery forces open to true) and makes the expansion state unpredictable after searching. Handle onOpenChange(open) by explicitly adding/removing _system_objects based on open instead of toggling blindly.

Suggested change
onOpenChange={() => toggleTypeExpanded('_system_objects')}
onOpenChange={(open) => {
const isExpanded = expandedTypes.has('_system_objects');
if (open && !isExpanded) {
toggleTypeExpanded('_system_objects');
}
if (!open && isExpanded) {
toggleTypeExpanded('_system_objects');
}
}}

Copilot uses AI. Check for mistakes.
asChild
>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton tooltip={`System Objects (${systemObjects.length})`}>
<Database className="h-4 w-4" />
<span className="flex-1 min-w-0 truncate">System Objects</span>
<span className="shrink-0 text-xs tabular-nums text-muted-foreground">{systemObjects.length}</span>
<ChevronRight className={`h-3.5 w-3.5 shrink-0 text-muted-foreground transition-transform duration-200 ${(expandedTypes.has('_system_objects') || !!searchQuery) ? 'rotate-90' : ''}`} />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{systemObjects
.filter((item: any) => matchesSearch(item.label || item.name || '', item.name || ''))
.map((item: any) => {
const itemName = item.name || item.id || 'unknown';
const itemLabel = item.label || item.name || 'Untitled';

return (
<SidebarMenuSubItem key={itemName}>
<SidebarMenuSubButton
isActive={selectedObject === itemName}
onClick={() => onSelectObject(itemName)}
>
<span className="truncate">
{itemName.startsWith(SYSTEM_OBJECT_PREFIX) && (
<span className="text-muted-foreground font-mono text-xs">{SYSTEM_OBJECT_PREFIX}</span>
)}
{itemLabel}
Comment on lines +514 to +518

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the System Objects list, the prefix hint uses SYSTEM_OBJECT_PREFIX (sys_) and checks itemName.startsWith(SYSTEM_OBJECT_PREFIX), but object names are FQNs like sys__user. As a result, the prefix label never renders for actual system objects. Consider rendering the namespace (e.g., sys__) or deriving a consistent display from parseFQN(itemName) instead of hardcoding sys_.

Copilot uses AI. Check for mistakes.
</span>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
);
})}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
)}

{/* Static system items */}
<SidebarMenuItem>
<SidebarMenuButton
tooltip="API Console"
Expand Down
38 changes: 38 additions & 0 deletions apps/studio/src/mocks/createKernel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,29 @@ import { ObjectQLPlugin, SchemaRegistry } from '@objectstack/objectql';
import { InMemoryDriver } from '@objectstack/driver-memory';
import { MSWPlugin } from '@objectstack/plugin-msw';

// System object definitions — resolved via Vite aliases to plugin source (no runtime deps)
import {
SysUser, SysSession, SysAccount, SysVerification,
SysOrganization, SysMember, SysInvitation,
SysTeam, SysTeamMember,
SysApiKey, SysTwoFactor,
} from '@objectstack/plugin-auth/objects';
import { SysRole, SysPermissionSet } from '@objectstack/plugin-security/objects';
import { SysAuditLog } from '@objectstack/plugin-audit/objects';

/** All system objects from auth, security, and audit plugins */
const SYSTEM_OBJECTS = [
// Auth
SysUser, SysSession, SysAccount, SysVerification,
SysOrganization, SysMember, SysInvitation,
SysTeam, SysTeamMember,
SysApiKey, SysTwoFactor,
// Security
SysRole, SysPermissionSet,
// Audit
SysAuditLog,
];

export interface KernelOptions {
appConfigs?: any[]; // Multiple app configs
appConfig?: any; // Legacy single app config (backward compat)
Expand All @@ -30,6 +53,21 @@ export async function createKernel(options: KernelOptions) {
// Register the driver
await kernel.use(new DriverPlugin(driver, 'memory'));

// Register system objects (auth, security, audit) as a built-in system package
const systemConfig = {
name: 'system',
manifest: {
id: 'com.objectstack.system',
name: 'System',
version: '1.0.0',
type: 'plugin',
namespace: 'sys',
},
objects: SYSTEM_OBJECTS,
};
console.log('[KernelFactory] Loading system objects:', SYSTEM_OBJECTS.length);
await kernel.use(new AppPlugin(systemConfig));

// Load all app configs as plugins (handles object registration & seeding)
for (const appConfig of allConfigs) {
console.log('[KernelFactory] Loading app:', appConfig.manifest?.id || appConfig.name || 'unknown');
Expand Down
4 changes: 4 additions & 0 deletions apps/studio/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export default defineConfig({
'react': path.resolve(__dirname, './node_modules/react'),
'react-dom': path.resolve(__dirname, './node_modules/react-dom'),
'@': path.resolve(__dirname, './src'),
// System object definitions — resolve to plugin source (no runtime deps)
'@objectstack/plugin-auth/objects': path.resolve(__dirname, '../../packages/plugins/plugin-auth/src/objects/index.ts'),
'@objectstack/plugin-security/objects': path.resolve(__dirname, '../../packages/plugins/plugin-security/src/objects/index.ts'),
'@objectstack/plugin-audit/objects': path.resolve(__dirname, '../../packages/plugins/plugin-audit/src/objects/index.ts'),
'node:fs/promises': path.resolve(__dirname, './mocks/node-polyfills.ts'),
'node:fs': path.resolve(__dirname, './mocks/node-polyfills.ts'),
'node:events': path.resolve(__dirname, './mocks/node-polyfills.ts'),
Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/studio/object-designer.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ Default view when entering the Object Designer
| :--- | :--- | :--- | :--- |
| **package** | `string` | optional | Filter by owning package |
| **tags** | `string[]` | optional | Filter by object tags |
| **includeSystem** | `boolean` | ✅ | Include system-level objects |
| **includeSystem** | `boolean` | ✅ | Include system-level objects (default: `true`) |

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs now state includeSystem defaults to true, but ObjectDesignerConfigSchema currently hardcodes objectManager.defaultFilter.includeSystem: false in its .default({ ... }) block (later in object-designer.zod.ts). Either update the code default or adjust this doc so it reflects the actual effective default.

Suggested change
| **includeSystem** | `boolean` || Include system-level objects (default: `true`) |
| **includeSystem** | `boolean` || Include system-level objects (default: `false`) |

Copilot uses AI. Check for mistakes.
| **includeAbstract** | `boolean` | ✅ | Include abstract base objects |
| **hasFieldType** | `string` | optional | Filter to objects containing a specific field type |
| **hasRelationships** | `boolean` | optional | Filter to objects with lookup/master_detail fields |
Expand Down
2 changes: 1 addition & 1 deletion packages/spec/src/studio/object-designer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ describe('ObjectSortFieldSchema', () => {
describe('ObjectFilterSchema', () => {
it('should accept empty object with defaults', () => {
const result = ObjectFilterSchema.parse({});
expect(result.includeSystem).toBe(false);
expect(result.includeSystem).toBe(true);
expect(result.includeAbstract).toBe(false);
expect(result.package).toBeUndefined();
expect(result.tags).toBeUndefined();
Comment on lines 308 to 313

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests update ObjectFilterSchema's default includeSystem, but there is no assertion that the top-level ObjectDesignerConfigSchema default (result.objectManager.defaultFilter.includeSystem) matches the new behavior. Adding that assertion would prevent regressions like a nested .default({ ... }) overriding the schema default.

Copilot uses AI. Check for mistakes.
Expand Down
4 changes: 2 additions & 2 deletions packages/spec/src/studio/object-designer.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ export const ObjectFilterSchema = z.object({
tags: z.array(z.string()).optional().describe('Filter by object tags'),

/** Show system objects */
includeSystem: z.boolean().default(false).describe('Include system-level objects'),
includeSystem: z.boolean().default(true).describe('Include system-level objects'),

/** Show abstract objects */
includeAbstract: z.boolean().default(false).describe('Include abstract base objects'),
Expand Down Expand Up @@ -361,7 +361,7 @@ export const ObjectManagerConfigSchema = z.object({

/** Default filters */
defaultFilter: ObjectFilterSchema.default({
includeSystem: false,
includeSystem: true,
includeAbstract: false,
}).describe('Default filter configuration'),

Comment on lines +364 to 367

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ObjectManagerConfigSchema.defaultFilter now defaults includeSystem: true, but ObjectDesignerConfigSchema later sets objectManager.defaultFilter.includeSystem: false in its own .default({ ... }) block (around line ~550 in this file). That higher-level default will override the new schema default and keep system objects hidden in practice. Align the ObjectDesignerConfigSchema default with the new includeSystem: true behavior (or remove the redundant nested default to avoid future drift).

Suggested change
includeSystem: true,
includeAbstract: false,
}).describe('Default filter configuration'),
includeAbstract: false,
}).describe('Default filter configuration'),

Copilot uses AI. Check for mistakes.
Expand Down
Loading