Skip to content

Commit 74210d0

Browse files
committed
feat: add metadata management demo with loading, saving, and backup features; include example objects and configuration
1 parent ffec530 commit 74210d0

12 files changed

Lines changed: 237 additions & 22 deletions

File tree

examples/metadata-demo/README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Metadata Management Demo
2+
3+
This example demonstrates how to use the `@objectstack/metadata` package to manage application metadata (Objects, Views, Flows, etc.).
4+
5+
## Key Features Demonstrated
6+
7+
1. **Metadata Manager**: Initialization and configuration.
8+
2. **Loading**: Reading metadata from the filesystem (`json`, `yaml`, `ts`).
9+
3. **Saving API**:
10+
* **Atomic Writes**: Safe file updates.
11+
* **Backups**: Automatic `.bak` creation.
12+
* **Format Control**: Saving as JSON or YAML.
13+
4. **Registry Pattern**: How the manager handles multiple loaders (FileSystem, etc.).
14+
15+
## Project Structure
16+
17+
```text
18+
├── metadata/ # Metadata Repository
19+
│ └── objects/ # Object Definitions
20+
│ └── demo.object.json
21+
├── src/
22+
│ └── index.ts # Demo Script
23+
└── package.json
24+
```
25+
26+
## Running the Demo
27+
28+
Make sure you have installed dependencies in the root workspace.
29+
30+
```bash
31+
# Run the demo script
32+
pnpm start
33+
```
34+
35+
## Expected Output
36+
37+
The script will:
38+
1. Load `demo_object` from JSON.
39+
2. Add a timestamp description and save it back (creating a backup).
40+
3. Create a new `generated_object` as a YAML file.
41+
4. List all available objects.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "demo_object",
3+
"label": "Demo Object",
4+
"type": "object",
5+
"fields": {
6+
"name": {
7+
"type": "text",
8+
"label": "Name",
9+
"required": true
10+
},
11+
"status": {
12+
"type": "select",
13+
"options": ["Draft", "Active"]
14+
}
15+
}
16+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { ServiceObject } from '@objectstack/spec/data';
2+
3+
export const metadata: ServiceObject = {
4+
"name": "demo_object",
5+
"label": "Demo Object",
6+
"type": "object",
7+
"fields": {
8+
"name": {
9+
"type": "text",
10+
"label": "Name",
11+
"required": true
12+
},
13+
"status": {
14+
"type": "select",
15+
"options": [
16+
"Draft",
17+
"Active"
18+
]
19+
}
20+
},
21+
"description": "Updated at 2026-02-04T15:41:55.800Z"
22+
};
23+
24+
export default metadata;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
name: generated_object
2+
label: Generated Object
3+
type: object
4+
fields:
5+
auto_field:
6+
type: text
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "@example/metadata-demo",
3+
"version": "0.1.0",
4+
"description": "Demonstration of Metadata Manager capabilities (Load/Save/Watch)",
5+
"private": true,
6+
"scripts": {
7+
"start": "tsx src/index.ts",
8+
"dirty-read": "tsx src/dirty-read.ts"
9+
},
10+
"dependencies": {
11+
"@objectstack/metadata": "workspace:*",
12+
"@objectstack/spec": "workspace:*",
13+
"@objectstack/core": "workspace:*"
14+
},
15+
"devDependencies": {
16+
"typescript": "^5.0.0",
17+
"tsx": "^4.21.0",
18+
"@types/node": "^22.0.0"
19+
}
20+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { MetadataManager } from '@objectstack/metadata';
2+
import * as path from 'node:path';
3+
// import { fileURLToPath } from 'node:url';
4+
5+
// const __dirname = path.dirname(fileURLToPath(import.meta.url));
6+
const rootDir = path.resolve(__dirname, '../metadata');
7+
8+
async function main() {
9+
console.log('🚀 Starting Metadata Demo...');
10+
console.log(`📂 Root Directory: ${rootDir}`);
11+
12+
// 1. Initialize Manager
13+
const manager = new MetadataManager({
14+
rootDir,
15+
formats: ['json', 'yaml', 'typescript'],
16+
watch: false
17+
});
18+
19+
// 2. List initial items
20+
console.log('\n2. Listing "object" items:');
21+
const objects = await manager.list('object');
22+
console.log(' Found:', objects);
23+
24+
// 3. Load an item
25+
console.log('\n3. Loading "demo_object":');
26+
const demoObject = await manager.load('object', 'demo_object');
27+
console.log(' Loaded:', demoObject?.label);
28+
29+
if (demoObject) {
30+
// 4. Modify and Save (Update)
31+
console.log('\n4. Updating "demo_object" (adding description)...');
32+
demoObject.description = `Updated at ${new Date().toISOString()}`;
33+
34+
// Explicitly using filesystem loader, using atomic writes
35+
const saveResult = await manager.save('object', 'demo_object', demoObject, {
36+
loader: 'filesystem',
37+
atomic: true,
38+
backup: true // Create .bak file
39+
});
40+
console.log(' Save Result:', saveResult.success ? 'Success' : 'Failed');
41+
console.log(' Path:', saveResult.path);
42+
}
43+
44+
// 5. Create a NEW item programmatically
45+
console.log('\n5. Creating new "generated_object"...');
46+
const newObject = {
47+
name: 'generated_object',
48+
label: 'Generated Object',
49+
type: 'object',
50+
fields: {
51+
auto_field: { type: 'text' }
52+
}
53+
};
54+
55+
const createResult = await manager.save('object', 'generated_object', newObject, {
56+
format: 'yaml', // Save as YAML
57+
create: true
58+
});
59+
console.log(' Created Result:', createResult.success ? 'Success' : 'Failed');
60+
console.log(' Path:', createResult.path);
61+
62+
// 6. Verify List again
63+
console.log('\n6. Final List:');
64+
const finalObjects = await manager.list('object');
65+
console.log(' Found:', finalObjects);
66+
}
67+
68+
main().catch(console.error);
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ESNext",
4+
"module": "NodeNext",
5+
"moduleResolution": "NodeNext",
6+
"strict": true,
7+
"esModuleInterop": true,
8+
"skipLibCheck": true,
9+
"forceConsistentCasingInFileNames": true,
10+
"outDir": "dist",
11+
"paths": {
12+
"@objectstack/metadata": ["../../packages/metadata/src/index.ts"],
13+
"@objectstack/spec": ["../../packages/spec/src/index.ts"],
14+
"@objectstack/core": ["../../packages/core/src/index.ts"]
15+
}
16+
},
17+
"include": ["src/**/*"]
18+
}

packages/metadata/src/loaders/filesystem-loader.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,7 @@ export class FilesystemLoader implements MetadataLoader {
342342
return {
343343
success: true,
344344
path: filePath,
345-
format,
345+
// format, // Not in schema
346346
size: Buffer.byteLength(content, 'utf-8'),
347347
backupPath,
348348
saveTime: Date.now() - startTime,
@@ -353,14 +353,7 @@ export class FilesystemLoader implements MetadataLoader {
353353
name,
354354
error: error instanceof Error ? error.message : String(error),
355355
});
356-
357-
return {
358-
success: false,
359-
path: '', // TODO: Should this be optional in result?
360-
format,
361-
error: error instanceof Error ? error : new Error(String(error)),
362-
saveTime: Date.now() - startTime,
363-
};
356+
throw error;
364357
}
365358
}
366359

packages/metadata/src/metadata-manager.ts

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@
44
* Main orchestrator for metadata loading, saving, and persistence
55
*/
66

7-
import * as fs from 'node:fs/promises';
87
import * as path from 'node:path';
9-
import { createHash } from 'node:crypto';
108
import { watch as chokidarWatch, type FSWatcher } from 'chokidar';
119
import type {
1210
MetadataManagerConfig,
@@ -111,7 +109,6 @@ export class MetadataManager {
111109
options?: MetadataLoadOptions
112110
): Promise<T[]> {
113111
const results: T[] = [];
114-
const seen = new Set<string>(); // De-duplication key needed? For now, simple aggregation
115112

116113
for (const loader of this.loaders.values()) {
117114
try {
@@ -318,14 +315,4 @@ export class MetadataManager {
318315
}
319316
}
320317
}
321-
322-
/**
323-
* Generate ETag for content
324-
* Uses SHA-256 hash truncated to 32 characters for reasonable collision resistance
325-
* while keeping ETag headers compact (full 64-char hash is overkill for this use case)
326-
*/
327-
private generateETag(content: string): string {
328-
const hash = createHash('sha256').update(content).digest('hex').substring(0, 32);
329-
return `"${hash}"`;
330-
}
331318
}

packages/spec/json-schema/system/MetadataLoadOptions.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@
4242
"items": {
4343
"type": "string"
4444
}
45+
},
46+
"loader": {
47+
"type": "string",
48+
"description": "Specific loader to use (e.g. filesystem, database)"
4549
}
4650
},
4751
"additionalProperties": false

0 commit comments

Comments
 (0)