Skip to content

Commit 8a1d199

Browse files
committed
feat: Add version, objects, and apis properties to App schema
- Updated App.mdx to include version, objects, and apis properties in the schema reference. - Modified App.json to define version, objects, and apis properties in the JSON schema. - Enhanced app.zod.ts to validate version property and make it optional. - Introduced new ApiCapabilities and ApiRoutes schema references with their respective properties. - Created Discovery schema reference with properties for name, version, environment, routes, features, and locale. - Added example server implementation with dynamic schema loading and unified API routes. - Updated package.json and tsconfig.json for the example server setup. - Implemented DataEngine and SchemaRegistry classes for managing data and schema registration.
1 parent 6ad44a5 commit 8a1d199

File tree

17 files changed

+741
-4
lines changed

17 files changed

+741
-4
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
title: ApiCapabilities
3+
description: ApiCapabilities Schema Reference
4+
---
5+
6+
## Properties
7+
8+
| Property | Type | Required | Description |
9+
| :--- | :--- | :--- | :--- |
10+
| **graphql** | `boolean` | optional | |
11+
| **search** | `boolean` | optional | |
12+
| **websockets** | `boolean` | optional | |
13+
| **files** | `boolean` | optional | |
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
title: ApiRoutes
3+
description: ApiRoutes Schema Reference
4+
---
5+
6+
## Properties
7+
8+
| Property | Type | Required | Description |
9+
| :--- | :--- | :--- | :--- |
10+
| **data** | `string` || e.g. /api/v1/data |
11+
| **metadata** | `string` || e.g. /api/v1/meta |
12+
| **auth** | `string` || e.g. /api/v1/auth |
13+
| **actions** | `string` | optional | e.g. /api/v1/p |
14+
| **storage** | `string` | optional | e.g. /api/v1/storage |
15+
| **graphql** | `string` | optional | e.g. /api/v1/graphql |
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
title: Discovery
3+
description: Discovery Schema Reference
4+
---
5+
6+
## Properties
7+
8+
| Property | Type | Required | Description |
9+
| :--- | :--- | :--- | :--- |
10+
| **name** | `string` || |
11+
| **version** | `string` || |
12+
| **environment** | `Enum<'production' \| 'sandbox' \| 'development'>` || |
13+
| **routes** | `object` || |
14+
| **features** | `object` || |
15+
| **locale** | `object` || |

content/docs/references/ui/app/App.mdx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,15 @@ description: App Schema Reference
99
| :--- | :--- | :--- | :--- |
1010
| **name** | `string` || App unique machine name |
1111
| **label** | `string` || App display label |
12+
| **version** | `string` | optional | App version |
1213
| **description** | `string` | optional | App description |
1314
| **icon** | `string` | optional | App icon used in the App Launcher |
1415
| **branding** | `object` | optional | App-specific branding |
1516
| **active** | `boolean` | optional | Whether the app is enabled |
1617
| **isDefault** | `boolean` | optional | Is default app |
17-
| **navigation** | `object \| object \| object \| object \| object[]` || Structured navigation menu tree |
18+
| **navigation** | `object \| object \| object \| object \| object[]` | optional | Structured navigation menu tree |
19+
| **menus** | `any[]` | optional | Deprecated: Use navigation instead |
1820
| **homePageId** | `string` | optional | ID of the navigation item to serve as landing page |
1921
| **requiredPermissions** | `string[]` | optional | Permissions required to access this app |
22+
| **objects** | `any[]` | optional | Objects belonging to this app |
23+
| **apis** | `any[]` | optional | Custom APIs belonging to this app |

examples/server/README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# ObjectStack Example Server
2+
3+
This is a reference implementation of the ObjectStack Server Protocol (Kernel).
4+
It demonstrates how to build a metadata-driven backend that dynamically loads object definitions from plugins and automatically generates REST APIs.
5+
6+
## Features
7+
8+
- **Dynamic Schema Loading**: Loads `crm` and `todo` apps as plugins.
9+
- **Unified Metadata API**: `/api/v1/meta/objects`
10+
- **Unified Data API**: `/api/v1/data/:object` (CRUD)
11+
- **Zero-Code Backend**: No creating routes or controllers per object.
12+
13+
## Setup
14+
15+
1. Make sure all dependencies are installed in the workspace root:
16+
```bash
17+
pnpm install
18+
```
19+
20+
2. Run the server:
21+
```bash
22+
npm run dev
23+
```
24+
25+
## API Usage Examples
26+
27+
### 1. Get All Objects
28+
```bash
29+
curl http://localhost:3000/api/v1/meta/objects
30+
```
31+
32+
### 2. Create a Todo
33+
```bash
34+
curl -X POST http://localhost:3000/api/v1/data/todo_task \
35+
-H "Content-Type: application/json" \
36+
-d '{"title": "Buy Milk", "priority": "high"}'
37+
```
38+
39+
### 3. List Todos
40+
```bash
41+
curl http://localhost:3000/api/v1/data/todo_task
42+
```

examples/server/package.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "@objectstack/example-server",
3+
"version": "0.1.0",
4+
"private": true,
5+
"scripts": {
6+
"dev": "ts-node src/index.ts",
7+
"build": "tsc"
8+
},
9+
"dependencies": {
10+
"@objectstack/spec": "workspace:*",
11+
"@objectstack/example-crm": "workspace:*",
12+
"@objectstack/example-todo": "workspace:*",
13+
"hono": "^4.0.0",
14+
"@hono/node-server": "^1.0.0",
15+
"zod": "^3.0.0"
16+
},
17+
"devDependencies": {
18+
"typescript": "^5.0.0",
19+
"ts-node": "^10.0.0",
20+
"@types/node": "^20.0.0"
21+
}
22+
}

examples/server/src/index.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { serve } from '@hono/node-server';
2+
import { Hono } from 'hono';
3+
import { logger } from 'hono/logger';
4+
import { cors } from 'hono/cors';
5+
import { loadPlugins } from './loader';
6+
import { SchemaRegistry } from './kernel/registry';
7+
import { DataEngine } from './kernel/engine';
8+
9+
// 1. Initialize Kernel
10+
const app = new Hono();
11+
const dataEngine = new DataEngine();
12+
13+
app.use('*', logger());
14+
app.use('*', cors());
15+
16+
// 2. Load Plugins (CRM, Todo)
17+
console.log('--- Starting ObjectStack Server ---');
18+
loadPlugins();
19+
console.log('--- Plugins Loaded ---');
20+
21+
// 3. Define Unified Routes
22+
23+
/**
24+
* Metadata API: Get All Objects
25+
*/
26+
app.get('/api/v1/meta/objects', (c) => {
27+
const objects = SchemaRegistry.getAll().map(obj => ({
28+
name: obj.name,
29+
label: obj.label,
30+
icon: obj.icon,
31+
path: `/api/v1/data/${obj.name}`
32+
}));
33+
return c.json({ data: objects });
34+
});
35+
36+
/**
37+
* Metadata API: Get Single Object
38+
*/
39+
app.get('/api/v1/meta/objects/:name', (c) => {
40+
const name = c.req.param('name');
41+
const schema = SchemaRegistry.get(name);
42+
if (!schema) return c.json({ error: 'Not found' }, 404);
43+
return c.json(schema);
44+
});
45+
46+
/**
47+
* Data API: Find
48+
*/
49+
app.get('/api/v1/data/:object', async (c) => {
50+
const objectName = c.req.param('object');
51+
const query = c.req.query();
52+
53+
try {
54+
const result = await dataEngine.find(objectName, query);
55+
return c.json(result);
56+
} catch (e: any) {
57+
return c.json({ error: e.message }, 400);
58+
}
59+
});
60+
61+
/**
62+
* Data API: Get
63+
*/
64+
app.get('/api/v1/data/:object/:id', async (c) => {
65+
const objectName = c.req.param('object');
66+
const id = c.req.param('id');
67+
68+
try {
69+
const result = await dataEngine.get(objectName, id);
70+
return c.json(result);
71+
} catch (e: any) {
72+
return c.json({ error: e.message }, 404);
73+
}
74+
});
75+
76+
/**
77+
* Data API: Create
78+
*/
79+
app.post('/api/v1/data/:object', async (c) => {
80+
const objectName = c.req.param('object');
81+
const body = await c.req.json();
82+
83+
try {
84+
const result = await dataEngine.create(objectName, body);
85+
return c.json(result, 201);
86+
} catch (e: any) {
87+
return c.json({ error: e.message }, 400);
88+
}
89+
});
90+
91+
/**
92+
* Data API: Update
93+
*/
94+
app.patch('/api/v1/data/:object/:id', async (c) => {
95+
const objectName = c.req.param('object');
96+
const id = c.req.param('id');
97+
const body = await c.req.json();
98+
99+
try {
100+
const result = await dataEngine.update(objectName, id, body);
101+
return c.json(result);
102+
} catch (e: any) {
103+
return c.json({ error: e.message }, 400);
104+
}
105+
});
106+
107+
/**
108+
* Data API: Delete
109+
*/
110+
app.delete('/api/v1/data/:object/:id', async (c) => {
111+
const objectName = c.req.param('object');
112+
const id = c.req.param('id');
113+
114+
try {
115+
const result = await dataEngine.delete(objectName, id);
116+
return c.json(result);
117+
} catch (e: any) {
118+
return c.json({ error: e.message }, 400);
119+
}
120+
});
121+
122+
// 4. Start Server
123+
const port = 3000;
124+
console.log(`Server is running on http://localhost:${port}`);
125+
126+
serve({
127+
fetch: app.fetch,
128+
port
129+
});
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { ServiceObject } from '@objectstack/spec';
2+
import { SchemaRegistry } from './registry';
3+
4+
// Simple in-memory storage for demonstration
5+
const MEMORY_DB: Record<string, any[]> = {};
6+
7+
export class DataEngine {
8+
9+
async find(objectName: string, query: any) {
10+
this.ensureSchema(objectName);
11+
const records = MEMORY_DB[objectName] || [];
12+
// In real world: Implement parsing of `query` (filter, sort, page)
13+
return {
14+
value: records,
15+
count: records.length
16+
};
17+
}
18+
19+
async get(objectName: string, id: string) {
20+
this.ensureSchema(objectName);
21+
const records = MEMORY_DB[objectName] || [];
22+
const record = records.find(r => r._id === id);
23+
if (!record) throw new Error(`Record not found: ${id}`);
24+
return record;
25+
}
26+
27+
async create(objectName: string, payload: any) {
28+
const schema = this.ensureSchema(objectName);
29+
30+
// Auto-generate ID and Timestamps
31+
const record = {
32+
_id: Math.random().toString(36).substring(7),
33+
created_at: new Date().toISOString(),
34+
...payload
35+
};
36+
37+
// TODO: Validate against schema.fields
38+
39+
if (!MEMORY_DB[objectName]) MEMORY_DB[objectName] = [];
40+
MEMORY_DB[objectName].push(record);
41+
42+
return record;
43+
}
44+
45+
async update(objectName: string, id: string, payload: any) {
46+
this.ensureSchema(objectName);
47+
const records = MEMORY_DB[objectName] || [];
48+
const index = records.findIndex(r => r._id === id);
49+
50+
if (index === -1) throw new Error(`Record not found: ${id}`);
51+
52+
records[index] = { ...records[index], ...payload, updated_at: new Date().toISOString() };
53+
return records[index];
54+
}
55+
56+
async delete(objectName: string, id: string) {
57+
this.ensureSchema(objectName);
58+
const records = MEMORY_DB[objectName] || [];
59+
const index = records.findIndex(r => r._id === id);
60+
61+
if (index === -1) throw new Error(`Record not found: ${id}`);
62+
63+
records.splice(index, 1);
64+
return { success: true };
65+
}
66+
67+
private ensureSchema(name: string): ServiceObject {
68+
const schema = SchemaRegistry.get(name);
69+
if (!schema) throw new Error(`Unknown object: ${name}`);
70+
return schema;
71+
}
72+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { ServiceObject } from '@objectstack/spec';
2+
3+
/**
4+
* Global Schema Registry
5+
*/
6+
export class SchemaRegistry {
7+
private static objects = new Map<string, ServiceObject>();
8+
9+
static register(schema: ServiceObject) {
10+
if (this.objects.has(schema.name)) {
11+
console.warn(`[Registry] Overwriting object: ${schema.name}`);
12+
}
13+
this.objects.set(schema.name, schema);
14+
console.log(`[Registry] Registered object: ${schema.name}`);
15+
}
16+
17+
static get(name: string): ServiceObject | undefined {
18+
return this.objects.get(name);
19+
}
20+
21+
static getAll(): ServiceObject[] {
22+
return Array.from(this.objects.values());
23+
}
24+
}

examples/server/src/loader.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { SchemaRegistry } from './kernel/registry';
2+
3+
// In a real monorepo scenario, we might use path aliases or require.resolve
4+
// Here we use relative paths to demonstrate loading from the sibling packages
5+
// @ts-ignore
6+
import CrmApp from '../../crm/objectstack.config';
7+
// @ts-ignore
8+
import TodoApp from '../../todo/objectstack.config';
9+
10+
export function loadPlugins() {
11+
const apps = [CrmApp, TodoApp];
12+
13+
for (const app of apps) {
14+
if (!app) continue;
15+
16+
console.log(`[Loader] Loading App: ${app.name} (${app.label})`);
17+
18+
// 1. Register Objects
19+
if (app.objects) {
20+
app.objects.forEach((obj: any) => {
21+
SchemaRegistry.register(obj);
22+
});
23+
}
24+
25+
console.log(`[Loader] Loaded ${app.objects?.length || 0} objects from ${app.name}`);
26+
}
27+
}

0 commit comments

Comments
 (0)