@objectstack/runtime 0.9.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # @objectstack/runtime
2
2
 
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - Major version release for ObjectStack Protocol v1.0.
8
+ - Stabilized Protocol Definitions
9
+ - Enhanced Runtime Plugin Support
10
+ - Fixed Type Compliance across Monorepo
11
+
12
+ ### Patch Changes
13
+
14
+ - Updated dependencies
15
+ - @objectstack/spec@1.0.0
16
+ - @objectstack/core@1.0.0
17
+ - @objectstack/types@1.0.0
18
+
3
19
  ## 0.9.2
4
20
 
5
21
  ### Patch Changes
@@ -0,0 +1,16 @@
1
+ import { Plugin } from '@objectstack/core';
2
+ import { RestServerConfig } from '@objectstack/spec/api';
3
+ export interface ApiRegistryConfig {
4
+ serverServiceName?: string;
5
+ protocolServiceName?: string;
6
+ api?: RestServerConfig;
7
+ }
8
+ /**
9
+ * ApiRegistryPlugin
10
+ *
11
+ * Responsibilities:
12
+ * 1. Consumes 'http.server' (or configured service)
13
+ * 2. Consumes 'protocol' (ObjectStackProtocol)
14
+ * 3. Instantiates RestServer to auto-generate routes
15
+ */
16
+ export declare function createApiRegistryPlugin(config?: ApiRegistryConfig): Plugin;
@@ -0,0 +1,42 @@
1
+ import { RestServer } from './rest-server.js';
2
+ /**
3
+ * ApiRegistryPlugin
4
+ *
5
+ * Responsibilities:
6
+ * 1. Consumes 'http.server' (or configured service)
7
+ * 2. Consumes 'protocol' (ObjectStackProtocol)
8
+ * 3. Instantiates RestServer to auto-generate routes
9
+ */
10
+ export function createApiRegistryPlugin(config = {}) {
11
+ return {
12
+ name: 'com.objectstack.runtime.api-registry',
13
+ version: '1.0.0',
14
+ init: async (ctx) => {
15
+ // No service registration, this is a consumer plugin
16
+ },
17
+ start: async (ctx) => {
18
+ const serverService = config.serverServiceName || 'http.server';
19
+ const protocolService = config.protocolServiceName || 'protocol';
20
+ const server = ctx.getService(serverService);
21
+ const protocol = ctx.getService(protocolService);
22
+ if (!server) {
23
+ ctx.logger.warn(`ApiRegistryPlugin: HTTP Server service '${serverService}' not found. REST routes skipped.`);
24
+ return;
25
+ }
26
+ if (!protocol) {
27
+ ctx.logger.warn(`ApiRegistryPlugin: Protocol service '${protocolService}' not found. REST routes skipped.`);
28
+ return;
29
+ }
30
+ ctx.logger.info('Hydrating REST API from Protocol...');
31
+ try {
32
+ const restServer = new RestServer(server, protocol, config.api);
33
+ restServer.registerRoutes();
34
+ ctx.logger.info('REST API successfully registered');
35
+ }
36
+ catch (err) {
37
+ ctx.logger.error('Failed to register REST API routes', { error: err.message });
38
+ throw err;
39
+ }
40
+ }
41
+ };
42
+ }
@@ -13,6 +13,6 @@ export declare class AppPlugin implements Plugin {
13
13
  version?: string;
14
14
  private bundle;
15
15
  constructor(bundle: any);
16
- init(ctx: PluginContext): Promise<void>;
17
- start(ctx: PluginContext): Promise<void>;
16
+ init: (ctx: PluginContext) => Promise<void>;
17
+ start: (ctx: PluginContext) => Promise<void>;
18
18
  }
@@ -9,6 +9,67 @@
9
9
  */
10
10
  export class AppPlugin {
11
11
  constructor(bundle) {
12
+ this.init = async (ctx) => {
13
+ const sys = this.bundle.manifest || this.bundle;
14
+ const appId = sys.id || sys.name;
15
+ ctx.logger.info('Registering App Service', {
16
+ appId,
17
+ pluginName: this.name,
18
+ version: this.version
19
+ });
20
+ // Register the app manifest as a service
21
+ // ObjectQLPlugin will discover this and call ql.registerApp()
22
+ const serviceName = `app.${appId}`;
23
+ // Merge manifest with the bundle to ensure objects/apps are accessible at root
24
+ // This supports both Legacy Manifests and new Stack Definitions
25
+ const servicePayload = this.bundle.manifest
26
+ ? { ...this.bundle.manifest, ...this.bundle }
27
+ : this.bundle;
28
+ ctx.registerService(serviceName, servicePayload);
29
+ };
30
+ this.start = async (ctx) => {
31
+ const sys = this.bundle.manifest || this.bundle;
32
+ const appId = sys.id || sys.name;
33
+ // Execute Runtime Step
34
+ // Retrieve ObjectQL engine from services
35
+ // We cast to any/ObjectQL because ctx.getService returns unknown
36
+ const ql = ctx.getService('objectql');
37
+ if (!ql) {
38
+ ctx.logger.warn('ObjectQL engine service not found', {
39
+ appName: this.name,
40
+ appId
41
+ });
42
+ return;
43
+ }
44
+ ctx.logger.debug('Retrieved ObjectQL engine service', { appId });
45
+ const runtime = this.bundle.default || this.bundle;
46
+ if (runtime && typeof runtime.onEnable === 'function') {
47
+ ctx.logger.info('Executing runtime.onEnable', {
48
+ appName: this.name,
49
+ appId
50
+ });
51
+ // Construct the Host Context (mirroring old ObjectQL.use logic)
52
+ const hostContext = {
53
+ ...ctx,
54
+ ql,
55
+ logger: ctx.logger,
56
+ drivers: {
57
+ register: (driver) => {
58
+ ctx.logger.debug('Registering driver via app runtime', {
59
+ driverName: driver.name,
60
+ appId
61
+ });
62
+ ql.registerDriver(driver);
63
+ }
64
+ },
65
+ };
66
+ await runtime.onEnable(hostContext);
67
+ ctx.logger.debug('Runtime.onEnable completed', { appId });
68
+ }
69
+ else {
70
+ ctx.logger.debug('No runtime.onEnable function found', { appId });
71
+ }
72
+ };
12
73
  this.bundle = bundle;
13
74
  // Support both direct manifest (legacy) and Stack Definition (nested manifest)
14
75
  const sys = bundle.manifest || bundle;
@@ -16,65 +77,4 @@ export class AppPlugin {
16
77
  this.name = `plugin.app.${appId}`;
17
78
  this.version = sys.version;
18
79
  }
19
- async init(ctx) {
20
- const sys = this.bundle.manifest || this.bundle;
21
- const appId = sys.id || sys.name;
22
- ctx.logger.info('Registering App Service', {
23
- appId,
24
- pluginName: this.name,
25
- version: this.version
26
- });
27
- // Register the app manifest as a service
28
- // ObjectQLPlugin will discover this and call ql.registerApp()
29
- const serviceName = `app.${appId}`;
30
- // Merge manifest with the bundle to ensure objects/apps are accessible at root
31
- // This supports both Legacy Manifests and new Stack Definitions
32
- const servicePayload = this.bundle.manifest
33
- ? { ...this.bundle.manifest, ...this.bundle }
34
- : this.bundle;
35
- ctx.registerService(serviceName, servicePayload);
36
- }
37
- async start(ctx) {
38
- const sys = this.bundle.manifest || this.bundle;
39
- const appId = sys.id || sys.name;
40
- // Execute Runtime Step
41
- // Retrieve ObjectQL engine from services
42
- // We cast to any/ObjectQL because ctx.getService returns unknown
43
- const ql = ctx.getService('objectql');
44
- if (!ql) {
45
- ctx.logger.warn('ObjectQL engine service not found', {
46
- appName: this.name,
47
- appId
48
- });
49
- return;
50
- }
51
- ctx.logger.debug('Retrieved ObjectQL engine service', { appId });
52
- const runtime = this.bundle.default || this.bundle;
53
- if (runtime && typeof runtime.onEnable === 'function') {
54
- ctx.logger.info('Executing runtime.onEnable', {
55
- appName: this.name,
56
- appId
57
- });
58
- // Construct the Host Context (mirroring old ObjectQL.use logic)
59
- const hostContext = {
60
- ...ctx,
61
- ql,
62
- logger: ctx.logger,
63
- drivers: {
64
- register: (driver) => {
65
- ctx.logger.debug('Registering driver via app runtime', {
66
- driverName: driver.name,
67
- appId
68
- });
69
- ql.registerDriver(driver);
70
- }
71
- },
72
- };
73
- await runtime.onEnable(hostContext);
74
- ctx.logger.debug('Runtime.onEnable completed', { appId });
75
- }
76
- else {
77
- ctx.logger.debug('No runtime.onEnable function found', { appId });
78
- }
79
- }
80
80
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,80 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { AppPlugin } from './app-plugin';
3
+ describe('AppPlugin', () => {
4
+ let mockContext;
5
+ beforeEach(() => {
6
+ mockContext = {
7
+ logger: {
8
+ info: vi.fn(),
9
+ error: vi.fn(),
10
+ warn: vi.fn(),
11
+ debug: vi.fn()
12
+ },
13
+ registerService: vi.fn(),
14
+ getService: vi.fn(),
15
+ getServices: vi.fn()
16
+ };
17
+ });
18
+ it('should initialize with manifest info', () => {
19
+ const bundle = {
20
+ id: 'com.test.app',
21
+ name: 'Test App',
22
+ version: '1.0.0'
23
+ };
24
+ const plugin = new AppPlugin(bundle);
25
+ expect(plugin.name).toBe('plugin.app.com.test.app');
26
+ expect(plugin.version).toBe('1.0.0');
27
+ });
28
+ it('should handle nested stack definition manifest', () => {
29
+ const bundle = {
30
+ manifest: {
31
+ id: 'com.test.stack',
32
+ version: '2.0.0'
33
+ },
34
+ objects: []
35
+ };
36
+ const plugin = new AppPlugin(bundle);
37
+ expect(plugin.name).toBe('plugin.app.com.test.stack');
38
+ expect(plugin.version).toBe('2.0.0');
39
+ });
40
+ it('registerService should register raw manifest in init phase', async () => {
41
+ const bundle = {
42
+ id: 'com.test.simple',
43
+ objects: []
44
+ };
45
+ const plugin = new AppPlugin(bundle);
46
+ await plugin.init(mockContext);
47
+ expect(mockContext.registerService).toHaveBeenCalledWith('app.com.test.simple', bundle);
48
+ });
49
+ it('start should do nothing if no runtime hooks', async () => {
50
+ const bundle = { id: 'com.test.static' };
51
+ const plugin = new AppPlugin(bundle);
52
+ vi.mocked(mockContext.getService).mockReturnValue({}); // Mock ObjectQL exists
53
+ await plugin.start(mockContext);
54
+ // Only logs, no errors
55
+ expect(mockContext.logger.debug).toHaveBeenCalled();
56
+ });
57
+ it('start should invoke onEnable if present', async () => {
58
+ const onEnableSpy = vi.fn();
59
+ const bundle = {
60
+ id: 'com.test.code',
61
+ onEnable: onEnableSpy
62
+ };
63
+ const plugin = new AppPlugin(bundle);
64
+ // Mock ObjectQL engine
65
+ const mockQL = { registry: {} };
66
+ vi.mocked(mockContext.getService).mockReturnValue(mockQL);
67
+ await plugin.start(mockContext);
68
+ expect(onEnableSpy).toHaveBeenCalled();
69
+ // Check context passed to onEnable
70
+ const callArg = onEnableSpy.mock.calls[0][0];
71
+ expect(callArg.ql).toBe(mockQL);
72
+ });
73
+ it('start should warn if objectql not found', async () => {
74
+ const bundle = { id: 'com.test.warn' };
75
+ const plugin = new AppPlugin(bundle);
76
+ vi.mocked(mockContext.getService).mockReturnValue(undefined); // No ObjectQL
77
+ await plugin.start(mockContext);
78
+ expect(mockContext.logger.warn).toHaveBeenCalledWith(expect.stringContaining('ObjectQL engine service not found'), expect.any(Object));
79
+ });
80
+ });
@@ -18,6 +18,6 @@ export declare class DriverPlugin implements Plugin {
18
18
  version: string;
19
19
  private driver;
20
20
  constructor(driver: any, driverName?: string);
21
- init(ctx: PluginContext): Promise<void>;
22
- start(ctx: PluginContext): Promise<void>;
21
+ init: (ctx: PluginContext) => Promise<void>;
22
+ start: (ctx: PluginContext) => Promise<void>;
23
23
  }
@@ -15,21 +15,21 @@
15
15
  export class DriverPlugin {
16
16
  constructor(driver, driverName) {
17
17
  this.version = '1.0.0';
18
+ this.init = async (ctx) => {
19
+ // Register driver as a service instead of directly to objectql
20
+ const serviceName = `driver.${this.driver.name || 'unknown'}`;
21
+ ctx.registerService(serviceName, this.driver);
22
+ ctx.logger.info('Driver service registered', {
23
+ serviceName,
24
+ driverName: this.driver.name,
25
+ driverVersion: this.driver.version
26
+ });
27
+ };
28
+ this.start = async (ctx) => {
29
+ // Drivers don't need start phase, initialization happens in init
30
+ ctx.logger.debug('Driver plugin started', { driverName: this.driver.name || 'unknown' });
31
+ };
18
32
  this.driver = driver;
19
33
  this.name = `com.objectstack.driver.${driverName || driver.name || 'unknown'}`;
20
34
  }
21
- async init(ctx) {
22
- // Register driver as a service instead of directly to objectql
23
- const serviceName = `driver.${this.driver.name || 'unknown'}`;
24
- ctx.registerService(serviceName, this.driver);
25
- ctx.logger.info('Driver service registered', {
26
- serviceName,
27
- driverName: this.driver.name,
28
- driverVersion: this.driver.version
29
- });
30
- }
31
- async start(ctx) {
32
- // Drivers don't need start phase, initialization happens in init
33
- ctx.logger.debug('Driver plugin started', { driverName: this.driver.name || 'unknown' });
34
- }
35
35
  }
@@ -0,0 +1,106 @@
1
+ import { ObjectKernel } from '@objectstack/core';
2
+ export interface HttpProtocolContext {
3
+ request: any;
4
+ response?: any;
5
+ }
6
+ export interface HttpDispatcherResult {
7
+ handled: boolean;
8
+ response?: {
9
+ status: number;
10
+ body?: any;
11
+ headers?: Record<string, string>;
12
+ };
13
+ result?: any;
14
+ }
15
+ export declare class HttpDispatcher {
16
+ private kernel;
17
+ constructor(kernel: ObjectKernel);
18
+ private success;
19
+ private error;
20
+ private ensureBroker;
21
+ /**
22
+ * Generates the discovery JSON response for the API root
23
+ */
24
+ getDiscoveryInfo(prefix: string): {
25
+ name: string;
26
+ version: string;
27
+ environment: string;
28
+ routes: {
29
+ data: string;
30
+ metadata: string;
31
+ auth: string;
32
+ graphql: string | undefined;
33
+ storage: string | undefined;
34
+ analytics: string | undefined;
35
+ hub: string | undefined;
36
+ };
37
+ features: {
38
+ graphql: boolean;
39
+ search: boolean;
40
+ websockets: boolean;
41
+ files: boolean;
42
+ analytics: boolean;
43
+ hub: boolean;
44
+ };
45
+ locale: {
46
+ default: string;
47
+ supported: string[];
48
+ timezone: string;
49
+ };
50
+ };
51
+ /**
52
+ * Handles GraphQL requests
53
+ */
54
+ handleGraphQL(body: {
55
+ query: string;
56
+ variables?: any;
57
+ }, context: HttpProtocolContext): Promise<any>;
58
+ /**
59
+ * Handles Auth requests
60
+ * path: sub-path after /auth/
61
+ */
62
+ handleAuth(path: string, method: string, body: any, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
63
+ /**
64
+ * Handles Metadata requests
65
+ * Standard: /metadata/:type/:name
66
+ * Fallback for backward compat: /metadata (all objects), /metadata/:objectName (get object)
67
+ */
68
+ handleMetadata(path: string, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
69
+ /**
70
+ * Handles Data requests
71
+ * path: sub-path after /data/ (e.g. "contacts", "contacts/123", "contacts/query")
72
+ */
73
+ handleData(path: string, method: string, body: any, query: any, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
74
+ /**
75
+ * Handles Analytics requests
76
+ * path: sub-path after /analytics/
77
+ */
78
+ handleAnalytics(path: string, method: string, body: any, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
79
+ /**
80
+ * Handles Hub requests
81
+ * path: sub-path after /hub/
82
+ */
83
+ handleHub(path: string, method: string, body: any, query: any, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
84
+ /**
85
+ * Handles Storage requests
86
+ * path: sub-path after /storage/
87
+ */
88
+ handleStorage(path: string, method: string, file: any, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
89
+ /**
90
+ * Handles Automation requests
91
+ * path: sub-path after /automation/
92
+ */
93
+ handleAutomation(path: string, method: string, body: any, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
94
+ private getServicesMap;
95
+ private getService;
96
+ private capitalize;
97
+ /**
98
+ * Main Dispatcher Entry Point
99
+ * Routes the request to the appropriate handler based on path and precedence
100
+ */
101
+ dispatch(method: string, path: string, body: any, query: any, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
102
+ /**
103
+ * Handles Custom API Endpoints defined in metadata
104
+ */
105
+ handleApiEndpoint(path: string, method: string, body: any, query: any, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
106
+ }