Skip to content

Commit 5ac906d

Browse files
authored
Merge pull request #912 from objectstack-ai/copilot/upgrade-object-architecture-naming
2 parents 69be3c0 + 615b762 commit 5ac906d

34 files changed

Lines changed: 1604 additions & 394 deletions

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [Unreleased]
99

1010
### Added
11+
- **ObjectSchema `namespace` property** — New optional `namespace` field on `ObjectSchema` for logical domain
12+
classification (e.g., `'sys'`, `'crm'`). When set, `tableName` is auto-derived as `{namespace}_{name}` by
13+
`ObjectSchema.create()` unless an explicit `tableName` is provided. This decouples the logical object name
14+
from the physical table name and enables unified routing, permissions, and discovery by domain.
15+
- **SystemObjectName constants** — Extended with all system objects: `ORGANIZATION`, `MEMBER`, `INVITATION`,
16+
`TEAM`, `TEAM_MEMBER`, `API_KEY`, `TWO_FACTOR`, `ROLE`, `PERMISSION_SET`, `AUDIT_LOG` (in addition to
17+
existing `USER`, `SESSION`, `ACCOUNT`, `VERIFICATION`, `METADATA`).
18+
- **plugin-auth system objects** — Added `SysOrganization`, `SysMember`, `SysInvitation`, `SysTeam`,
19+
`SysTeamMember`, `SysApiKey`, `SysTwoFactor` object definitions with `namespace: 'sys'`. Existing objects
20+
(`SysUser`, `SysSession`, `SysAccount`, `SysVerification`) migrated to use namespace convention.
21+
- **plugin-security system objects** — Added `SysRole` and `SysPermissionSet` object definitions.
22+
- **plugin-audit** — New plugin package with `SysAuditLog` immutable audit trail object definition.
23+
- **StorageNameMapping.resolveTableName()** — Now supports namespace-aware auto-derivation
24+
(`{namespace}_{name}` fallback when no explicit `tableName` is set).
25+
26+
### Changed
27+
- **System object naming convention** — All system objects now use `namespace: 'sys'` with short `name`
28+
(e.g., `name: 'user'` instead of `name: 'sys_user'`). The `sys_` prefix is auto-derived via
29+
`tableName` = `{namespace}_{name}`. File naming follows `sys-{name}.object.ts` pattern.
30+
- **plugin-auth object exports** — New canonical exports use `Sys*` prefix (e.g., `SysUser`, `SysSession`).
31+
Legacy `Auth*` exports are preserved as deprecated re-exports for backward compatibility.
32+
- **sys_metadata object** — Migrated to `namespace: 'sys'`, `name: 'metadata'` convention (tableName
33+
auto-derived as `sys_metadata`).
1134
- **Locale code fallback** — New `resolveLocale()` helper in `@objectstack/core` that resolves
1235
locale codes through a 4-step fallback chain: exact match → case-insensitive match
1336
(`zh-cn``zh-CN`) → base language match (`zh-CN``zh`) → variant expansion

ROADMAP.md

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -307,24 +307,50 @@ The following renames are planned for packages that implement core service contr
307307
### System Object Naming Convention (`sys_` Prefix)
308308

309309
> **Adopted:** 2026-02-19
310+
> **Updated:** 2026-03-11 — Namespace-based architecture with auto-derivation
310311
> **Scope:** All system kernel objects in `SystemObjectName` constants.
311312
312-
All system kernel objects use the `sys_` prefix to clearly distinguish platform-internal objects from
313+
All system kernel objects use the `sys` namespace to clearly distinguish platform-internal objects from
313314
business/custom objects, aligning with industry best practices (e.g., ServiceNow `sys_user`, `sys_audit`).
314315

315-
| Constant Key | Protocol Name | Description |
316-
|:---|:---|:---|
317-
| `SystemObjectName.USER` | `sys_user` | Authentication: user identity |
318-
| `SystemObjectName.SESSION` | `sys_session` | Authentication: active session |
319-
| `SystemObjectName.ACCOUNT` | `sys_account` | Authentication: OAuth / credential account |
320-
| `SystemObjectName.VERIFICATION` | `sys_verification` | Authentication: email / phone verification |
321-
| `SystemObjectName.METADATA` | `sys_metadata` | System metadata storage |
316+
Objects now declare `namespace: 'sys'` and a short `name` (e.g., `name: 'user'`). The physical table name
317+
`sys_user` is auto-derived as `{namespace}_{name}` by `ObjectSchema.create()`.
318+
319+
| Constant Key | Protocol Name | Plugin | Description |
320+
|:---|:---|:---|:---|
321+
| `SystemObjectName.USER` | `sys_user` | plugin-auth | Authentication: user identity |
322+
| `SystemObjectName.SESSION` | `sys_session` | plugin-auth | Authentication: active session |
323+
| `SystemObjectName.ACCOUNT` | `sys_account` | plugin-auth | Authentication: OAuth / credential account |
324+
| `SystemObjectName.VERIFICATION` | `sys_verification` | plugin-auth | Authentication: email / phone verification |
325+
| `SystemObjectName.ORGANIZATION` | `sys_organization` | plugin-auth | Authentication: organization (multi-org) |
326+
| `SystemObjectName.MEMBER` | `sys_member` | plugin-auth | Authentication: organization member |
327+
| `SystemObjectName.INVITATION` | `sys_invitation` | plugin-auth | Authentication: organization invitation |
328+
| `SystemObjectName.TEAM` | `sys_team` | plugin-auth | Authentication: team within an organization |
329+
| `SystemObjectName.TEAM_MEMBER` | `sys_team_member` | plugin-auth | Authentication: team membership |
330+
| `SystemObjectName.API_KEY` | `sys_api_key` | plugin-auth | Authentication: API key for programmatic access |
331+
| `SystemObjectName.TWO_FACTOR` | `sys_two_factor` | plugin-auth | Authentication: two-factor credentials |
332+
| `SystemObjectName.ROLE` | `sys_role` | plugin-security | Security: RBAC role definition |
333+
| `SystemObjectName.PERMISSION_SET` | `sys_permission_set` | plugin-security | Security: permission set grouping |
334+
| `SystemObjectName.AUDIT_LOG` | `sys_audit_log` | plugin-audit | Audit: immutable audit trail |
335+
| `SystemObjectName.METADATA` | `sys_metadata` | metadata | System metadata storage |
336+
337+
**Object Definition Convention:**
338+
- File naming: `sys-{name}.object.ts` (e.g., `sys-user.object.ts`, `sys-role.object.ts`)
339+
- Export naming: `Sys{PascalCase}` (e.g., `SysUser`, `SysRole`, `SysAuditLog`)
340+
- Object schema: `namespace: 'sys'`, `name: '{short_name}'` (no `sys_` prefix in name)
341+
- Table derivation: `tableName` auto-derived as `sys_{name}` unless explicitly overridden
322342

323343
**Rationale:**
324344
- Prevents naming collisions between system objects and business objects (e.g., a CRM `account` vs. `sys_account`)
325345
- Aligns with ServiceNow and similar platforms that use `sys_` as a reserved namespace
326346
- ObjectStack already uses namespace + FQN for business object isolation; the `sys_` prefix completes the picture for kernel-level objects
327347
- Physical storage table names can differ via `ObjectSchema.tableName` + `StorageNameMapping.resolveTableName()` for backward compatibility
348+
- Namespace-based auto-derivation eliminates manual `tableName` boilerplate and ensures consistency
349+
350+
**Plugin Architecture:**
351+
- Each plugin (plugin-auth, plugin-security, plugin-audit) owns and registers its own `sys` namespace objects
352+
- Plugins remain decoupled and optional — consumers aggregate all `sys` objects at runtime
353+
- Object definitions follow the ObjectSchema protocol with `isSystem: true`
328354

329355
**Migration (v3.x → v4.0):**
330356
- v3.x: The `SystemObjectName` constants now emit `sys_`-prefixed names. Implementations using `StorageNameMapping.resolveTableName()` can set `tableName` to preserve legacy physical table names during the transition.
@@ -333,7 +359,7 @@ business/custom objects, aligning with industry best practices (e.g., ServiceNow
333359
- v3.x: **Bug fix**`AuthManager.createDatabaseConfig()` now wraps the ObjectQL adapter as a `DBAdapterInstance` factory function (`(options) => DBAdapter`). Previously the raw adapter object was passed, which fell through to the Kysely adapter path and failed silently. `AuthManager.handleRequest()` and `AuthPlugin.registerAuthRoutes()` now inspect `response.status >= 500` and log the error body, since better-auth catches internal errors and returns 500 Responses without throwing.
334360
- v3.x: **Bug fix**`AuthPlugin` now defers HTTP route registration to a `kernel:ready` hook instead of doing it synchronously in `start()`. This makes the plugin resilient to plugin loading order — the `http-server` service is guaranteed to be available after all plugins complete their init/start phases. The CLI `serve` command also registers `HonoServerPlugin` before config plugins (with duplicate detection) for the same reason.
335361
- v3.x: **Bug fix** — Studio `useApiDiscovery` hook no longer hardcodes auth endpoints as `/api/auth/...`. The `discover()` callback now fetches `/api/v1/discovery` and reads `routes.auth` to dynamically construct auth endpoint paths (falling back to `/api/v1/auth`). The session endpoint is corrected from `/session` to `/get-session` to align with better-auth's `AuthEndpointPaths.getSession`.
336-
- v4.0: Legacy un-prefixed aliases will be fully removed.
362+
- v4.0: Legacy un-prefixed aliases and `Auth*` export names will be fully removed.
337363

338364
---
339365

packages/metadata/src/objects/sys-metadata.object.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import { ObjectSchema, Field } from '@objectstack/spec/data';
1515
* @see MetadataRecordSchema in metadata-persistence.zod.ts
1616
*/
1717
export const SysMetadataObject = ObjectSchema.create({
18-
name: 'sys_metadata',
18+
namespace: 'sys',
19+
name: 'metadata',
1920
label: 'System Metadata',
2021
pluralLabel: 'System Metadata',
2122
icon: 'settings',
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"name": "@objectstack/plugin-audit",
3+
"version": "3.2.5",
4+
"license": "Apache-2.0",
5+
"description": "Audit Plugin for ObjectStack — System audit log object and audit trail",
6+
"main": "dist/index.js",
7+
"types": "dist/index.d.ts",
8+
"exports": {
9+
".": {
10+
"types": "./dist/index.d.ts",
11+
"import": "./dist/index.mjs",
12+
"require": "./dist/index.js"
13+
}
14+
},
15+
"scripts": {
16+
"build": "tsup --config ../../../tsup.config.ts",
17+
"test": "vitest run"
18+
},
19+
"dependencies": {
20+
"@objectstack/spec": "workspace:*"
21+
},
22+
"devDependencies": {
23+
"@types/node": "^25.3.5",
24+
"typescript": "^5.0.0",
25+
"vitest": "^4.0.18"
26+
}
27+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* @objectstack/plugin-audit
5+
*
6+
* Audit Plugin for ObjectStack
7+
* Provides the sys_audit_log system object definition for immutable audit trails.
8+
*/
9+
10+
// System Object Definitions (sys namespace)
11+
export { SysAuditLog } from './objects/index.js';
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Audit Plugin — System Object Definitions (sys namespace)
5+
*
6+
* Canonical ObjectSchema definitions for audit-related system objects.
7+
*/
8+
9+
export { SysAuditLog } from './sys-audit-log.object.js';
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { ObjectSchema, Field } from '@objectstack/spec/data';
4+
5+
/**
6+
* sys_audit_log — System Audit Log Object
7+
*
8+
* Immutable audit trail for all significant platform events.
9+
* Records who did what, when, and the before/after state.
10+
*
11+
* @namespace sys
12+
*/
13+
export const SysAuditLog = ObjectSchema.create({
14+
namespace: 'sys',
15+
name: 'audit_log',
16+
label: 'Audit Log',
17+
pluralLabel: 'Audit Logs',
18+
icon: 'scroll-text',
19+
isSystem: true,
20+
description: 'Immutable audit trail for platform events',
21+
titleFormat: '{action} on {object_name} by {user_id}',
22+
compactLayout: ['action', 'object_name', 'user_id', 'created_at'],
23+
24+
fields: {
25+
id: Field.text({
26+
label: 'Audit Log ID',
27+
required: true,
28+
readonly: true,
29+
}),
30+
31+
created_at: Field.datetime({
32+
label: 'Timestamp',
33+
required: true,
34+
defaultValue: 'NOW()',
35+
readonly: true,
36+
}),
37+
38+
user_id: Field.text({
39+
label: 'User ID',
40+
required: false,
41+
description: 'User who performed the action (null for system actions)',
42+
}),
43+
44+
action: Field.select(['create', 'update', 'delete', 'restore', 'login', 'logout', 'permission_change', 'config_change', 'export', 'import'], {
45+
label: 'Action',
46+
required: true,
47+
description: 'Action type (snake_case). Values: create, update, delete, restore, login, logout, permission_change, config_change, export, import',
48+
}),
49+
50+
object_name: Field.text({
51+
label: 'Object Name',
52+
required: false,
53+
maxLength: 255,
54+
description: 'Target object (e.g. sys_user, project_task)',
55+
}),
56+
57+
record_id: Field.text({
58+
label: 'Record ID',
59+
required: false,
60+
description: 'ID of the affected record',
61+
}),
62+
63+
old_value: Field.textarea({
64+
label: 'Old Value',
65+
required: false,
66+
description: 'JSON-serialized previous state',
67+
}),
68+
69+
new_value: Field.textarea({
70+
label: 'New Value',
71+
required: false,
72+
description: 'JSON-serialized new state',
73+
}),
74+
75+
ip_address: Field.text({
76+
label: 'IP Address',
77+
required: false,
78+
maxLength: 45,
79+
}),
80+
81+
user_agent: Field.textarea({
82+
label: 'User Agent',
83+
required: false,
84+
}),
85+
86+
tenant_id: Field.text({
87+
label: 'Tenant ID',
88+
required: false,
89+
description: 'Tenant context for multi-tenant isolation',
90+
}),
91+
92+
metadata: Field.textarea({
93+
label: 'Metadata',
94+
required: false,
95+
description: 'JSON-serialized additional context',
96+
}),
97+
},
98+
99+
indexes: [
100+
{ fields: ['created_at'] },
101+
{ fields: ['user_id'] },
102+
{ fields: ['object_name', 'record_id'] },
103+
{ fields: ['action'] },
104+
{ fields: ['tenant_id'] },
105+
],
106+
107+
enable: {
108+
trackHistory: false, // Audit logs are themselves the audit trail
109+
searchable: true,
110+
apiEnabled: true,
111+
apiMethods: ['get', 'list'], // Read-only — audit logs are immutable; creation happens via internal system hooks only
112+
trash: false, // Never soft-delete audit logs
113+
mru: false,
114+
clone: false,
115+
},
116+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"extends": "../../../tsconfig.json",
3+
"compilerOptions": {
4+
"outDir": "./dist",
5+
"rootDir": "./src"
6+
},
7+
"include": ["src/**/*"],
8+
"exclude": ["dist", "node_modules", "**/*.test.ts"]
9+
}

packages/plugins/plugin-auth/src/auth-schema-config.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ export const AUTH_VERIFICATION_CONFIG = {
157157
* | updatedAt | updated_at |
158158
*/
159159
export const AUTH_ORGANIZATION_SCHEMA = {
160-
modelName: 'sys_organization',
160+
modelName: SystemObjectName.ORGANIZATION, // 'sys_organization'
161161
fields: {
162162
createdAt: 'created_at',
163163
updatedAt: 'updated_at',
@@ -178,7 +178,7 @@ export const AUTH_ORGANIZATION_SCHEMA = {
178178
* | createdAt | created_at |
179179
*/
180180
export const AUTH_MEMBER_SCHEMA = {
181-
modelName: 'sys_member',
181+
modelName: SystemObjectName.MEMBER, // 'sys_member'
182182
fields: {
183183
organizationId: 'organization_id',
184184
userId: 'user_id',
@@ -202,7 +202,7 @@ export const AUTH_MEMBER_SCHEMA = {
202202
* | teamId | team_id |
203203
*/
204204
export const AUTH_INVITATION_SCHEMA = {
205-
modelName: 'sys_invitation',
205+
modelName: SystemObjectName.INVITATION, // 'sys_invitation'
206206
fields: {
207207
organizationId: 'organization_id',
208208
inviterId: 'inviter_id',
@@ -240,7 +240,7 @@ export const AUTH_ORG_SESSION_FIELDS = {
240240
* | updatedAt | updated_at |
241241
*/
242242
export const AUTH_TEAM_SCHEMA = {
243-
modelName: 'sys_team',
243+
modelName: SystemObjectName.TEAM, // 'sys_team'
244244
fields: {
245245
organizationId: 'organization_id',
246246
createdAt: 'created_at',
@@ -262,7 +262,7 @@ export const AUTH_TEAM_SCHEMA = {
262262
* | createdAt | created_at |
263263
*/
264264
export const AUTH_TEAM_MEMBER_SCHEMA = {
265-
modelName: 'sys_team_member',
265+
modelName: SystemObjectName.TEAM_MEMBER, // 'sys_team_member'
266266
fields: {
267267
teamId: 'team_id',
268268
userId: 'user_id',
@@ -283,7 +283,7 @@ export const AUTH_TEAM_MEMBER_SCHEMA = {
283283
* | userId | user_id |
284284
*/
285285
export const AUTH_TWO_FACTOR_SCHEMA = {
286-
modelName: 'sys_two_factor',
286+
modelName: SystemObjectName.TWO_FACTOR, // 'sys_two_factor'
287287
fields: {
288288
backupCodes: 'backup_codes',
289289
userId: 'user_id',

0 commit comments

Comments
 (0)