@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.
- package/CHANGELOG.md +888 -0
- package/README.md +3 -11
- package/dist/index.cjs +1511 -1115
- package/dist/index.d.cts +1222 -824
- package/dist/index.d.ts +1222 -824
- package/dist/index.js +1503 -1111
- package/package.json +5 -6
- package/src/aggregate-capability.test.ts +0 -177
- package/src/bulkDelete.test.ts +0 -75
- package/src/bulkUpdate.test.ts +0 -93
- package/src/cache/MetadataCache.test.ts +0 -477
- package/src/cache/MetadataCache.ts +0 -289
- package/src/cloud.ts +0 -109
- package/src/connection.test.ts +0 -188
- package/src/contracts.ts +0 -115
- package/src/errors.test.ts +0 -426
- package/src/errors.ts +0 -293
- package/src/exportDownload.test.ts +0 -141
- package/src/filter-entry-translation.test.ts +0 -360
- package/src/filter-operator-ast-parity.test.ts +0 -71
- package/src/getObjectSchema.test.ts +0 -145
- package/src/importJob.test.ts +0 -71
- package/src/index.ts +0 -3658
- package/src/integration.ts +0 -200
- package/src/listViews.test.ts +0 -175
- package/src/metadata-client.test.ts +0 -303
- package/src/metadata-client.ts +0 -849
- package/src/occ.test.ts +0 -101
- package/src/onMutation.test.ts +0 -562
- package/src/onWriteWarning.test.ts +0 -273
- package/src/orderby-serialization.test.ts +0 -115
- package/src/queryDataset.test.ts +0 -132
- package/src/searchAll.test.ts +0 -120
- package/src/security.ts +0 -243
- package/src/spec-symbol-batch6.test.ts +0 -306
- package/src/studio.ts +0 -152
- package/src/userState.test.ts +0 -315
- package/src/userState.ts +0 -238
- package/src/v3-compat.test.ts +0 -240
- 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
|
-
}
|
package/src/connection.test.ts
DELETED
|
@@ -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
|
-
}
|