Skip to content

Commit 206eb22

Browse files
committed
feat: add in-memory metadata service and update related fallbacks and tests
1 parent 32c86b8 commit 206eb22

8 files changed

Lines changed: 69 additions & 55 deletions

File tree

packages/core/src/fallbacks/fallbacks.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import { createMemoryI18n, resolveLocale } from './memory-i18n';
66
import { CORE_FALLBACK_FACTORIES } from './index';
77

88
describe('CORE_FALLBACK_FACTORIES', () => {
9-
it('should have exactly 4 entries: cache, queue, job, i18n', () => {
10-
expect(Object.keys(CORE_FALLBACK_FACTORIES)).toEqual(['cache', 'queue', 'job', 'i18n']);
9+
it('should have exactly 5 entries: metadata, cache, queue, job, i18n', () => {
10+
expect(Object.keys(CORE_FALLBACK_FACTORIES)).toEqual(['metadata', 'cache', 'queue', 'job', 'i18n']);
1111
});
1212

1313
it('should map to factory functions', () => {

packages/core/src/fallbacks/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,21 @@ import { createMemoryCache } from './memory-cache.js';
44
import { createMemoryQueue } from './memory-queue.js';
55
import { createMemoryJob } from './memory-job.js';
66
import { createMemoryI18n } from './memory-i18n.js';
7+
import { createMemoryMetadata } from './memory-metadata.js';
78

89
export { createMemoryCache } from './memory-cache.js';
910
export { createMemoryQueue } from './memory-queue.js';
1011
export { createMemoryJob } from './memory-job.js';
1112
export { createMemoryI18n, resolveLocale } from './memory-i18n.js';
13+
export { createMemoryMetadata } from './memory-metadata.js';
1214

1315
/**
1416
* Map of core-criticality service names to their in-memory fallback factories.
1517
* Used by ObjectKernel.validateSystemRequirements() to auto-inject fallbacks
1618
* when no real plugin provides the service.
1719
*/
1820
export const CORE_FALLBACK_FACTORIES: Record<string, () => Record<string, any>> = {
21+
metadata: createMemoryMetadata,
1922
cache: createMemoryCache,
2023
queue: createMemoryQueue,
2124
job: createMemoryJob,
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* In-memory metadata service fallback.
5+
*
6+
* Implements the IMetadataService contract with a simple Map-of-Maps store.
7+
* Used by ObjectKernel as an automatic fallback when no real metadata plugin
8+
* (e.g. MetadataPlugin with file-system persistence) is registered.
9+
*/
10+
export function createMemoryMetadata() {
11+
// type -> name -> data
12+
const store = new Map<string, Map<string, any>>();
13+
14+
function getTypeMap(type: string): Map<string, any> {
15+
let map = store.get(type);
16+
if (!map) {
17+
map = new Map();
18+
store.set(type, map);
19+
}
20+
return map;
21+
}
22+
23+
return {
24+
_fallback: true, _serviceName: 'metadata',
25+
async register(type: string, name: string, data: any): Promise<void> {
26+
getTypeMap(type).set(name, data);
27+
},
28+
async get(type: string, name: string): Promise<any> {
29+
return getTypeMap(type).get(name);
30+
},
31+
async list(type: string): Promise<any[]> {
32+
return Array.from(getTypeMap(type).values());
33+
},
34+
async unregister(type: string, name: string): Promise<void> {
35+
getTypeMap(type).delete(name);
36+
},
37+
async exists(type: string, name: string): Promise<boolean> {
38+
return getTypeMap(type).has(name);
39+
},
40+
async listNames(type: string): Promise<string[]> {
41+
return Array.from(getTypeMap(type).keys());
42+
},
43+
async getObject(name: string): Promise<any> {
44+
return getTypeMap('object').get(name);
45+
},
46+
async listObjects(): Promise<any[]> {
47+
return Array.from(getTypeMap('object').values());
48+
},
49+
};
50+
}

packages/metadata/src/plugin.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,7 @@ export class MetadataPlugin implements Plugin {
4444
watch: this.options.watch
4545
});
4646

47-
// Register Metadata Manager as primary metadata service provider
48-
// This takes precedence over ObjectQL's fallback metadata service
47+
// Register Metadata Manager as the primary metadata service provider.
4948
ctx.registerService('metadata', this.manager);
5049

5150
// Register metadata system objects via the manifest service (if available).

packages/objectql/src/plugin.integration.test.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,23 @@ describe('ObjectQLPlugin - Metadata Service Integration', () => {
1515
});
1616

1717
describe('Simple Mode (ObjectQL-only)', () => {
18-
it('should register ObjectQL as metadata service provider', async () => {
18+
it('should register objectql, data, and protocol services', async () => {
1919
// Arrange
2020
const plugin = new ObjectQLPlugin();
2121
await kernel.use(plugin);
2222

2323
// Act
2424
await kernel.bootstrap();
2525

26-
// Assert
27-
const metadataService = kernel.getService('metadata');
28-
expect(metadataService).toBeDefined();
29-
30-
// ObjectQL registers a MetadataFacade as the metadata service;
31-
// it is separate from (but backed by the same registry as) the objectql service.
26+
// Assert — ObjectQL no longer registers metadata (kernel provides fallback)
3227
const objectql = kernel.getService('objectql');
3328
expect(objectql).toBeDefined();
34-
// metadata and objectql are distinct service instances
35-
expect(metadataService).not.toBe(objectql);
29+
expect(kernel.getService('data')).toBeDefined();
30+
expect(kernel.getService('protocol')).toBeDefined();
31+
// metadata is provided by kernel's core fallback, not ObjectQL
32+
const metadataService = kernel.getService('metadata');
33+
expect(metadataService).toBeDefined();
34+
expect((metadataService as any)._fallback).toBe(true);
3635
});
3736

3837
it('should serve in-memory metadata definitions', async () => {

packages/objectql/src/plugin.ts

Lines changed: 3 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

33
import { ObjectQL } from './engine.js';
4-
import { MetadataFacade } from './metadata-facade.js';
54
import { ObjectStackProtocolImplementation } from './protocol.js';
65
import { Plugin, PluginContext } from '@objectstack/core';
76

@@ -33,40 +32,7 @@ export class ObjectQLPlugin implements Plugin {
3332

3433
// Register as provider for Core Kernel Services
3534
ctx.registerService('objectql', this.ql);
36-
37-
// Register MetadataFacade as metadata service (unless external service exists)
38-
let hasMetadata = false;
39-
let metadataProvider = 'objectql';
40-
try {
41-
if (ctx.getService('metadata')) {
42-
hasMetadata = true;
43-
metadataProvider = 'external';
44-
}
45-
} catch (e: any) {
46-
// Ignore errors during check (e.g. "Service is async")
47-
}
4835

49-
if (!hasMetadata) {
50-
try {
51-
const metadataFacade = new MetadataFacade();
52-
ctx.registerService('metadata', metadataFacade);
53-
ctx.logger.info('MetadataFacade registered as metadata service', {
54-
mode: 'in-memory',
55-
features: ['registry', 'fast-lookup']
56-
});
57-
} catch (e: any) {
58-
// Ignore if already registered (race condition or async mis-detection)
59-
if (!e.message?.includes('already registered')) {
60-
throw e;
61-
}
62-
}
63-
} else {
64-
ctx.logger.info('External metadata service detected', {
65-
provider: metadataProvider,
66-
mode: 'will-sync-in-start-phase'
67-
});
68-
}
69-
7036
ctx.registerService('data', this.ql); // ObjectQL implements IDataEngine
7137

7238
// Register manifest service for direct app/package registration.
@@ -84,7 +50,6 @@ export class ObjectQLPlugin implements Plugin {
8450

8551
ctx.logger.info('ObjectQL engine registered', {
8652
services: ['objectql', 'data', 'manifest'],
87-
metadataProvider: metadataProvider
8853
});
8954

9055
// Register Protocol Implementation
@@ -99,16 +64,14 @@ export class ObjectQLPlugin implements Plugin {
9964

10065
start = async (ctx: PluginContext) => {
10166
ctx.logger.info('ObjectQL engine starting...');
102-
103-
// Check if we should load from external metadata service
67+
68+
// Sync from external metadata service (e.g. MetadataPlugin) if available
10469
try {
10570
const metadataService = ctx.getService('metadata') as any;
106-
// Only sync if metadata service is external (not our own MetadataFacade)
107-
if (metadataService && !(metadataService instanceof MetadataFacade) && this.ql) {
71+
if (metadataService && typeof metadataService.loadMany === 'function' && this.ql) {
10872
await this.loadMetadataFromService(metadataService, ctx);
10973
}
11074
} catch (e: any) {
111-
// No external metadata service or error accessing it
11275
ctx.logger.debug('No external metadata service to sync from');
11376
}
11477

packages/spec/src/system/core-services.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ describe('ServiceCriticalitySchema', () => {
4646

4747
describe('ServiceRequirementDef', () => {
4848
it('should define required services', () => {
49-
expect(ServiceRequirementDef.metadata).toBe('required');
5049
expect(ServiceRequirementDef.data).toBe('required');
50+
expect(ServiceRequirementDef.metadata).toBe('core');
5151
expect(ServiceRequirementDef.auth).toBe('core');
5252
});
5353

packages/spec/src/system/core-services.zod.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,10 @@ export const ServiceCriticalitySchema = z.enum([
5858
*/
5959
export const ServiceRequirementDef = {
6060
// Required: The kernel cannot function without these
61-
metadata: 'required',
6261
data: 'required',
6362

6463
// Core: Highly recommended, defaults to in-memory / no-op if missing
64+
metadata: 'core',
6565
auth: 'core',
6666

6767
// Core: Highly recommended, defaults to in-memory / no-op if missing

0 commit comments

Comments
 (0)