-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathrest-api-plugin.ts
More file actions
242 lines (213 loc) · 11.5 KB
/
Copy pathrest-api-plugin.ts
File metadata and controls
242 lines (213 loc) · 11.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
import { Plugin, PluginContext, IHttpServer } from '@objectstack/core';
import { RestServer, RestKernelManager } from './rest-server.js';
import { ObjectStackProtocol, RestServerConfig } from '@objectstack/spec/api';
import { registerPackageRoutes } from './package-routes.js';
import { registerExternalDatasourceRoutes } from './external-datasource-routes.js';
import type { PackageService } from '@objectstack/service-package';
export interface RestApiPluginConfig {
serverServiceName?: string;
protocolServiceName?: string;
/**
* Optional override for the kernel-manager service name. When the service
* is registered (by @objectstack/runtime's MultiProjectPlugin), scoped
* routes resolve per-environment protocols at request time.
*/
kernelManagerServiceName?: string;
api?: RestServerConfig;
}
/**
* REST API Plugin
*
* Responsibilities:
* 1. Consumes 'http.server' (or configured service)
* 2. Consumes 'protocol' (ObjectStackProtocol)
* 3. Instantiates RestServer to auto-generate routes
*/
export function createRestApiPlugin(config: RestApiPluginConfig = {}): Plugin {
return {
name: 'com.objectstack.rest.api',
version: '1.0.0',
init: async (_ctx: PluginContext) => {
// No service registration, this is a consumer plugin
},
start: async (ctx: PluginContext) => {
const serverService = config.serverServiceName || 'http.server';
const protocolService = config.protocolServiceName || 'protocol';
let server: IHttpServer | undefined;
let protocol: ObjectStackProtocol | undefined;
try {
server = ctx.getService<IHttpServer>(serverService);
} catch (e) {
// Ignore missing service
}
try {
protocol = ctx.getService<ObjectStackProtocol>(protocolService);
} catch (e) {
// Ignore missing service
}
// Optional — only present when MultiProjectPlugin is mounted. When
// available, RestServer will resolve a per-environment protocol at
// request time for scoped (`/environments/:environmentId/...`) routes.
let kernelManager: RestKernelManager | undefined;
const kernelManagerService = config.kernelManagerServiceName || 'kernel-manager';
try {
kernelManager = ctx.getService<RestKernelManager>(kernelManagerService);
} catch (e) {
// Single-kernel deployment — fall back to the control protocol
}
// Optional — only present in runtime mode. When available,
// RestServer will resolve hostname → environmentId on unscoped
// routes so a remote runtime node can dispatch every request
// to the matching per-environment kernel without requiring callers
// to know the environmentId.
let envRegistry: any;
try {
envRegistry = ctx.getService<any>('env-registry');
} catch (e) {
// Not running in runtime/multi-environment mode — fine.
}
// Optional default-project provider — registered by
// `createSingleEnvironmentPlugin` in single-environment local mode.
// Lets RestServer route bare `/api/v1/data/...` URLs into the
// lone project's kernel.
const defaultEnvironmentIdProvider = (): string | undefined => {
try {
const dp: any = ctx.getService('default-project');
return dp?.environmentId;
} catch { return undefined; }
};
// Auth service resolver — used by RestServer.resolveExecCtx in
// single-kernel deployments where there is no kernelManager.
// Multi-kernel paths look up auth via kernelManager.getOrCreate,
// so this provider is the single-kernel fallback.
const authServiceProvider = async (_environmentId?: string): Promise<any | undefined> => {
try {
return ctx.getService<any>('auth');
} catch { return undefined; }
};
// ObjectQL resolver — single-kernel fallback so resolveExecCtx
// can run sys_member / sys_user_permission_set lookups when
// there is no kernelManager wired (e.g. `pnpm dev:crm`).
const objectQLProvider = async (_environmentId?: string): Promise<any | undefined> => {
try {
return ctx.getService<any>('objectql');
} catch { return undefined; }
};
// Email service resolver — used by POST /email/send. Single-
// kernel deployments resolve from the local kernel; multi-
// tenant paths would resolve via kernelManager.getOrCreate.
const emailServiceProvider = async (_environmentId?: string): Promise<any | undefined> => {
try {
return ctx.getService<any>('email');
} catch { return undefined; }
};
// Sharing service resolver — used by /data/:object/:id/shares.
const sharingServiceProvider = async (_environmentId?: string): Promise<any | undefined> => {
try {
return ctx.getService<any>('sharing');
} catch { return undefined; }
};
// Reports service resolver — used by /reports/* routes.
const reportsServiceProvider = async (_environmentId?: string): Promise<any | undefined> => {
try {
return ctx.getService<any>('reports');
} catch { return undefined; }
};
// Approvals service resolver — used by /approvals/* routes.
const approvalsServiceProvider = async (_environmentId?: string): Promise<any | undefined> => {
try {
return ctx.getService<any>('approvals');
} catch { return undefined; }
};
// Sharing-rule service resolver — used by /sharing/rules/* routes.
const sharingRulesServiceProvider = async (_environmentId?: string): Promise<any | undefined> => {
try {
return ctx.getService<any>('sharingRules');
} catch { return undefined; }
};
// i18n service resolver — used to localize view / action / object
// metadata. Single-kernel fallback so labels and select options
// get translated even without a full multi-tenant kernelManager.
const i18nServiceProvider = async (_environmentId?: string): Promise<any | undefined> => {
try {
return ctx.getService<any>('i18n');
} catch { return undefined; }
};
// Analytics service resolver — used by /analytics/dataset/query
// (ADR-0021 dataset preview/query). Returns undefined when no
// analytics service is registered so the route fails cleanly (501).
const analyticsServiceProvider = async (_environmentId?: string): Promise<any | undefined> => {
try {
return ctx.getService<any>('analytics');
} catch { return undefined; }
};
// Settings service resolver — used by resolveExecCtx to resolve the
// reference timezone/locale (localization manifest) through the 4-tier
// cascade incl. the `OS_LOCALIZATION_TIMEZONE` env override. Returns
// undefined when no settings service is registered (UTC default).
const settingsServiceProvider = async (_environmentId?: string): Promise<any | undefined> => {
try {
return ctx.getService<any>('settings');
} catch { return undefined; }
};
if (!server) {
ctx.logger.warn(`RestApiPlugin: HTTP Server service '${serverService}' not found. REST routes skipped.`);
return;
}
if (!protocol) {
ctx.logger.warn(`RestApiPlugin: Protocol service '${protocolService}' not found. REST routes skipped.`);
return;
}
ctx.logger.info('Hydrating REST API from Protocol...');
try {
const restServer = new RestServer(server, protocol, config.api as any, kernelManager, envRegistry, defaultEnvironmentIdProvider, authServiceProvider, objectQLProvider, emailServiceProvider, sharingServiceProvider, reportsServiceProvider, approvalsServiceProvider, sharingRulesServiceProvider, i18nServiceProvider, analyticsServiceProvider, settingsServiceProvider);
restServer.registerRoutes();
ctx.logger.info('REST API successfully registered');
} catch (err: any) {
ctx.logger.error('Failed to register REST API routes', { error: err.message } as any);
throw err;
}
const basePath = config.api?.api?.basePath || '/api';
const version = config.api?.api?.version || 'v1';
const versionedBase = `${basePath}/${version}`;
const enableProjectScoping = config.api?.api?.enableProjectScoping ?? false;
const projectResolution = config.api?.api?.projectResolution ?? 'auto';
// Register package management routes if the service is available.
try {
const packageService = ctx.getService<PackageService>('package');
if (packageService) {
if (enableProjectScoping && projectResolution === 'required') {
// Only register the scoped variant
registerPackageRoutes(server, packageService, `${versionedBase}/environments/:environmentId`, {
protocol,
});
} else {
registerPackageRoutes(server, packageService, versionedBase, { protocol });
if (enableProjectScoping) {
registerPackageRoutes(server, packageService, `${versionedBase}/environments/:environmentId`, {
protocol,
});
}
}
ctx.logger.info('Package management routes registered');
}
} catch (e) {
// Package service not available, skip
ctx.logger.debug('Package service not available, package routes skipped');
}
// External Datasource Federation routes (ADR-0015): catalog / draft /
// import / validate. Registered unconditionally — they degrade
// gracefully (503) when the `external-datasource` service is absent.
// NOTE: the datasource *lifecycle* routes (ADR-0015 Addendum:
// list / test / create / update / remove) moved to the private
// `@objectstack/datasource-admin` package, which registers its own.
try {
registerExternalDatasourceRoutes(server, ctx, versionedBase);
ctx.logger.info('Datasource federation routes registered');
} catch (e: any) {
ctx.logger.warn('Datasource federation routes registration failed', { error: e?.message });
}
}
};
}