Skip to content

Commit ad0662c

Browse files
committed
Add JSON schema for ObjectOwnershipEnum with enum values 'own' and 'extend'
1 parent 57e8520 commit ad0662c

26 files changed

Lines changed: 3526 additions & 112 deletions

PLANNING.md

Lines changed: 134 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# ObjectStack Project Master Plan & Status Report
22

3-
**Date:** 2026-02-05
4-
**Version:** 1.0.0
3+
**Date:** 2026-02-07
4+
**Version:** 1.1.0
55

66
## 1. Project Master Scheme (项目总体方案)
77

@@ -58,7 +58,14 @@ ObjectStack is designed as a **metadata-driven, post-SaaS operating system**. It
5858

5959
## 3. Next Development Roadmap (下一步开发计划)
6060

61-
The immediate focus shifts from **Architecture/Kernel** stability to **Feature/Persistence** implementation.
61+
The focus shifts from **Architecture/Kernel** stability to **Namespace Isolation** and **Feature/Persistence** implementation.
62+
63+
### Phase 0: Namespace & Ownership Model (Current Week) ← ACTIVE
64+
**Objective:** Enable multi-vendor object isolation and extension.
65+
1. **Spec Schemas** ✅: `namespace` in manifest, `ObjectOwnershipEnum`, `ObjectExtensionSchema`.
66+
2. **Registry Rewrite** 🔄: FQN computation, contributor tracking, priority-based merge.
67+
3. **Engine Update**: Pass namespace/ownership through registration flow.
68+
4. **Testing**: Comprehensive ownership scenario tests.
6269

6370
### Phase 1: Persistence Strategy (Week 1-2)
6471
**Objective:** Move beyond in-memory data to persistent storage.
@@ -94,7 +101,130 @@ The immediate focus shifts from **Architecture/Kernel** stability to **Feature/P
94101

95102
---
96103

97-
## 4. Immediate Action Items (Today/Tomorrow)
104+
## 4. **ACTIVE: Namespace & Object Ownership Model** (当前进行中)
105+
106+
**Date Started:** 2026-02-07
107+
**Status:** 🔄 In Progress
108+
**Branch:** `feature/namespace-ownership`
109+
110+
### 4.1 Problem Statement
111+
112+
Multi-vendor packages from different vendors will inevitably define objects with the same short name (e.g., both `app-todo` and `app-crm` define `task`). The current flat registry causes silent overwrites. Object names are also tied to database table names, requiring a namespace isolation mechanism.
113+
114+
### 4.2 Agreed Solution (Salesforce-style Namespace Model)
115+
116+
#### 4.2.1 Namespace Rules
117+
118+
| Property | Rule |
119+
|----------|------|
120+
| **Format** | `^[a-z][a-z0-9_]{1,19}$` (2-20 chars) |
121+
| **Uniqueness** | Instance-unique (validated at boot) |
122+
| **Reserved** | `base`, `system` — no FQN prefix applied |
123+
| **FQN Formula** | `{namespace}__{short_name}` (double underscore) |
124+
| **Example** | namespace `crm` + object `account` → FQN `crm__account` |
125+
126+
#### 4.2.2 Ownership Model
127+
128+
| Mode | Description |
129+
|------|-------------|
130+
| **`own`** | This package is the original author. Only ONE owner per FQN. Defines base schema (table name, primary key, core fields). |
131+
| **`extend`** | This package adds fields/config to an existing object. Multiple extenders allowed. Fields merged additively. |
132+
133+
#### 4.2.3 Merge Strategy
134+
135+
1. Owner defines the base object.
136+
2. Extenders sorted by `priority` (lower first, higher wins on conflict).
137+
3. Fields: additive merge. Same-name field → higher priority wins.
138+
4. Non-field props (`label`, `description`): last writer by priority wins.
139+
140+
### 4.3 Implementation Checklist
141+
142+
| # | Task | Status | File(s) |
143+
|---|------|--------|---------|
144+
| 1 | Add `namespace` to ManifestSchema | ✅ Done | `spec/src/kernel/manifest.zod.ts` |
145+
| 2 | Add `ObjectOwnershipEnum`, `ObjectExtensionSchema` | ✅ Done | `spec/src/data/object.zod.ts` |
146+
| 3 | Add `objectExtensions` to StackDefinitionSchema | ✅ Done | `spec/src/stack.zod.ts` |
147+
| 4 | **Rewrite SchemaRegistry with ownership model** | 🔄 In Progress | `objectql/src/registry.ts` |
148+
| 5 | Update Engine to pass namespace/ownership | ⬜ Pending | `objectql/src/engine.ts` |
149+
| 6 | Update Broker shim | ⬜ Pending | `console/src/mocks/createKernel.ts` |
150+
| 7 | Add `namespace` to example apps | ⬜ Pending | `app-todo/objectstack.config.ts`, `app-crm/objectstack.config.ts` |
151+
| 8 | Write comprehensive registry tests | ⬜ Pending | `objectql/src/registry.test.ts` |
152+
| 9 | Update engine tests | ⬜ Pending | `objectql/src/engine.test.ts` |
153+
| 10 | Build and verify all packages | ⬜ Pending ||
154+
155+
### 4.4 Key Data Structures (Registry Rewrite)
156+
157+
```typescript
158+
// Reserved namespaces (no FQN prefix)
159+
const RESERVED_NAMESPACES = new Set(['base', 'system']);
160+
161+
// Contributor record
162+
interface ObjectContributor {
163+
packageId: string;
164+
namespace: string;
165+
ownership: 'own' | 'extend';
166+
priority: number; // 100 = owner default, 200+ = extender
167+
definition: ServiceObject;
168+
}
169+
170+
// Primary storage: FQN → Contributor[]
171+
objectContributors: Map<string, ObjectContributor[]>;
172+
173+
// Merge cache: FQN → merged ServiceObject
174+
mergedObjectCache: Map<string, ServiceObject>;
175+
176+
// Namespace uniqueness: namespace → packageId
177+
namespaceRegistry: Map<string, string>;
178+
```
179+
180+
### 4.5 API Changes
181+
182+
```typescript
183+
// Before
184+
SchemaRegistry.registerObject(schema, packageId?)
185+
186+
// After
187+
SchemaRegistry.registerObject(schema, packageId, namespace, ownership, priority)
188+
SchemaRegistry.resolveObject(fqn) // Returns merged object
189+
SchemaRegistry.getAllObjects(packageId?) // Returns merged objects
190+
SchemaRegistry.getObjectContributors(fqn) // Returns all contributors
191+
```
192+
193+
---
194+
195+
## 5. Immediate Action Items (Today/Tomorrow)
196+
197+
1. **Complete Registry Rewrite** — Implement FQN computation, contributor tracking, merge engine.
198+
2. **Update Engine** — Extract namespace from manifest, handle `objectExtensions` array.
199+
3. **Test Extensively** — Own/extend/conflict/merge/FQN/uninstall scenarios.
200+
201+
---
202+
203+
## 6. Future Development Roadmap (下一步开发计划)
98204
99205
1. **Scaffold `packages/plugins/driver-sqlite`**.
100206
2. **Extract Standard Driver Test Suite**: Ensure `packages/core/src/qa` has a reusable test suite that can be applied to new drivers immediately.
207+
208+
---
209+
210+
## Appendix: Design Decision Records
211+
212+
### DDR-001: Namespace Separator (2026-02-07)
213+
**Decision:** Use double underscore `__` as FQN separator.
214+
**Rationale:**
215+
- Single underscore `_` is common in field names (`created_at`).
216+
- Dot `.` conflicts with JavaScript property access.
217+
- Matches Salesforce convention for managed packages.
218+
219+
### DDR-002: Reserved Namespaces (2026-02-07)
220+
**Decision:** `base` and `system` namespaces are platform-reserved (no prefix applied).
221+
**Rationale:**
222+
- Platform-defined objects like `user`, `organization` should not be prefixed.
223+
- These objects form the foundation that all apps depend on.
224+
225+
### DDR-003: Ownership vs Extension (2026-02-07)
226+
**Decision:** Separate `own` vs `extend` declaration.
227+
**Rationale:**
228+
- Clear ownership prevents accidental table creation by extenders.
229+
- Extension priority enables deterministic merge order.
230+
- Follows Salesforce pattern of "managed object" vs "managed package extensions".

examples/app-crm/objectstack.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
export default defineStack({
2525
manifest: {
2626
id: 'com.example.crm',
27+
namespace: 'crm',
2728
version: '3.0.0',
2829
type: 'app',
2930
name: 'Enterprise CRM',

examples/app-todo/objectstack.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import * as apps from './src/apps';
1111
export default defineStack({
1212
manifest: {
1313
id: 'com.example.todo',
14+
namespace: 'todo',
1415
version: '2.0.0',
1516
type: 'app',
1617
name: 'Todo Manager',

packages/objectql/src/engine.test.ts

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,13 @@ vi.mock('./registry', () => {
99
return {
1010
SchemaRegistry: {
1111
getObject: vi.fn((name) => mockObjects.get(name)),
12-
registerObject: vi.fn((obj) => mockObjects.set(obj.name, obj)),
12+
resolveObject: vi.fn((name) => mockObjects.get(name)),
13+
registerObject: vi.fn((obj, packageId, namespace, ownership, priority) => {
14+
const fqn = namespace ? `${namespace}__${obj.name}` : obj.name;
15+
mockObjects.set(fqn, { ...obj, name: fqn });
16+
return fqn;
17+
}),
18+
registerNamespace: vi.fn(),
1319
registerKind: vi.fn(),
1420
registerItem: vi.fn(),
1521
registerApp: vi.fn(),
@@ -19,6 +25,7 @@ vi.mock('./registry', () => {
1925
enabled: true,
2026
installedAt: new Date().toISOString(),
2127
})),
28+
reset: vi.fn(() => mockObjects.clear()),
2229
metadata: {
2330
get: vi.fn(() => mockObjects) // Expose for verification if needed
2431
}
@@ -79,16 +86,58 @@ describe('ObjectQL Engine', () => {
7986
});
8087

8188
describe('Metadata Registration', () => {
82-
it('should register objects from app manifest', () => {
89+
it('should register objects from app manifest with namespace', () => {
8390
const manifest = {
8491
id: 'com.example.app',
92+
namespace: 'example',
8593
objects: [
8694
{ name: 'task', fields: {} }
8795
]
8896
};
8997

9098
engine.registerApp(manifest);
91-
expect(SchemaRegistry.registerObject).toHaveBeenCalledWith(expect.objectContaining({ name: 'task' }), 'com.example.app');
99+
expect(SchemaRegistry.registerObject).toHaveBeenCalledWith(
100+
expect.objectContaining({ name: 'task' }),
101+
'com.example.app',
102+
'example',
103+
'own'
104+
);
105+
});
106+
107+
it('should register objects without namespace (legacy)', () => {
108+
const manifest = {
109+
id: 'com.legacy.app',
110+
objects: [
111+
{ name: 'item', fields: {} }
112+
]
113+
};
114+
115+
engine.registerApp(manifest);
116+
expect(SchemaRegistry.registerObject).toHaveBeenCalledWith(
117+
expect.objectContaining({ name: 'item' }),
118+
'com.legacy.app',
119+
undefined,
120+
'own'
121+
);
122+
});
123+
124+
it('should register object extensions', () => {
125+
const manifest = {
126+
id: 'com.extender.app',
127+
namespace: 'ext',
128+
objectExtensions: [
129+
{ extend: 'base__contact', fields: { custom_field: { type: 'text' } }, priority: 250 }
130+
]
131+
};
132+
133+
engine.registerApp(manifest);
134+
expect(SchemaRegistry.registerObject).toHaveBeenCalledWith(
135+
expect.objectContaining({ name: 'base__contact' }),
136+
'com.extender.app',
137+
undefined,
138+
'extend',
139+
250
140+
);
92141
});
93142

94143
it('should register kinds from app manifest', () => {

packages/objectql/src/engine.ts

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -150,31 +150,54 @@ export class ObjectQL implements IDataEngine {
150150
*/
151151
registerApp(manifest: any) {
152152
const id = manifest.id || manifest.name;
153-
this.logger.debug('Registering package manifest', { id });
153+
const namespace = manifest.namespace as string | undefined;
154+
this.logger.debug('Registering package manifest', { id, namespace });
154155

155156
// 1. Register the Package (manifest + lifecycle state)
156157
SchemaRegistry.installPackage(manifest);
157-
this.logger.debug('Installed Package', { id: manifest.id, name: manifest.name });
158+
this.logger.debug('Installed Package', { id: manifest.id, name: manifest.name, namespace });
158159

159-
// 2. Register objects
160+
// 2. Register owned objects
160161
if (manifest.objects) {
161162
if (Array.isArray(manifest.objects)) {
162163
this.logger.debug('Registering objects from manifest (Array)', { id, objectCount: manifest.objects.length });
163164
for (const objDef of manifest.objects) {
164-
SchemaRegistry.registerObject(objDef, id);
165-
this.logger.debug('Registered Object', { object: objDef.name, from: id });
165+
const fqn = SchemaRegistry.registerObject(objDef, id, namespace, 'own');
166+
this.logger.debug('Registered Object', { fqn, from: id });
166167
}
167168
} else {
168169
this.logger.debug('Registering objects from manifest (Map)', { id, objectCount: Object.keys(manifest.objects).length });
169170
for (const [name, objDef] of Object.entries(manifest.objects)) {
170171
// Ensure name in definition matches key
171172
(objDef as any).name = name;
172-
SchemaRegistry.registerObject(objDef as any, id);
173-
this.logger.debug('Registered Object', { object: name, from: id });
173+
const fqn = SchemaRegistry.registerObject(objDef as any, id, namespace, 'own');
174+
this.logger.debug('Registered Object', { fqn, from: id });
174175
}
175176
}
176177
}
177178

179+
// 2b. Register object extensions (fields added to objects owned by other packages)
180+
if (Array.isArray(manifest.objectExtensions) && manifest.objectExtensions.length > 0) {
181+
this.logger.debug('Registering object extensions', { id, count: manifest.objectExtensions.length });
182+
for (const ext of manifest.objectExtensions) {
183+
const targetFqn = ext.extend;
184+
const priority = ext.priority ?? 200;
185+
// Create a partial object definition for the extension
186+
const extDef = {
187+
name: targetFqn, // Use the target FQN as name
188+
fields: ext.fields,
189+
label: ext.label,
190+
pluralLabel: ext.pluralLabel,
191+
description: ext.description,
192+
validations: ext.validations,
193+
indexes: ext.indexes,
194+
};
195+
// Register as extension (namespace is undefined since we're targeting by FQN)
196+
SchemaRegistry.registerObject(extDef as any, id, undefined, 'extend', priority);
197+
this.logger.debug('Registered Object Extension', { target: targetFqn, priority, from: id });
198+
}
199+
}
200+
178201
// 3. Register apps (UI navigation definitions) as their own metadata type
179202
if (Array.isArray(manifest.apps) && manifest.apps.length > 0) {
180203
this.logger.debug('Registering apps from manifest', { id, count: manifest.apps.length });

packages/objectql/src/index.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
// Export Registry
2-
export { SchemaRegistry } from './registry.js';
2+
export {
3+
SchemaRegistry,
4+
computeFQN,
5+
parseFQN,
6+
RESERVED_NAMESPACES,
7+
DEFAULT_OWNER_PRIORITY,
8+
DEFAULT_EXTENDER_PRIORITY,
9+
} from './registry.js';
10+
export type { ObjectContributor } from './registry.js';
311

412
// Export Protocol Implementation
513
export { ObjectStackProtocolImplementation } from './protocol.js';

0 commit comments

Comments
 (0)