@objectstack/core 4.0.4 → 4.0.5
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/README.md +95 -10
- package/dist/index.cjs +169 -507
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -223
- package/dist/index.d.ts +24 -223
- package/dist/index.js +175 -505
- package/dist/index.js.map +1 -1
- package/dist/logger.cjs +177 -0
- package/dist/logger.cjs.map +1 -0
- package/dist/logger.d.cts +26 -0
- package/dist/logger.d.ts +26 -0
- package/dist/logger.js +158 -0
- package/dist/logger.js.map +1 -0
- package/package.json +36 -15
- package/.turbo/turbo-build.log +0 -22
- package/ADVANCED_FEATURES.md +0 -380
- package/API_REGISTRY.md +0 -392
- package/CHANGELOG.md +0 -472
- package/PHASE2_IMPLEMENTATION.md +0 -388
- package/REFACTORING_SUMMARY.md +0 -40
- package/examples/api-registry-example.ts +0 -559
- package/examples/kernel-features-example.ts +0 -311
- package/examples/phase2-integration.ts +0 -357
- package/src/api-registry-plugin.test.ts +0 -393
- package/src/api-registry-plugin.ts +0 -89
- package/src/api-registry.test.ts +0 -1089
- package/src/api-registry.ts +0 -739
- package/src/contracts/data-engine.ts +0 -57
- package/src/contracts/http-server.ts +0 -151
- package/src/contracts/logger.ts +0 -72
- package/src/dependency-resolver.test.ts +0 -287
- package/src/dependency-resolver.ts +0 -390
- package/src/fallbacks/fallbacks.test.ts +0 -281
- package/src/fallbacks/index.ts +0 -26
- package/src/fallbacks/memory-cache.ts +0 -34
- package/src/fallbacks/memory-i18n.ts +0 -112
- package/src/fallbacks/memory-job.ts +0 -23
- package/src/fallbacks/memory-metadata.ts +0 -50
- package/src/fallbacks/memory-queue.ts +0 -28
- package/src/health-monitor.test.ts +0 -81
- package/src/health-monitor.ts +0 -318
- package/src/hot-reload.ts +0 -382
- package/src/index.ts +0 -50
- package/src/kernel-base.ts +0 -273
- package/src/kernel.test.ts +0 -624
- package/src/kernel.ts +0 -631
- package/src/lite-kernel.test.ts +0 -248
- package/src/lite-kernel.ts +0 -137
- package/src/logger.test.ts +0 -116
- package/src/logger.ts +0 -355
- package/src/namespace-resolver.test.ts +0 -130
- package/src/namespace-resolver.ts +0 -188
- package/src/package-manager.test.ts +0 -225
- package/src/package-manager.ts +0 -428
- package/src/plugin-loader.test.ts +0 -421
- package/src/plugin-loader.ts +0 -484
- package/src/qa/adapter.ts +0 -16
- package/src/qa/http-adapter.ts +0 -116
- package/src/qa/index.ts +0 -5
- package/src/qa/runner.ts +0 -189
- package/src/security/index.ts +0 -50
- package/src/security/permission-manager.test.ts +0 -256
- package/src/security/permission-manager.ts +0 -338
- package/src/security/plugin-config-validator.test.ts +0 -276
- package/src/security/plugin-config-validator.ts +0 -193
- package/src/security/plugin-permission-enforcer.test.ts +0 -251
- package/src/security/plugin-permission-enforcer.ts +0 -436
- package/src/security/plugin-signature-verifier.ts +0 -403
- package/src/security/sandbox-runtime.ts +0 -462
- package/src/security/security-scanner.ts +0 -367
- package/src/types.ts +0 -120
- package/src/utils/env.test.ts +0 -62
- package/src/utils/env.ts +0 -53
- package/tsconfig.json +0 -10
- package/vitest.config.ts +0 -10
package/src/logger.ts
DELETED
|
@@ -1,355 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
import type { LoggerConfig, LogLevel } from '@objectstack/spec/system';
|
|
4
|
-
import type { Logger } from '@objectstack/spec/contracts';
|
|
5
|
-
import { isNode } from './utils/env.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Universal Logger Implementation
|
|
9
|
-
*
|
|
10
|
-
* A configurable logger that works in both browser and Node.js environments.
|
|
11
|
-
* - Node.js: Uses Pino for high-performance structured logging
|
|
12
|
-
* - Browser: Simple console-based implementation
|
|
13
|
-
*
|
|
14
|
-
* Features:
|
|
15
|
-
* - Structured logging with multiple formats (json, text, pretty)
|
|
16
|
-
* - Log level filtering
|
|
17
|
-
* - Sensitive data redaction
|
|
18
|
-
* - File logging with rotation (Node.js only via Pino)
|
|
19
|
-
* - Browser console integration
|
|
20
|
-
* - Distributed tracing support (traceId, spanId)
|
|
21
|
-
*/
|
|
22
|
-
export class ObjectLogger implements Logger {
|
|
23
|
-
private config: Required<Omit<LoggerConfig, 'file' | 'rotation' | 'name'>> & { file?: string; rotation?: { maxSize: string; maxFiles: number }; name?: string };
|
|
24
|
-
private isNode: boolean;
|
|
25
|
-
private pinoLogger?: any; // Pino logger instance for Node.js
|
|
26
|
-
private pinoInstance?: any; // Base Pino instance for creating child loggers
|
|
27
|
-
private require?: any; // CommonJS require function for Node.js
|
|
28
|
-
|
|
29
|
-
constructor(config: Partial<LoggerConfig> = {}) {
|
|
30
|
-
// Detect runtime environment
|
|
31
|
-
this.isNode = isNode;
|
|
32
|
-
|
|
33
|
-
// Set defaults
|
|
34
|
-
this.config = {
|
|
35
|
-
name: config.name,
|
|
36
|
-
level: config.level ?? 'info',
|
|
37
|
-
format: config.format ?? (this.isNode ? 'json' : 'pretty'),
|
|
38
|
-
redact: config.redact ?? ['password', 'token', 'secret', 'key'],
|
|
39
|
-
sourceLocation: config.sourceLocation ?? false,
|
|
40
|
-
file: config.file,
|
|
41
|
-
rotation: config.rotation ?? {
|
|
42
|
-
maxSize: '10m',
|
|
43
|
-
maxFiles: 5
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
// Initialize Pino logger for Node.js
|
|
48
|
-
if (this.isNode) {
|
|
49
|
-
this.initPinoLogger();
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Initialize Pino logger for Node.js
|
|
55
|
-
*/
|
|
56
|
-
private async initPinoLogger() {
|
|
57
|
-
if (!this.isNode) return;
|
|
58
|
-
|
|
59
|
-
try {
|
|
60
|
-
// Create require function dynamically for Node.js (avoids bundling issues in browser)
|
|
61
|
-
// @ts-ignore - dynamic import of Node.js module
|
|
62
|
-
const { createRequire } = await import('module');
|
|
63
|
-
this.require = createRequire(import.meta.url);
|
|
64
|
-
|
|
65
|
-
// Synchronous import for Pino using createRequire (works in ESM)
|
|
66
|
-
const pino = this.require('pino');
|
|
67
|
-
|
|
68
|
-
// Build Pino options
|
|
69
|
-
const pinoOptions: any = {
|
|
70
|
-
level: this.config.level,
|
|
71
|
-
redact: {
|
|
72
|
-
paths: this.config.redact,
|
|
73
|
-
censor: '***REDACTED***'
|
|
74
|
-
}
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
// Add name if provided
|
|
78
|
-
if (this.config.name) {
|
|
79
|
-
pinoOptions.name = this.config.name;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Transport configuration for pretty printing or file output
|
|
83
|
-
const targets: any[] = [];
|
|
84
|
-
|
|
85
|
-
// Console transport
|
|
86
|
-
if (this.config.format === 'pretty') {
|
|
87
|
-
// Check if pino-pretty is available
|
|
88
|
-
let hasPretty = false;
|
|
89
|
-
try {
|
|
90
|
-
this.require.resolve('pino-pretty');
|
|
91
|
-
hasPretty = true;
|
|
92
|
-
} catch (e) {
|
|
93
|
-
// ignore
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (hasPretty) {
|
|
97
|
-
targets.push({
|
|
98
|
-
target: 'pino-pretty',
|
|
99
|
-
options: {
|
|
100
|
-
colorize: true,
|
|
101
|
-
translateTime: 'SYS:standard',
|
|
102
|
-
ignore: 'pid,hostname'
|
|
103
|
-
},
|
|
104
|
-
level: this.config.level
|
|
105
|
-
});
|
|
106
|
-
} else {
|
|
107
|
-
console.warn('[Logger] pino-pretty not found. Install it for pretty logging: pnpm add -D pino-pretty');
|
|
108
|
-
// Fallback to text/simple
|
|
109
|
-
targets.push({
|
|
110
|
-
target: 'pino/file',
|
|
111
|
-
options: { destination: 1 },
|
|
112
|
-
level: this.config.level
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
} else if (this.config.format === 'json') {
|
|
116
|
-
// JSON to stdout
|
|
117
|
-
targets.push({
|
|
118
|
-
target: 'pino/file',
|
|
119
|
-
options: { destination: 1 }, // stdout
|
|
120
|
-
level: this.config.level
|
|
121
|
-
});
|
|
122
|
-
} else {
|
|
123
|
-
// text format (simple)
|
|
124
|
-
targets.push({
|
|
125
|
-
target: 'pino/file',
|
|
126
|
-
options: { destination: 1 },
|
|
127
|
-
level: this.config.level
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// File transport (if configured)
|
|
132
|
-
if (this.config.file) {
|
|
133
|
-
targets.push({
|
|
134
|
-
target: 'pino/file',
|
|
135
|
-
options: {
|
|
136
|
-
destination: this.config.file,
|
|
137
|
-
mkdir: true
|
|
138
|
-
},
|
|
139
|
-
level: this.config.level
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Create transport
|
|
144
|
-
if (targets.length > 0) {
|
|
145
|
-
pinoOptions.transport = targets.length === 1 ? targets[0] : { targets };
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Create Pino logger
|
|
149
|
-
this.pinoInstance = pino(pinoOptions);
|
|
150
|
-
this.pinoLogger = this.pinoInstance;
|
|
151
|
-
|
|
152
|
-
} catch (error) {
|
|
153
|
-
// Fallback to console if Pino is not available
|
|
154
|
-
console.warn('[Logger] Pino not available, falling back to console:', error);
|
|
155
|
-
this.pinoLogger = null;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Redact sensitive keys from context object (for browser)
|
|
161
|
-
*/
|
|
162
|
-
private redactSensitive(obj: any): any {
|
|
163
|
-
if (!obj || typeof obj !== 'object') return obj;
|
|
164
|
-
|
|
165
|
-
const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
|
|
166
|
-
|
|
167
|
-
for (const key in redacted) {
|
|
168
|
-
const lowerKey = key.toLowerCase();
|
|
169
|
-
const shouldRedact = this.config.redact.some((pattern: string) =>
|
|
170
|
-
lowerKey.includes(pattern.toLowerCase())
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
if (shouldRedact) {
|
|
174
|
-
redacted[key] = '***REDACTED***';
|
|
175
|
-
} else if (typeof redacted[key] === 'object' && redacted[key] !== null) {
|
|
176
|
-
redacted[key] = this.redactSensitive(redacted[key]);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return redacted;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Format log entry for browser
|
|
185
|
-
*/
|
|
186
|
-
private formatBrowserLog(level: LogLevel, message: string, context?: Record<string, any>): string {
|
|
187
|
-
if (this.config.format === 'json') {
|
|
188
|
-
return JSON.stringify({
|
|
189
|
-
timestamp: new Date().toISOString(),
|
|
190
|
-
level,
|
|
191
|
-
message,
|
|
192
|
-
...context
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (this.config.format === 'text') {
|
|
197
|
-
const parts = [new Date().toISOString(), level.toUpperCase(), message];
|
|
198
|
-
if (context && Object.keys(context).length > 0) {
|
|
199
|
-
parts.push(JSON.stringify(context));
|
|
200
|
-
}
|
|
201
|
-
return parts.join(' | ');
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Pretty format
|
|
205
|
-
const levelColors: Record<LogLevel, string> = {
|
|
206
|
-
debug: '\x1b[36m', // Cyan
|
|
207
|
-
info: '\x1b[32m', // Green
|
|
208
|
-
warn: '\x1b[33m', // Yellow
|
|
209
|
-
error: '\x1b[31m', // Red
|
|
210
|
-
fatal: '\x1b[35m', // Magenta
|
|
211
|
-
silent: ''
|
|
212
|
-
};
|
|
213
|
-
const reset = '\x1b[0m';
|
|
214
|
-
const color = levelColors[level] || '';
|
|
215
|
-
|
|
216
|
-
let output = `${color}[${level.toUpperCase()}]${reset} ${message}`;
|
|
217
|
-
|
|
218
|
-
if (context && Object.keys(context).length > 0) {
|
|
219
|
-
output += ` ${JSON.stringify(context, null, 2)}`;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return output;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Log using browser console
|
|
227
|
-
*/
|
|
228
|
-
private logBrowser(level: LogLevel, message: string, context?: Record<string, any>, error?: Error) {
|
|
229
|
-
const redactedContext = context ? this.redactSensitive(context) : undefined;
|
|
230
|
-
const mergedContext = error ? { ...redactedContext, error: { message: error.message, stack: error.stack } } : redactedContext;
|
|
231
|
-
|
|
232
|
-
const formatted = this.formatBrowserLog(level, message, mergedContext);
|
|
233
|
-
|
|
234
|
-
const consoleMethod = level === 'debug' ? 'debug' :
|
|
235
|
-
level === 'info' ? 'log' :
|
|
236
|
-
level === 'warn' ? 'warn' :
|
|
237
|
-
level === 'error' || level === 'fatal' ? 'error' :
|
|
238
|
-
'log';
|
|
239
|
-
|
|
240
|
-
console[consoleMethod](formatted);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Public logging methods
|
|
245
|
-
*/
|
|
246
|
-
debug(message: string, meta?: Record<string, any>): void {
|
|
247
|
-
if (this.isNode && this.pinoLogger) {
|
|
248
|
-
this.pinoLogger.debug(meta || {}, message);
|
|
249
|
-
} else {
|
|
250
|
-
this.logBrowser('debug', message, meta);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
info(message: string, meta?: Record<string, any>): void {
|
|
255
|
-
if (this.isNode && this.pinoLogger) {
|
|
256
|
-
this.pinoLogger.info(meta || {}, message);
|
|
257
|
-
} else {
|
|
258
|
-
this.logBrowser('info', message, meta);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
warn(message: string, meta?: Record<string, any>): void {
|
|
263
|
-
if (this.isNode && this.pinoLogger) {
|
|
264
|
-
this.pinoLogger.warn(meta || {}, message);
|
|
265
|
-
} else {
|
|
266
|
-
this.logBrowser('warn', message, meta);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
error(message: string, errorOrMeta?: Error | Record<string, any>, meta?: Record<string, any>): void {
|
|
271
|
-
let error: Error | undefined;
|
|
272
|
-
let context: Record<string, any> = {};
|
|
273
|
-
|
|
274
|
-
if (errorOrMeta instanceof Error) {
|
|
275
|
-
error = errorOrMeta;
|
|
276
|
-
context = meta || {};
|
|
277
|
-
} else {
|
|
278
|
-
context = errorOrMeta || {};
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
if (this.isNode && this.pinoLogger) {
|
|
282
|
-
const errorContext = error ? { err: error, ...context } : context;
|
|
283
|
-
this.pinoLogger.error(errorContext, message);
|
|
284
|
-
} else {
|
|
285
|
-
this.logBrowser('error', message, context, error);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
fatal(message: string, errorOrMeta?: Error | Record<string, any>, meta?: Record<string, any>): void {
|
|
290
|
-
let error: Error | undefined;
|
|
291
|
-
let context: Record<string, any> = {};
|
|
292
|
-
|
|
293
|
-
if (errorOrMeta instanceof Error) {
|
|
294
|
-
error = errorOrMeta;
|
|
295
|
-
context = meta || {};
|
|
296
|
-
} else {
|
|
297
|
-
context = errorOrMeta || {};
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
if (this.isNode && this.pinoLogger) {
|
|
301
|
-
const errorContext = error ? { err: error, ...context } : context;
|
|
302
|
-
this.pinoLogger.fatal(errorContext, message);
|
|
303
|
-
} else {
|
|
304
|
-
this.logBrowser('fatal', message, context, error);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Create a child logger with additional context
|
|
310
|
-
* Note: Child loggers share the parent's Pino instance
|
|
311
|
-
*/
|
|
312
|
-
child(context: Record<string, any>): ObjectLogger {
|
|
313
|
-
const childLogger = new ObjectLogger(this.config);
|
|
314
|
-
|
|
315
|
-
// For Node.js with Pino, create a Pino child logger
|
|
316
|
-
if (this.isNode && this.pinoInstance) {
|
|
317
|
-
childLogger.pinoLogger = this.pinoInstance.child(context);
|
|
318
|
-
childLogger.pinoInstance = this.pinoInstance;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
return childLogger;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* Set trace context for distributed tracing
|
|
326
|
-
*/
|
|
327
|
-
withTrace(traceId: string, spanId?: string): ObjectLogger {
|
|
328
|
-
return this.child({ traceId, spanId });
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* Cleanup resources
|
|
333
|
-
*/
|
|
334
|
-
async destroy(): Promise<void> {
|
|
335
|
-
if (this.pinoLogger && this.pinoLogger.flush) {
|
|
336
|
-
await new Promise<void>((resolve) => {
|
|
337
|
-
this.pinoLogger.flush(() => resolve());
|
|
338
|
-
});
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* Compatibility method for console.log usage
|
|
344
|
-
*/
|
|
345
|
-
log(message: string, ...args: any[]): void {
|
|
346
|
-
this.info(message, args.length > 0 ? { args } : undefined);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
/**
|
|
351
|
-
* Create a logger instance
|
|
352
|
-
*/
|
|
353
|
-
export function createLogger(config?: Partial<LoggerConfig>): ObjectLogger {
|
|
354
|
-
return new ObjectLogger(config);
|
|
355
|
-
}
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import { NamespaceResolver } from './namespace-resolver.js';
|
|
3
|
-
import { createLogger } from './logger.js';
|
|
4
|
-
|
|
5
|
-
describe('NamespaceResolver', () => {
|
|
6
|
-
let resolver: NamespaceResolver;
|
|
7
|
-
|
|
8
|
-
beforeEach(() => {
|
|
9
|
-
const logger = createLogger({ level: 'silent' });
|
|
10
|
-
resolver = new NamespaceResolver(logger);
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
describe('register', () => {
|
|
14
|
-
it('should register namespaces for a package', () => {
|
|
15
|
-
resolver.register('pkg-a', ['objects.task', 'views.task_list']);
|
|
16
|
-
|
|
17
|
-
const registry = resolver.getRegistry();
|
|
18
|
-
expect(registry.size).toBe(2);
|
|
19
|
-
expect(registry.get('objects.task')?.packageId).toBe('pkg-a');
|
|
20
|
-
expect(registry.get('views.task_list')?.packageId).toBe('pkg-a');
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('should overwrite namespace when same package re-registers', () => {
|
|
24
|
-
resolver.register('pkg-a', ['objects.task']);
|
|
25
|
-
resolver.register('pkg-a', ['objects.task']);
|
|
26
|
-
|
|
27
|
-
expect(resolver.getRegistry().size).toBe(1);
|
|
28
|
-
expect(resolver.getRegistry().get('objects.task')?.packageId).toBe('pkg-a');
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
describe('unregister', () => {
|
|
33
|
-
it('should remove all namespaces for a package', () => {
|
|
34
|
-
resolver.register('pkg-a', ['objects.task', 'views.task_list']);
|
|
35
|
-
resolver.register('pkg-b', ['objects.project']);
|
|
36
|
-
|
|
37
|
-
const removed = resolver.unregister('pkg-a');
|
|
38
|
-
expect(removed).toEqual(['objects.task', 'views.task_list']);
|
|
39
|
-
expect(resolver.getRegistry().size).toBe(1);
|
|
40
|
-
expect(resolver.getRegistry().has('objects.project')).toBe(true);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('should return empty array for unknown package', () => {
|
|
44
|
-
const removed = resolver.unregister('unknown');
|
|
45
|
-
expect(removed).toEqual([]);
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
describe('checkAvailability', () => {
|
|
50
|
-
it('should report no conflicts for unused namespaces', () => {
|
|
51
|
-
const result = resolver.checkAvailability('pkg-a', ['objects.task']);
|
|
52
|
-
expect(result.available).toBe(true);
|
|
53
|
-
expect(result.conflicts).toHaveLength(0);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('should detect conflict with another package', () => {
|
|
57
|
-
resolver.register('pkg-a', ['objects.task']);
|
|
58
|
-
|
|
59
|
-
const result = resolver.checkAvailability('pkg-b', ['objects.task']);
|
|
60
|
-
expect(result.available).toBe(false);
|
|
61
|
-
expect(result.conflicts).toHaveLength(1);
|
|
62
|
-
expect(result.conflicts[0].namespace).toBe('objects.task');
|
|
63
|
-
expect(result.conflicts[0].existingPackageId).toBe('pkg-a');
|
|
64
|
-
expect(result.conflicts[0].incomingPackageId).toBe('pkg-b');
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('should not conflict with own namespaces', () => {
|
|
68
|
-
resolver.register('pkg-a', ['objects.task']);
|
|
69
|
-
|
|
70
|
-
const result = resolver.checkAvailability('pkg-a', ['objects.task']);
|
|
71
|
-
expect(result.available).toBe(true);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('should provide suggestions for conflicts', () => {
|
|
75
|
-
resolver.register('pkg-a', ['objects.task']);
|
|
76
|
-
|
|
77
|
-
const result = resolver.checkAvailability('@myorg/plugin-crm', ['objects.task']);
|
|
78
|
-
expect(result.available).toBe(false);
|
|
79
|
-
expect(result.suggestions['objects.task']).toBeDefined();
|
|
80
|
-
expect(result.suggestions['objects.task']).toContain('crm');
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
describe('extractNamespaces', () => {
|
|
85
|
-
it('should extract namespaces from object-style metadata', () => {
|
|
86
|
-
const config = {
|
|
87
|
-
objects: { task: {}, project: {} },
|
|
88
|
-
views: { task_list: {} },
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
const ns = resolver.extractNamespaces(config);
|
|
92
|
-
expect(ns).toContain('objects.task');
|
|
93
|
-
expect(ns).toContain('objects.project');
|
|
94
|
-
expect(ns).toContain('views.task_list');
|
|
95
|
-
expect(ns).toHaveLength(3);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('should extract namespaces from array-style metadata', () => {
|
|
99
|
-
const config = {
|
|
100
|
-
objects: [{ name: 'task' }, { name: 'project' }],
|
|
101
|
-
flows: [{ name: 'approval_flow' }],
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
const ns = resolver.extractNamespaces(config);
|
|
105
|
-
expect(ns).toContain('objects.task');
|
|
106
|
-
expect(ns).toContain('objects.project');
|
|
107
|
-
expect(ns).toContain('flows.approval_flow');
|
|
108
|
-
expect(ns).toHaveLength(3);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('should return empty array for empty config', () => {
|
|
112
|
-
const ns = resolver.extractNamespaces({});
|
|
113
|
-
expect(ns).toEqual([]);
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
describe('getPackageNamespaces', () => {
|
|
118
|
-
it('should return namespaces for a specific package', () => {
|
|
119
|
-
resolver.register('pkg-a', ['objects.task', 'views.task_list']);
|
|
120
|
-
resolver.register('pkg-b', ['objects.project']);
|
|
121
|
-
|
|
122
|
-
expect(resolver.getPackageNamespaces('pkg-a')).toEqual(['objects.task', 'views.task_list']);
|
|
123
|
-
expect(resolver.getPackageNamespaces('pkg-b')).toEqual(['objects.project']);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('should return empty for unknown package', () => {
|
|
127
|
-
expect(resolver.getPackageNamespaces('unknown')).toEqual([]);
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
});
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
import type { ObjectLogger } from './logger.js';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Namespace entry representing an object/view/flow etc. registered by a package.
|
|
7
|
-
*/
|
|
8
|
-
export interface NamespaceEntry {
|
|
9
|
-
/** The namespace path (e.g. "objects.project_task", "views.task_list") */
|
|
10
|
-
namespace: string;
|
|
11
|
-
/** The package that owns this namespace */
|
|
12
|
-
packageId: string;
|
|
13
|
-
/** When this entry was registered */
|
|
14
|
-
registeredAt: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Result of a namespace conflict check.
|
|
19
|
-
*/
|
|
20
|
-
export interface NamespaceConflict {
|
|
21
|
-
/** The conflicting namespace path */
|
|
22
|
-
namespace: string;
|
|
23
|
-
/** The package that currently owns this namespace */
|
|
24
|
-
existingPackageId: string;
|
|
25
|
-
/** The package attempting to register the same namespace */
|
|
26
|
-
incomingPackageId: string;
|
|
27
|
-
/** A suggested alternative name to avoid the conflict */
|
|
28
|
-
suggestion?: string;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Result of namespace availability check.
|
|
33
|
-
*/
|
|
34
|
-
export interface NamespaceCheckResult {
|
|
35
|
-
/** Whether all requested namespaces are available */
|
|
36
|
-
available: boolean;
|
|
37
|
-
/** List of conflicts detected */
|
|
38
|
-
conflicts: NamespaceConflict[];
|
|
39
|
-
/** Suggested alternatives for each conflict */
|
|
40
|
-
suggestions: Record<string, string>;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Namespace Resolver
|
|
45
|
-
*
|
|
46
|
-
* Manages namespace registration for installed packages and detects collisions
|
|
47
|
-
* during install-time. Each metadata item (object, view, flow, page, etc.)
|
|
48
|
-
* produces a namespace like `objects.<name>` or `views.<name>`.
|
|
49
|
-
*
|
|
50
|
-
* When a new package declares objects, views, or other metadata that would
|
|
51
|
-
* collide with an existing package's metadata, this resolver reports the
|
|
52
|
-
* conflicts and suggests prefixed alternatives.
|
|
53
|
-
*/
|
|
54
|
-
export class NamespaceResolver {
|
|
55
|
-
private logger: ObjectLogger;
|
|
56
|
-
private registry: Map<string, NamespaceEntry> = new Map();
|
|
57
|
-
|
|
58
|
-
constructor(logger: ObjectLogger) {
|
|
59
|
-
this.logger = logger.child({ component: 'NamespaceResolver' });
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Register namespaces owned by a package.
|
|
64
|
-
*/
|
|
65
|
-
register(packageId: string, namespaces: string[]): void {
|
|
66
|
-
const now = new Date().toISOString();
|
|
67
|
-
for (const ns of namespaces) {
|
|
68
|
-
if (this.registry.has(ns)) {
|
|
69
|
-
const existing = this.registry.get(ns)!;
|
|
70
|
-
if (existing.packageId !== packageId) {
|
|
71
|
-
this.logger.warn('Overwriting namespace entry', { namespace: ns, existing: existing.packageId, incoming: packageId });
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
this.registry.set(ns, { namespace: ns, packageId, registeredAt: now });
|
|
75
|
-
this.logger.debug('Namespace registered', { namespace: ns, packageId });
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Unregister all namespaces belonging to a package.
|
|
81
|
-
*/
|
|
82
|
-
unregister(packageId: string): string[] {
|
|
83
|
-
const removed: string[] = [];
|
|
84
|
-
for (const [ns, entry] of this.registry) {
|
|
85
|
-
if (entry.packageId === packageId) {
|
|
86
|
-
this.registry.delete(ns);
|
|
87
|
-
removed.push(ns);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
this.logger.debug('Namespaces unregistered', { packageId, count: removed.length });
|
|
91
|
-
return removed;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Check whether a set of namespaces is available for a given package.
|
|
96
|
-
*/
|
|
97
|
-
checkAvailability(packageId: string, namespaces: string[]): NamespaceCheckResult {
|
|
98
|
-
const conflicts: NamespaceConflict[] = [];
|
|
99
|
-
const suggestions: Record<string, string> = {};
|
|
100
|
-
|
|
101
|
-
for (const ns of namespaces) {
|
|
102
|
-
const existing = this.registry.get(ns);
|
|
103
|
-
if (existing && existing.packageId !== packageId) {
|
|
104
|
-
const suggestion = this.suggestAlternative(ns, packageId);
|
|
105
|
-
conflicts.push({
|
|
106
|
-
namespace: ns,
|
|
107
|
-
existingPackageId: existing.packageId,
|
|
108
|
-
incomingPackageId: packageId,
|
|
109
|
-
suggestion,
|
|
110
|
-
});
|
|
111
|
-
suggestions[ns] = suggestion;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return {
|
|
116
|
-
available: conflicts.length === 0,
|
|
117
|
-
conflicts,
|
|
118
|
-
suggestions,
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Extract namespace strings from a package's metadata definition.
|
|
124
|
-
*/
|
|
125
|
-
extractNamespaces(config: Record<string, unknown>): string[] {
|
|
126
|
-
const namespaces: string[] = [];
|
|
127
|
-
const categories = [
|
|
128
|
-
'objects', 'views', 'pages', 'flows', 'workflows',
|
|
129
|
-
'apps', 'dashboards', 'reports', 'actions', 'agents',
|
|
130
|
-
];
|
|
131
|
-
|
|
132
|
-
for (const category of categories) {
|
|
133
|
-
const items = config[category];
|
|
134
|
-
if (Array.isArray(items)) {
|
|
135
|
-
for (const item of items) {
|
|
136
|
-
const name = (item as Record<string, unknown>)?.name;
|
|
137
|
-
if (typeof name === 'string') {
|
|
138
|
-
namespaces.push(`${category}.${name}`);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
} else if (items && typeof items === 'object') {
|
|
142
|
-
for (const key of Object.keys(items as object)) {
|
|
143
|
-
namespaces.push(`${category}.${key}`);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return namespaces;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Get all registered entries.
|
|
153
|
-
*/
|
|
154
|
-
getRegistry(): ReadonlyMap<string, NamespaceEntry> {
|
|
155
|
-
return this.registry;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Get all namespaces belonging to a specific package.
|
|
160
|
-
*/
|
|
161
|
-
getPackageNamespaces(packageId: string): string[] {
|
|
162
|
-
const namespaces: string[] = [];
|
|
163
|
-
for (const [ns, entry] of this.registry) {
|
|
164
|
-
if (entry.packageId === packageId) {
|
|
165
|
-
namespaces.push(ns);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
return namespaces;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Generate a prefixed alternative namespace to avoid conflicts.
|
|
173
|
-
*/
|
|
174
|
-
private suggestAlternative(ns: string, packageId: string): string {
|
|
175
|
-
// Extract the short package name for prefixing
|
|
176
|
-
const shortName = packageId
|
|
177
|
-
.replace(/^@[^/]+\//, '')
|
|
178
|
-
.replace(/^plugin-/, '')
|
|
179
|
-
.replace(/-/g, '_');
|
|
180
|
-
|
|
181
|
-
const parts = ns.split('.');
|
|
182
|
-
if (parts.length >= 2) {
|
|
183
|
-
// e.g. "objects.task" → "objects.crm_task"
|
|
184
|
-
return `${parts[0]}.${shortName}_${parts.slice(1).join('.')}`;
|
|
185
|
-
}
|
|
186
|
-
return `${shortName}_${ns}`;
|
|
187
|
-
}
|
|
188
|
-
}
|