@objectstack/objectql 4.0.3 → 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/src/registry.ts DELETED
@@ -1,716 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import { ServiceObject, ObjectSchema, ObjectOwnership } from '@objectstack/spec/data';
4
- import { ObjectStackManifest, ManifestSchema, InstalledPackage, InstalledPackageSchema } from '@objectstack/spec/kernel';
5
- import { AppSchema } from '@objectstack/spec/ui';
6
-
7
- /**
8
- * Reserved namespaces that do not get FQN prefix applied.
9
- * Objects in these namespaces keep their short names (e.g., "user" not "base__user").
10
- */
11
- export const RESERVED_NAMESPACES = new Set(['base', 'system']);
12
-
13
- /**
14
- * Default priorities for ownership types.
15
- */
16
- export const DEFAULT_OWNER_PRIORITY = 100;
17
- export const DEFAULT_EXTENDER_PRIORITY = 200;
18
-
19
- /**
20
- * Contributor Record
21
- * Tracks how a package contributes to an object (own or extend).
22
- */
23
- export interface ObjectContributor {
24
- packageId: string;
25
- namespace: string;
26
- ownership: ObjectOwnership;
27
- priority: number;
28
- definition: ServiceObject;
29
- }
30
-
31
- /**
32
- * Compute Fully Qualified Name (FQN) for an object.
33
- *
34
- * @param namespace - The package namespace (e.g., "crm", "todo")
35
- * @param shortName - The object's short name (e.g., "task", "account")
36
- * @returns FQN string (e.g., "crm__task") or just shortName for reserved namespaces
37
- *
38
- * @example
39
- * computeFQN('crm', 'account') // => 'crm__account'
40
- * computeFQN('base', 'user') // => 'user' (reserved, no prefix)
41
- * computeFQN(undefined, 'task') // => 'task' (legacy, no namespace)
42
- */
43
- export function computeFQN(namespace: string | undefined, shortName: string): string {
44
- if (!namespace || RESERVED_NAMESPACES.has(namespace)) {
45
- return shortName;
46
- }
47
- return `${namespace}__${shortName}`;
48
- }
49
-
50
- /**
51
- * Parse FQN back to namespace and short name.
52
- *
53
- * @param fqn - Fully qualified name (e.g., "crm__account" or "user")
54
- * @returns { namespace, shortName } - namespace is undefined for unprefixed names
55
- */
56
- export function parseFQN(fqn: string): { namespace: string | undefined; shortName: string } {
57
- const idx = fqn.indexOf('__');
58
- if (idx === -1) {
59
- return { namespace: undefined, shortName: fqn };
60
- }
61
- return {
62
- namespace: fqn.slice(0, idx),
63
- shortName: fqn.slice(idx + 2),
64
- };
65
- }
66
-
67
- /**
68
- * Deep merge two ServiceObject definitions.
69
- * Fields are merged additively. Other props: later value wins.
70
- */
71
- function mergeObjectDefinitions(base: ServiceObject, extension: Partial<ServiceObject>): ServiceObject {
72
- const merged = { ...base };
73
-
74
- // Merge fields additively
75
- if (extension.fields) {
76
- merged.fields = { ...base.fields, ...extension.fields };
77
- }
78
-
79
- // Merge validations additively
80
- if (extension.validations) {
81
- merged.validations = [...(base.validations || []), ...extension.validations];
82
- }
83
-
84
- // Merge indexes additively
85
- if (extension.indexes) {
86
- merged.indexes = [...(base.indexes || []), ...extension.indexes];
87
- }
88
-
89
- // Override scalar props (last writer wins)
90
- if (extension.label !== undefined) merged.label = extension.label;
91
- if (extension.pluralLabel !== undefined) merged.pluralLabel = extension.pluralLabel;
92
- if (extension.description !== undefined) merged.description = extension.description;
93
-
94
- return merged;
95
- }
96
-
97
- /**
98
- * Global Schema Registry
99
- * Unified storage for all metadata types (Objects, Apps, Flows, Layouts, etc.)
100
- *
101
- * ## Namespace & Ownership Model
102
- *
103
- * Objects use a namespace-based FQN system:
104
- * - `namespace`: Short identifier from package manifest (e.g., "crm", "todo")
105
- * - `FQN`: `{namespace}__{short_name}` (e.g., "crm__account")
106
- * - Reserved namespaces (`base`, `system`) don't get prefixed
107
- *
108
- * Ownership modes:
109
- * - `own`: One package owns the object (creates the table, defines base schema)
110
- * - `extend`: Multiple packages can extend an object (add fields, merge by priority)
111
- *
112
- * ## Package vs App Distinction
113
- * - **Package**: The unit of installation, stored under type 'package'.
114
- * Each InstalledPackage wraps a ManifestSchema with lifecycle state.
115
- * - **App**: A UI navigation shell (AppSchema), registered under type 'apps'.
116
- * Apps are extracted from packages during registration.
117
- * - A package may contain 0, 1, or many apps.
118
- */
119
- export type RegistryLogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent';
120
-
121
- export class SchemaRegistry {
122
- // ==========================================
123
- // Logging control
124
- // ==========================================
125
-
126
- /** Controls verbosity of registry console messages. Default: 'info'. */
127
- private static _logLevel: RegistryLogLevel = 'info';
128
-
129
- static get logLevel(): RegistryLogLevel { return this._logLevel; }
130
- static set logLevel(level: RegistryLogLevel) { this._logLevel = level; }
131
-
132
- private static log(msg: string): void {
133
- if (this._logLevel === 'silent' || this._logLevel === 'error' || this._logLevel === 'warn') return;
134
- console.log(msg);
135
- }
136
-
137
- // ==========================================
138
- // Object-specific storage (Ownership Model)
139
- // ==========================================
140
-
141
- /** FQN → Contributor[] (all packages that own/extend this object) */
142
- private static objectContributors = new Map<string, ObjectContributor[]>();
143
-
144
- /** FQN → Merged ServiceObject (cached, invalidated on changes) */
145
- private static mergedObjectCache = new Map<string, ServiceObject>();
146
-
147
- /** Namespace → Set<PackageId> (multiple packages can share a namespace) */
148
- private static namespaceRegistry = new Map<string, Set<string>>();
149
-
150
- // ==========================================
151
- // Generic metadata storage (non-object types)
152
- // ==========================================
153
-
154
- /** Type → Name/ID → MetadataItem */
155
- private static metadata = new Map<string, Map<string, any>>();
156
-
157
- // ==========================================
158
- // Namespace Management
159
- // ==========================================
160
-
161
- /**
162
- * Register a namespace for a package.
163
- * Multiple packages can share the same namespace (e.g. 'sys').
164
- */
165
- static registerNamespace(namespace: string, packageId: string): void {
166
- if (!namespace) return;
167
-
168
- let owners = this.namespaceRegistry.get(namespace);
169
- if (!owners) {
170
- owners = new Set();
171
- this.namespaceRegistry.set(namespace, owners);
172
- }
173
- owners.add(packageId);
174
- this.log(`[Registry] Registered namespace: ${namespace} → ${packageId}`);
175
- }
176
-
177
- /**
178
- * Unregister a namespace when a package is uninstalled.
179
- */
180
- static unregisterNamespace(namespace: string, packageId: string): void {
181
- const owners = this.namespaceRegistry.get(namespace);
182
- if (owners) {
183
- owners.delete(packageId);
184
- if (owners.size === 0) {
185
- this.namespaceRegistry.delete(namespace);
186
- }
187
- this.log(`[Registry] Unregistered namespace: ${namespace} ← ${packageId}`);
188
- }
189
- }
190
-
191
- /**
192
- * Get the packages that use a namespace.
193
- */
194
- static getNamespaceOwner(namespace: string): string | undefined {
195
- const owners = this.namespaceRegistry.get(namespace);
196
- if (!owners || owners.size === 0) return undefined;
197
- // Return the first registered package for backwards compatibility
198
- return owners.values().next().value;
199
- }
200
-
201
- /**
202
- * Get all packages that share a namespace.
203
- */
204
- static getNamespaceOwners(namespace: string): string[] {
205
- const owners = this.namespaceRegistry.get(namespace);
206
- return owners ? Array.from(owners) : [];
207
- }
208
-
209
- // ==========================================
210
- // Object Registration (Ownership Model)
211
- // ==========================================
212
-
213
- /**
214
- * Register an object with ownership semantics.
215
- *
216
- * @param schema - The object definition
217
- * @param packageId - The owning package ID
218
- * @param namespace - The package namespace (for FQN computation)
219
- * @param ownership - 'own' (single owner) or 'extend' (additive merge)
220
- * @param priority - Merge priority (lower applied first, higher wins on conflict)
221
- *
222
- * @throws Error if trying to 'own' an object that already has an owner
223
- */
224
- static registerObject(
225
- schema: ServiceObject,
226
- packageId: string,
227
- namespace?: string,
228
- ownership: ObjectOwnership = 'own',
229
- priority: number = ownership === 'own' ? DEFAULT_OWNER_PRIORITY : DEFAULT_EXTENDER_PRIORITY
230
- ): string {
231
- const shortName = schema.name;
232
- const fqn = computeFQN(namespace, shortName);
233
-
234
- // Ensure namespace is registered
235
- if (namespace) {
236
- this.registerNamespace(namespace, packageId);
237
- }
238
-
239
- // Get or create contributor list
240
- let contributors = this.objectContributors.get(fqn);
241
- if (!contributors) {
242
- contributors = [];
243
- this.objectContributors.set(fqn, contributors);
244
- }
245
-
246
- // Validate ownership rules
247
- if (ownership === 'own') {
248
- const existingOwner = contributors.find(c => c.ownership === 'own');
249
- if (existingOwner && existingOwner.packageId !== packageId) {
250
- throw new Error(
251
- `Object "${fqn}" is already owned by package "${existingOwner.packageId}". ` +
252
- `Package "${packageId}" cannot claim ownership. Use 'extend' to add fields.`
253
- );
254
- }
255
- // Remove existing owner contribution from same package (re-registration)
256
- const idx = contributors.findIndex(c => c.packageId === packageId && c.ownership === 'own');
257
- if (idx !== -1) {
258
- contributors.splice(idx, 1);
259
- console.warn(`[Registry] Re-registering owned object: ${fqn} from ${packageId}`);
260
- }
261
- } else {
262
- // extend mode: remove existing extension from same package
263
- const idx = contributors.findIndex(c => c.packageId === packageId && c.ownership === 'extend');
264
- if (idx !== -1) {
265
- contributors.splice(idx, 1);
266
- }
267
- }
268
-
269
- // Add new contributor
270
- const contributor: ObjectContributor = {
271
- packageId,
272
- namespace: namespace || '',
273
- ownership,
274
- priority,
275
- definition: { ...schema, name: fqn }, // Store with FQN as name
276
- };
277
- contributors.push(contributor);
278
-
279
- // Sort by priority (ascending: lower priority applied first)
280
- contributors.sort((a, b) => a.priority - b.priority);
281
-
282
- // Invalidate merge cache
283
- this.mergedObjectCache.delete(fqn);
284
-
285
- this.log(`[Registry] Registered object: ${fqn} (${ownership}, priority=${priority}) from ${packageId}`);
286
- return fqn;
287
- }
288
-
289
- /**
290
- * Resolve an object by FQN, merging all contributions.
291
- * Returns the merged object or undefined if not found.
292
- */
293
- static resolveObject(fqn: string): ServiceObject | undefined {
294
- // Check cache first
295
- const cached = this.mergedObjectCache.get(fqn);
296
- if (cached) return cached;
297
-
298
- const contributors = this.objectContributors.get(fqn);
299
- if (!contributors || contributors.length === 0) {
300
- return undefined;
301
- }
302
-
303
- // Find owner (must exist for a valid object)
304
- const ownerContrib = contributors.find(c => c.ownership === 'own');
305
- if (!ownerContrib) {
306
- console.warn(`[Registry] Object "${fqn}" has extenders but no owner. Skipping.`);
307
- return undefined;
308
- }
309
-
310
- // Start with owner's definition
311
- let merged = { ...ownerContrib.definition };
312
-
313
- // Apply extensions in priority order (already sorted)
314
- for (const contrib of contributors) {
315
- if (contrib.ownership === 'extend') {
316
- merged = mergeObjectDefinitions(merged, contrib.definition);
317
- }
318
- }
319
-
320
- // Cache the result
321
- this.mergedObjectCache.set(fqn, merged);
322
- return merged;
323
- }
324
-
325
- /**
326
- * Get object by name (FQN, short name, or physical table name).
327
- *
328
- * Resolution order:
329
- * 1. Exact FQN match (e.g., 'crm__account')
330
- * 2. Short name fallback (e.g., 'account' → 'crm__account')
331
- * 3. Physical table name match (e.g., 'sys_user' → 'sys__user')
332
- * ObjectSchema.create() auto-derives tableName as {namespace}_{name},
333
- * which uses a single underscore — different from the FQN double underscore.
334
- */
335
- static getObject(name: string): ServiceObject | undefined {
336
- // Direct FQN lookup
337
- const direct = this.resolveObject(name);
338
- if (direct) return direct;
339
-
340
- // Fallback: scan for objects ending with the short name
341
- // This handles legacy code that doesn't use FQN
342
- for (const fqn of this.objectContributors.keys()) {
343
- const { shortName } = parseFQN(fqn);
344
- if (shortName === name) {
345
- return this.resolveObject(fqn);
346
- }
347
- }
348
-
349
- // Fallback: match by physical table name (e.g., 'sys_user' → FQN 'sys__user')
350
- // This bridges the gap between protocol names (SystemObjectName) and FQN.
351
- for (const fqn of this.objectContributors.keys()) {
352
- const resolved = this.resolveObject(fqn);
353
- if (resolved?.tableName === name) {
354
- return resolved;
355
- }
356
- }
357
-
358
- return undefined;
359
- }
360
-
361
- /**
362
- * Get all registered objects (merged).
363
- *
364
- * @param packageId - Optional filter: only objects contributed by this package
365
- */
366
- static getAllObjects(packageId?: string): ServiceObject[] {
367
- const results: ServiceObject[] = [];
368
-
369
- for (const fqn of this.objectContributors.keys()) {
370
- // If filtering by package, check if this package contributes
371
- if (packageId) {
372
- const contributors = this.objectContributors.get(fqn);
373
- const hasContribution = contributors?.some(c => c.packageId === packageId);
374
- if (!hasContribution) continue;
375
- }
376
-
377
- const merged = this.resolveObject(fqn);
378
- if (merged) {
379
- // Tag with contributor info for UI
380
- (merged as any)._packageId = this.getObjectOwner(fqn)?.packageId;
381
- results.push(merged);
382
- }
383
- }
384
-
385
- return results;
386
- }
387
-
388
- /**
389
- * Get all contributors for an object.
390
- */
391
- static getObjectContributors(fqn: string): ObjectContributor[] {
392
- return this.objectContributors.get(fqn) || [];
393
- }
394
-
395
- /**
396
- * Get the owner contributor for an object.
397
- */
398
- static getObjectOwner(fqn: string): ObjectContributor | undefined {
399
- const contributors = this.objectContributors.get(fqn);
400
- return contributors?.find(c => c.ownership === 'own');
401
- }
402
-
403
- /**
404
- * Unregister all objects contributed by a package.
405
- *
406
- * @throws Error if trying to uninstall an owner that has extenders
407
- */
408
- static unregisterObjectsByPackage(packageId: string, force: boolean = false): void {
409
- for (const [fqn, contributors] of this.objectContributors.entries()) {
410
- // Find this package's contributions
411
- const packageContribs = contributors.filter(c => c.packageId === packageId);
412
-
413
- for (const contrib of packageContribs) {
414
- if (contrib.ownership === 'own' && !force) {
415
- // Check if there are extenders from other packages
416
- const otherExtenders = contributors.filter(
417
- c => c.packageId !== packageId && c.ownership === 'extend'
418
- );
419
- if (otherExtenders.length > 0) {
420
- throw new Error(
421
- `Cannot uninstall package "${packageId}": object "${fqn}" is extended by ` +
422
- `${otherExtenders.map(c => c.packageId).join(', ')}. Uninstall extenders first.`
423
- );
424
- }
425
- }
426
-
427
- // Remove contribution
428
- const idx = contributors.indexOf(contrib);
429
- if (idx !== -1) {
430
- contributors.splice(idx, 1);
431
- this.log(`[Registry] Removed ${contrib.ownership} contribution to ${fqn} from ${packageId}`);
432
- }
433
- }
434
-
435
- // Clean up empty contributor lists
436
- if (contributors.length === 0) {
437
- this.objectContributors.delete(fqn);
438
- }
439
-
440
- // Invalidate cache
441
- this.mergedObjectCache.delete(fqn);
442
- }
443
- }
444
-
445
- // ==========================================
446
- // Generic Metadata (Non-Object Types)
447
- // ==========================================
448
-
449
- /**
450
- * Universal Register Method for non-object metadata.
451
- */
452
- static registerItem<T>(type: string, item: T, keyField: keyof T = 'name' as keyof T, packageId?: string) {
453
- if (!this.metadata.has(type)) {
454
- this.metadata.set(type, new Map());
455
- }
456
- const collection = this.metadata.get(type)!;
457
- const baseName = String(item[keyField]);
458
-
459
- // Tag item with owning package for scoped queries
460
- if (packageId) {
461
- (item as any)._packageId = packageId;
462
- }
463
-
464
- // Validation Hook
465
- try {
466
- this.validate(type, item);
467
- } catch (e: any) {
468
- console.error(`[Registry] Validation failed for ${type} ${baseName}: ${e.message}`);
469
- }
470
-
471
- // Use composite key (packageId:name) when packageId is provided
472
- const storageKey = packageId ? `${packageId}:${baseName}` : baseName;
473
-
474
- if (collection.has(storageKey)) {
475
- console.warn(`[Registry] Overwriting ${type}: ${storageKey}`);
476
- }
477
- collection.set(storageKey, item);
478
- this.log(`[Registry] Registered ${type}: ${storageKey}`);
479
- }
480
-
481
- /**
482
- * Validate Metadata against Spec Zod Schemas
483
- */
484
- static validate(type: string, item: any) {
485
- if (type === 'object') {
486
- return ObjectSchema.parse(item);
487
- }
488
- if (type === 'app') {
489
- return AppSchema.parse(item);
490
- }
491
- if (type === 'package') {
492
- return InstalledPackageSchema.parse(item);
493
- }
494
- if (type === 'plugin') {
495
- return ManifestSchema.parse(item);
496
- }
497
- return true;
498
- }
499
-
500
- /**
501
- * Universal Unregister Method
502
- */
503
- static unregisterItem(type: string, name: string) {
504
- const collection = this.metadata.get(type);
505
- if (!collection) {
506
- console.warn(`[Registry] Attempted to unregister non-existent ${type}: ${name}`);
507
- return;
508
- }
509
- if (collection.has(name)) {
510
- collection.delete(name);
511
- this.log(`[Registry] Unregistered ${type}: ${name}`);
512
- return;
513
- }
514
- // Scan composite keys
515
- for (const key of collection.keys()) {
516
- if (key.endsWith(`:${name}`)) {
517
- collection.delete(key);
518
- this.log(`[Registry] Unregistered ${type}: ${key}`);
519
- return;
520
- }
521
- }
522
- console.warn(`[Registry] Attempted to unregister non-existent ${type}: ${name}`);
523
- }
524
-
525
- /**
526
- * Universal Get Method
527
- */
528
- static getItem<T>(type: string, name: string): T | undefined {
529
- // Special handling for 'object' and 'objects' types - use objectContributors
530
- if (type === 'object' || type === 'objects') {
531
- return this.getObject(name) as unknown as T | undefined;
532
- }
533
-
534
- const collection = this.metadata.get(type);
535
- if (!collection) return undefined;
536
- const direct = collection.get(name);
537
- if (direct) return direct as T;
538
- // Scan for composite keys
539
- for (const [key, item] of collection) {
540
- if (key.endsWith(`:${name}`)) return item as T;
541
- }
542
- return undefined;
543
- }
544
-
545
- /**
546
- * Universal List Method
547
- */
548
- static listItems<T>(type: string, packageId?: string): T[] {
549
- // Special handling for 'object' and 'objects' types - use objectContributors
550
- if (type === 'object' || type === 'objects') {
551
- return this.getAllObjects(packageId) as unknown as T[];
552
- }
553
-
554
- const items = Array.from(this.metadata.get(type)?.values() || []) as T[];
555
- if (packageId) {
556
- return items.filter((item: any) => item._packageId === packageId);
557
- }
558
- return items;
559
- }
560
-
561
- /**
562
- * Get all registered metadata types (Kinds)
563
- */
564
- static getRegisteredTypes(): string[] {
565
- const types = Array.from(this.metadata.keys());
566
- // Always include 'object' even if stored separately
567
- if (!types.includes('object') && this.objectContributors.size > 0) {
568
- types.push('object');
569
- }
570
- return types;
571
- }
572
-
573
- // ==========================================
574
- // Package Management
575
- // ==========================================
576
-
577
- static installPackage(manifest: ObjectStackManifest, settings?: Record<string, any>): InstalledPackage {
578
- const now = new Date().toISOString();
579
- const pkg: InstalledPackage = {
580
- manifest,
581
- status: 'installed',
582
- enabled: true,
583
- installedAt: now,
584
- updatedAt: now,
585
- settings,
586
- };
587
-
588
- // Register namespace if present
589
- if (manifest.namespace) {
590
- this.registerNamespace(manifest.namespace, manifest.id);
591
- }
592
-
593
- if (!this.metadata.has('package')) {
594
- this.metadata.set('package', new Map());
595
- }
596
- const collection = this.metadata.get('package')!;
597
- if (collection.has(manifest.id)) {
598
- console.warn(`[Registry] Overwriting package: ${manifest.id}`);
599
- }
600
- collection.set(manifest.id, pkg);
601
- this.log(`[Registry] Installed package: ${manifest.id} (${manifest.name})`);
602
- return pkg;
603
- }
604
-
605
- static uninstallPackage(id: string): boolean {
606
- const pkg = this.getPackage(id);
607
- if (!pkg) {
608
- console.warn(`[Registry] Package not found for uninstall: ${id}`);
609
- return false;
610
- }
611
-
612
- // Unregister namespace
613
- if (pkg.manifest.namespace) {
614
- this.unregisterNamespace(pkg.manifest.namespace, id);
615
- }
616
-
617
- // Unregister objects (will throw if extenders exist)
618
- this.unregisterObjectsByPackage(id);
619
-
620
- // Remove package record
621
- const collection = this.metadata.get('package');
622
- if (collection) {
623
- collection.delete(id);
624
- this.log(`[Registry] Uninstalled package: ${id}`);
625
- return true;
626
- }
627
- return false;
628
- }
629
-
630
- static getPackage(id: string): InstalledPackage | undefined {
631
- return this.metadata.get('package')?.get(id) as InstalledPackage | undefined;
632
- }
633
-
634
- static getAllPackages(): InstalledPackage[] {
635
- return this.listItems<InstalledPackage>('package');
636
- }
637
-
638
- static enablePackage(id: string): InstalledPackage | undefined {
639
- const pkg = this.getPackage(id);
640
- if (pkg) {
641
- pkg.enabled = true;
642
- pkg.status = 'installed';
643
- pkg.statusChangedAt = new Date().toISOString();
644
- pkg.updatedAt = new Date().toISOString();
645
- this.log(`[Registry] Enabled package: ${id}`);
646
- }
647
- return pkg;
648
- }
649
-
650
- static disablePackage(id: string): InstalledPackage | undefined {
651
- const pkg = this.getPackage(id);
652
- if (pkg) {
653
- pkg.enabled = false;
654
- pkg.status = 'disabled';
655
- pkg.statusChangedAt = new Date().toISOString();
656
- pkg.updatedAt = new Date().toISOString();
657
- this.log(`[Registry] Disabled package: ${id}`);
658
- }
659
- return pkg;
660
- }
661
-
662
- // ==========================================
663
- // App Helpers
664
- // ==========================================
665
-
666
- static registerApp(app: any, packageId?: string) {
667
- this.registerItem('app', app, 'name', packageId);
668
- }
669
-
670
- static getApp(name: string): any {
671
- return this.getItem('app', name);
672
- }
673
-
674
- static getAllApps(): any[] {
675
- return this.listItems('app');
676
- }
677
-
678
- // ==========================================
679
- // Plugin Helpers
680
- // ==========================================
681
-
682
- static registerPlugin(manifest: ObjectStackManifest) {
683
- this.registerItem('plugin', manifest, 'id');
684
- }
685
-
686
- static getAllPlugins(): ObjectStackManifest[] {
687
- return this.listItems<ObjectStackManifest>('plugin');
688
- }
689
-
690
- // ==========================================
691
- // Kind Helpers
692
- // ==========================================
693
-
694
- static registerKind(kind: { id: string, globs: string[] }) {
695
- this.registerItem('kind', kind, 'id');
696
- }
697
-
698
- static getAllKinds(): { id: string, globs: string[] }[] {
699
- return this.listItems('kind');
700
- }
701
-
702
- // ==========================================
703
- // Reset (for testing)
704
- // ==========================================
705
-
706
- /**
707
- * Clear all registry state. Use only for testing.
708
- */
709
- static reset(): void {
710
- this.objectContributors.clear();
711
- this.mergedObjectCache.clear();
712
- this.namespaceRegistry.clear();
713
- this.metadata.clear();
714
- this.log('[Registry] Reset complete');
715
- }
716
- }