@object-ui/data-objectstack 17.3.0 → 17.6.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +888 -0
  2. package/README.md +3 -11
  3. package/dist/index.cjs +1511 -1115
  4. package/dist/index.d.cts +1222 -824
  5. package/dist/index.d.ts +1222 -824
  6. package/dist/index.js +1503 -1111
  7. package/package.json +5 -6
  8. package/src/aggregate-capability.test.ts +0 -177
  9. package/src/bulkDelete.test.ts +0 -75
  10. package/src/bulkUpdate.test.ts +0 -93
  11. package/src/cache/MetadataCache.test.ts +0 -477
  12. package/src/cache/MetadataCache.ts +0 -289
  13. package/src/cloud.ts +0 -109
  14. package/src/connection.test.ts +0 -188
  15. package/src/contracts.ts +0 -115
  16. package/src/errors.test.ts +0 -426
  17. package/src/errors.ts +0 -293
  18. package/src/exportDownload.test.ts +0 -141
  19. package/src/filter-entry-translation.test.ts +0 -360
  20. package/src/filter-operator-ast-parity.test.ts +0 -71
  21. package/src/getObjectSchema.test.ts +0 -145
  22. package/src/importJob.test.ts +0 -71
  23. package/src/index.ts +0 -3658
  24. package/src/integration.ts +0 -200
  25. package/src/listViews.test.ts +0 -175
  26. package/src/metadata-client.test.ts +0 -303
  27. package/src/metadata-client.ts +0 -849
  28. package/src/occ.test.ts +0 -101
  29. package/src/onMutation.test.ts +0 -562
  30. package/src/onWriteWarning.test.ts +0 -273
  31. package/src/orderby-serialization.test.ts +0 -115
  32. package/src/queryDataset.test.ts +0 -132
  33. package/src/searchAll.test.ts +0 -120
  34. package/src/security.ts +0 -243
  35. package/src/spec-symbol-batch6.test.ts +0 -306
  36. package/src/studio.ts +0 -152
  37. package/src/userState.test.ts +0 -315
  38. package/src/userState.ts +0 -238
  39. package/src/v3-compat.test.ts +0 -240
  40. package/src/validation-error.test.ts +0 -94
@@ -1,289 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- /**
10
- * Represents a cached schema entry with metadata
11
- */
12
- interface CachedSchema {
13
- data: unknown;
14
- timestamp: number;
15
- accessCount: number;
16
- lastAccessed: number;
17
- }
18
-
19
- /**
20
- * Statistics reported by {@link MetadataCache.getStats} for monitoring.
21
- *
22
- * NOT the spec's `CacheStats` (`@objectstack/spec/contracts`), whose name this
23
- * interface wore until objectui#3160 (objectstack#4115 ledger batch 6). That one
24
- * describes the platform's `ICacheService` — a server-side KV cache measured by
25
- * `keyCount` and `memoryUsage`. This one describes the browser-side LRU in front
26
- * of `/api/v1/meta/*`: it is bounded (`size`/`maxSize`), it evicts, it coalesces
27
- * concurrent fetches onto one in-flight promise, and it reports a `hitRate`.
28
- * Neither type has a key the other has, so this is a name collision, not a
29
- * dialect — deriving would have replaced every field.
30
- */
31
- export interface MetadataCacheStats {
32
- size: number;
33
- maxSize: number;
34
- hits: number;
35
- misses: number;
36
- evictions: number;
37
- /** Number of concurrent fetches that were coalesced onto an in-flight request. */
38
- coalesced: number;
39
- hitRate: number;
40
- }
41
-
42
- /**
43
- * MetadataCache - LRU cache with TTL expiration for schema metadata
44
- *
45
- * Features:
46
- * - LRU (Least Recently Used) eviction policy
47
- * - TTL (Time To Live) based expiration (fixed from creation, not sliding)
48
- * - Memory limit controls
49
- * - Async-safe operations
50
- * - Performance statistics tracking
51
- *
52
- * Concurrent requests for the same uncached key are deduplicated via an
53
- * internal in-flight Promise map: only the first call invokes the fetcher,
54
- * and subsequent callers receive the same Promise.
55
- *
56
- * @example
57
- * ```typescript
58
- * const cache = new MetadataCache({ maxSize: 100, ttl: 300000 });
59
- *
60
- * const schema = await cache.get('users', async () => {
61
- * return await fetchSchemaFromServer('users');
62
- * });
63
- *
64
- * console.log(cache.getStats());
65
- * ```
66
- */
67
- export class MetadataCache {
68
- private cache: Map<string, CachedSchema>;
69
- private inflight: Map<string, Promise<unknown>>;
70
- private maxSize: number;
71
- private ttl: number;
72
- private stats: {
73
- hits: number;
74
- misses: number;
75
- evictions: number;
76
- coalesced: number;
77
- };
78
-
79
- /**
80
- * Create a new MetadataCache instance
81
- *
82
- * @param options - Configuration options
83
- * @param options.maxSize - Maximum number of entries (default: 100)
84
- * @param options.ttl - Time to live in milliseconds (default: 5 minutes)
85
- */
86
- constructor(options: { maxSize?: number; ttl?: number } = {}) {
87
- this.cache = new Map();
88
- this.inflight = new Map();
89
- this.maxSize = options.maxSize || 100;
90
- this.ttl = options.ttl || 5 * 60 * 1000; // 5 minutes default
91
- this.stats = {
92
- hits: 0,
93
- misses: 0,
94
- evictions: 0,
95
- coalesced: 0,
96
- };
97
- }
98
-
99
- /**
100
- * Get a value from cache or fetch it using the provided fetcher function
101
- *
102
- * @param key - Cache key
103
- * @param fetcher - Async function to fetch data if not in cache
104
- * @returns Promise resolving to the cached or fetched data
105
- */
106
- async get<T = unknown>(key: string, fetcher: () => Promise<T>): Promise<T> {
107
- const now = Date.now();
108
- const cached = this.cache.get(key);
109
-
110
- // Check if cache entry exists and is not expired
111
- if (cached) {
112
- const age = now - cached.timestamp;
113
-
114
- if (age < this.ttl) {
115
- // Cache hit - update access metadata
116
- cached.accessCount++;
117
- cached.lastAccessed = now;
118
- this.stats.hits++;
119
-
120
- // Move to end (most recently used) by re-inserting
121
- this.cache.delete(key);
122
- this.cache.set(key, cached);
123
-
124
- return cached.data as T;
125
- } else {
126
- // Expired entry - remove it
127
- this.cache.delete(key);
128
- }
129
- }
130
-
131
- // Cache miss - dedupe concurrent fetches for the same key
132
- const existing = this.inflight.get(key);
133
- if (existing) {
134
- this.stats.coalesced++;
135
- return existing as Promise<T>;
136
- }
137
-
138
- this.stats.misses++;
139
- const promise = (async () => {
140
- try {
141
- const data = await fetcher();
142
- this.set(key, data);
143
- return data;
144
- } finally {
145
- this.inflight.delete(key);
146
- }
147
- })();
148
- this.inflight.set(key, promise as Promise<unknown>);
149
- return promise;
150
- }
151
-
152
- /**
153
- * Prime the cache with a pre-fetched value. Useful when a bulk endpoint
154
- * (e.g. list of all object schemas) returns data that would otherwise
155
- * be fetched again per item.
156
- */
157
- prime(key: string, data: unknown): void {
158
- this.set(key, data);
159
- }
160
-
161
- /**
162
- * Set a value in the cache
163
- *
164
- * @param key - Cache key
165
- * @param data - Data to cache
166
- */
167
- private set(key: string, data: unknown): void {
168
- const now = Date.now();
169
-
170
- // Check if we need to evict entries
171
- if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
172
- this.evictLRU();
173
- }
174
-
175
- // Add or update the entry
176
- this.cache.set(key, {
177
- data,
178
- timestamp: now,
179
- accessCount: 1,
180
- lastAccessed: now,
181
- });
182
- }
183
-
184
- /**
185
- * Evict the least recently used entry
186
- */
187
- private evictLRU(): void {
188
- // The first entry in the Map is the least recently used
189
- // (since we move accessed items to the end)
190
- const firstKey = this.cache.keys().next().value;
191
-
192
- if (firstKey !== undefined) {
193
- this.cache.delete(firstKey);
194
- this.stats.evictions++;
195
- }
196
- }
197
-
198
- /**
199
- * Invalidate a specific cache entry or all entries
200
- *
201
- * @param key - Optional key to invalidate. If omitted, invalidates all entries
202
- */
203
- invalidate(key?: string): void {
204
- if (key) {
205
- this.cache.delete(key);
206
- } else {
207
- this.cache.clear();
208
- }
209
- }
210
-
211
- /**
212
- * Clear all cache entries and reset statistics
213
- */
214
- clear(): void {
215
- this.cache.clear();
216
- this.inflight.clear();
217
- this.stats = {
218
- hits: 0,
219
- misses: 0,
220
- evictions: 0,
221
- coalesced: 0,
222
- };
223
- }
224
-
225
- /**
226
- * Get cache statistics
227
- *
228
- * @returns Cache statistics including hit rate
229
- */
230
- getStats(): MetadataCacheStats {
231
- const total = this.stats.hits + this.stats.misses;
232
- const hitRate = total > 0 ? this.stats.hits / total : 0;
233
-
234
- return {
235
- size: this.cache.size,
236
- maxSize: this.maxSize,
237
- hits: this.stats.hits,
238
- misses: this.stats.misses,
239
- evictions: this.stats.evictions,
240
- coalesced: this.stats.coalesced,
241
- hitRate: hitRate,
242
- };
243
- }
244
-
245
- /**
246
- * Get a cached value synchronously without triggering a fetch.
247
- * Returns undefined if not in cache or expired.
248
- */
249
- getCachedSync<V = unknown>(key: string): V | undefined {
250
- const entry = this.cache.get(key);
251
- if (!entry) return undefined;
252
-
253
- // Check TTL
254
- if (this.ttl > 0 && Date.now() - entry.timestamp > this.ttl) {
255
- this.cache.delete(key);
256
- return undefined;
257
- }
258
-
259
- // Update access order for LRU
260
- this.cache.delete(key);
261
- this.cache.set(key, entry);
262
- this.stats.hits++;
263
-
264
- return entry.data as V;
265
- }
266
-
267
- /**
268
- * Check if a key exists in the cache (and is not expired)
269
- *
270
- * @param key - Cache key to check
271
- * @returns true if the key exists and is not expired
272
- */
273
- has(key: string): boolean {
274
- const cached = this.cache.get(key);
275
-
276
- if (!cached) {
277
- return false;
278
- }
279
-
280
- const age = Date.now() - cached.timestamp;
281
-
282
- if (age >= this.ttl) {
283
- this.cache.delete(key);
284
- return false;
285
- }
286
-
287
- return true;
288
- }
289
- }
package/src/cloud.ts DELETED
@@ -1,109 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- /**
10
- * Cloud namespace integration for @objectstack/spec v3.0.0
11
- * Replaces the legacy Hub namespace for cloud deployment, hosting, and marketplace schemas.
12
- */
13
-
14
- export interface CloudDeploymentConfig {
15
- /** Target environment */
16
- environment: 'development' | 'staging' | 'production';
17
- /** Cloud region */
18
- region?: string;
19
- /** Auto-scaling configuration */
20
- scaling?: {
21
- minInstances: number;
22
- maxInstances: number;
23
- targetCPU?: number;
24
- };
25
- /** Environment variables */
26
- envVars?: Record<string, string>;
27
- }
28
-
29
- export interface CloudHostingConfig {
30
- /** Custom domain */
31
- customDomain?: string;
32
- /** SSL configuration */
33
- ssl?: {
34
- enabled: boolean;
35
- autoRenew: boolean;
36
- };
37
- /** CDN configuration */
38
- cdn?: {
39
- enabled: boolean;
40
- regions?: string[];
41
- };
42
- }
43
-
44
- export interface CloudMarketplaceEntry {
45
- /** Plugin or app ID */
46
- id: string;
47
- /** Display name */
48
- name: string;
49
- /** Description */
50
- description: string;
51
- /** Version */
52
- version: string;
53
- /** Author */
54
- author: string;
55
- /** Category */
56
- category: string;
57
- /** Tags */
58
- tags: string[];
59
- /** Rating (1-5) */
60
- rating?: number;
61
- /** Install count */
62
- installCount?: number;
63
- /** Price (0 = free) */
64
- price?: number;
65
- }
66
-
67
- /**
68
- * Cloud operations helper for ObjectStack adapter.
69
- * Provides methods for cloud deployment, hosting, and marketplace operations.
70
- */
71
- export class CloudOperations {
72
- constructor(private getClient: () => any) {}
73
-
74
- /**
75
- * Deploy an application to the cloud.
76
- */
77
- async deploy(appId: string, config: CloudDeploymentConfig): Promise<{ deploymentId: string; status: string }> {
78
- const client = this.getClient();
79
- const result = await client.cloud?.deploy?.(appId, config);
80
- return result ?? { deploymentId: `deploy-${Date.now()}`, status: 'pending' };
81
- }
82
-
83
- /**
84
- * Get deployment status.
85
- */
86
- async getDeploymentStatus(deploymentId: string): Promise<{ status: string; url?: string }> {
87
- const client = this.getClient();
88
- const result = await client.cloud?.getDeployment?.(deploymentId);
89
- return result ?? { status: 'unknown' };
90
- }
91
-
92
- /**
93
- * Search marketplace entries.
94
- */
95
- async searchMarketplace(query?: string, category?: string): Promise<CloudMarketplaceEntry[]> {
96
- const client = this.getClient();
97
- const result = await client.cloud?.marketplace?.search?.({ query, category });
98
- return result?.items ?? [];
99
- }
100
-
101
- /**
102
- * Install a marketplace plugin.
103
- */
104
- async installPlugin(pluginId: string): Promise<{ success: boolean }> {
105
- const client = this.getClient();
106
- const result = await client.cloud?.marketplace?.install?.(pluginId);
107
- return result ?? { success: false };
108
- }
109
- }
@@ -1,188 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- import { describe, it, expect, beforeEach, vi } from 'vitest';
10
- import {
11
- ObjectStackAdapter,
12
- ConnectionState,
13
- ConnectionStateEvent,
14
- BatchProgressEvent,
15
- clearSharedDiscoveryCache,
16
- } from './index';
17
-
18
- describe('Connection State Monitoring', () => {
19
- let adapter: ObjectStackAdapter;
20
-
21
- beforeEach(() => {
22
- adapter = new ObjectStackAdapter({
23
- baseUrl: 'http://localhost:3000',
24
- autoReconnect: false, // Disable auto-reconnect for testing
25
- });
26
- });
27
-
28
- it('should initialize with disconnected state', () => {
29
- expect(adapter.getConnectionState()).toBe('disconnected');
30
- expect(adapter.isConnected()).toBe(false);
31
- });
32
-
33
- it('should allow subscribing to connection state changes', () => {
34
- const listener = vi.fn();
35
- const unsubscribe = adapter.onConnectionStateChange(listener);
36
-
37
- expect(typeof unsubscribe).toBe('function');
38
- expect(listener).not.toHaveBeenCalled();
39
-
40
- // Cleanup
41
- unsubscribe();
42
- });
43
-
44
- it('should allow subscribing to batch progress events', () => {
45
- const listener = vi.fn();
46
- const unsubscribe = adapter.onBatchProgress(listener);
47
-
48
- expect(typeof unsubscribe).toBe('function');
49
- expect(listener).not.toHaveBeenCalled();
50
-
51
- // Cleanup
52
- unsubscribe();
53
- });
54
-
55
- it('should unsubscribe connection state listener', () => {
56
- const listener = vi.fn();
57
- const unsubscribe = adapter.onConnectionStateChange(listener);
58
-
59
- // Unsubscribe
60
- unsubscribe();
61
-
62
- // Listener should not be called after unsubscribe
63
- // (We can't easily test this without triggering a connection state change)
64
- });
65
-
66
- it('should unsubscribe batch progress listener', () => {
67
- const listener = vi.fn();
68
- const unsubscribe = adapter.onBatchProgress(listener);
69
-
70
- // Unsubscribe
71
- unsubscribe();
72
-
73
- // Listener should not be called after unsubscribe
74
- });
75
-
76
- it('should support auto-reconnect configuration', () => {
77
- const adapterWithReconnect = new ObjectStackAdapter({
78
- baseUrl: 'http://localhost:3000',
79
- autoReconnect: true,
80
- maxReconnectAttempts: 5,
81
- reconnectDelay: 2000,
82
- });
83
-
84
- expect(adapterWithReconnect.getConnectionState()).toBe('disconnected');
85
- });
86
- });
87
-
88
- describe('Batch Progress Events', () => {
89
- let adapter: ObjectStackAdapter;
90
-
91
- beforeEach(() => {
92
- adapter = new ObjectStackAdapter({
93
- baseUrl: 'http://localhost:3000',
94
- });
95
- });
96
-
97
- it('should allow subscribing to batch progress', () => {
98
- const listener = vi.fn();
99
- const unsubscribe = adapter.onBatchProgress(listener);
100
-
101
- expect(typeof unsubscribe).toBe('function');
102
-
103
- // Cleanup
104
- unsubscribe();
105
- });
106
- });
107
-
108
- describe('getDiscovery', () => {
109
- beforeEach(() => {
110
- clearSharedDiscoveryCache();
111
- });
112
-
113
- it('should return discoveryInfo from the underlying client after connect', async () => {
114
- const mockDiscovery = {
115
- name: 'test-server',
116
- version: '1.0.0',
117
- services: {
118
- auth: { enabled: false, status: 'unavailable' },
119
- data: { enabled: true, status: 'available' },
120
- },
121
- };
122
-
123
- const fetchImpl = vi.fn(async () =>
124
- ({
125
- ok: true,
126
- json: async () => ({ success: true, data: mockDiscovery }),
127
- }) as unknown as Response,
128
- );
129
-
130
- const adapter = new ObjectStackAdapter({
131
- baseUrl: 'http://localhost:3000',
132
- autoReconnect: false,
133
- fetch: fetchImpl,
134
- });
135
-
136
- await adapter.connect();
137
-
138
- const discovery = await adapter.getDiscovery();
139
- expect(discovery).toEqual(mockDiscovery);
140
- expect((discovery as any)?.services?.auth?.enabled).toBe(false);
141
- expect(fetchImpl).toHaveBeenCalledTimes(1);
142
- expect((fetchImpl.mock.calls[0] as unknown as unknown[])[0]).toContain('/api/v1/discovery');
143
- });
144
-
145
- it('should return null when connection fails', async () => {
146
- const fetchImpl = vi.fn(async () => {
147
- throw new Error('Connection failed');
148
- });
149
-
150
- const adapter = new ObjectStackAdapter({
151
- baseUrl: 'http://localhost:3000',
152
- autoReconnect: false,
153
- fetch: fetchImpl as any,
154
- });
155
-
156
- await expect(adapter.connect()).rejects.toThrow();
157
-
158
- const discovery = await adapter.getDiscovery();
159
- expect(discovery).toBeNull();
160
- });
161
-
162
- it('should share a single discovery fetch across adapters with the same baseUrl', async () => {
163
- const mockDiscovery = { name: 'shared', version: '1.0.0' };
164
- const fetchImpl = vi.fn(async () =>
165
- ({
166
- ok: true,
167
- json: async () => ({ success: true, data: mockDiscovery }),
168
- }) as unknown as Response,
169
- );
170
-
171
- const a1 = new ObjectStackAdapter({
172
- baseUrl: 'http://localhost:3000',
173
- autoReconnect: false,
174
- fetch: fetchImpl,
175
- });
176
- const a2 = new ObjectStackAdapter({
177
- baseUrl: 'http://localhost:3000',
178
- autoReconnect: false,
179
- fetch: fetchImpl,
180
- });
181
-
182
- await Promise.all([a1.connect(), a2.connect()]);
183
-
184
- expect(fetchImpl).toHaveBeenCalledTimes(1);
185
- expect(await a1.getDiscovery()).toEqual(mockDiscovery);
186
- expect(await a2.getDiscovery()).toEqual(mockDiscovery);
187
- });
188
- });
package/src/contracts.ts DELETED
@@ -1,115 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- /**
10
- * Contracts module integration for @objectstack/spec v3.0.0
11
- * Provides plugin contract validation and marketplace publishing utilities.
12
- */
13
-
14
- export interface PluginContract {
15
- /** Plugin name */
16
- name: string;
17
- /** Plugin version */
18
- version: string;
19
- /** Required peer dependencies */
20
- peerDependencies?: Record<string, string>;
21
- /** Exported component types */
22
- exports: PluginExport[];
23
- /** Required permissions */
24
- permissions?: string[];
25
- /** API surface contract */
26
- api?: PluginAPIContract;
27
- }
28
-
29
- export interface PluginExport {
30
- /** Export name */
31
- name: string;
32
- /** Export type */
33
- type: 'component' | 'hook' | 'utility' | 'provider';
34
- /** Description */
35
- description?: string;
36
- }
37
-
38
- export interface PluginAPIContract {
39
- /** Consumed data sources */
40
- dataSources?: string[];
41
- /** Required object schemas */
42
- requiredSchemas?: string[];
43
- /** Event subscriptions */
44
- events?: string[];
45
- }
46
-
47
- export interface ContractValidationResult {
48
- valid: boolean;
49
- errors: ContractValidationError[];
50
- warnings: string[];
51
- }
52
-
53
- export interface ContractValidationError {
54
- field: string;
55
- message: string;
56
- code: string;
57
- }
58
-
59
- /**
60
- * Validate a plugin contract against the ObjectStack spec.
61
- */
62
- export function validatePluginContract(contract: PluginContract): ContractValidationResult {
63
- const errors: ContractValidationError[] = [];
64
- const warnings: string[] = [];
65
-
66
- if (!contract.name || contract.name.trim().length === 0) {
67
- errors.push({ field: 'name', message: 'Plugin name is required', code: 'MISSING_NAME' });
68
- }
69
-
70
- if (!contract.version || !/^\d+\.\d+\.\d+/.test(contract.version)) {
71
- errors.push({ field: 'version', message: 'Valid semver version is required', code: 'INVALID_VERSION' });
72
- }
73
-
74
- if (!contract.exports || contract.exports.length === 0) {
75
- errors.push({ field: 'exports', message: 'At least one export is required', code: 'NO_EXPORTS' });
76
- }
77
-
78
- if (contract.exports) {
79
- const validTypes = ['component', 'hook', 'utility', 'provider'];
80
- for (const exp of contract.exports) {
81
- if (!exp.name) {
82
- errors.push({ field: 'exports.name', message: 'Export name is required', code: 'MISSING_EXPORT_NAME' });
83
- }
84
- if (!validTypes.includes(exp.type)) {
85
- errors.push({ field: 'exports.type', message: `Invalid export type: ${exp.type}`, code: 'INVALID_EXPORT_TYPE' });
86
- }
87
- }
88
- }
89
-
90
- if (!contract.permissions || contract.permissions.length === 0) {
91
- warnings.push('No permissions declared — plugin will have minimal access');
92
- }
93
-
94
- return { valid: errors.length === 0, errors, warnings };
95
- }
96
-
97
- /**
98
- * Generate a plugin contract manifest for marketplace publishing.
99
- */
100
- export function generateContractManifest(contract: PluginContract): Record<string, unknown> {
101
- return {
102
- $schema: 'https://objectui.org/schemas/plugin-contract-v1.json',
103
- name: contract.name,
104
- version: contract.version,
105
- peerDependencies: contract.peerDependencies ?? {},
106
- exports: contract.exports.map(exp => ({
107
- name: exp.name,
108
- type: exp.type,
109
- description: exp.description ?? '',
110
- })),
111
- permissions: contract.permissions ?? [],
112
- api: contract.api ?? {},
113
- generatedAt: new Date().toISOString(),
114
- };
115
- }