@objectstack/client 4.0.4 → 4.1.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/dist/index.d.mts +979 -5
- package/dist/index.d.ts +979 -5
- package/dist/index.js +1419 -42
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1417 -41
- package/dist/index.mjs.map +1 -1
- package/package.json +38 -13
- package/.turbo/turbo-build.log +0 -22
- package/CHANGELOG.md +0 -645
- package/CLIENT_SERVER_INTEGRATION_TESTS.md +0 -939
- package/CLIENT_SPEC_COMPLIANCE.md +0 -361
- package/src/client.feed.test.ts +0 -273
- package/src/client.hono.test.ts +0 -169
- package/src/client.msw.test.ts +0 -223
- package/src/client.test.ts +0 -891
- package/src/index.ts +0 -1889
- package/src/query-builder.ts +0 -337
- package/src/realtime-api.ts +0 -208
- package/tests/integration/01-discovery.test.ts +0 -68
- package/tests/integration/README.md +0 -72
- package/tsconfig.json +0 -11
- package/vitest.config.ts +0 -13
- package/vitest.integration.config.ts +0 -18
package/src/query-builder.ts
DELETED
|
@@ -1,337 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Type-Safe Query Builder
|
|
5
|
-
*
|
|
6
|
-
* Provides a fluent API for building ObjectStack queries with:
|
|
7
|
-
* - Compile-time type checking
|
|
8
|
-
* - Intelligent code completion
|
|
9
|
-
* - Runtime validation
|
|
10
|
-
* - Type-safe filters and selections
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { QueryAST, FilterCondition, SortNode } from '@objectstack/spec/data';
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Type-safe filter builder
|
|
17
|
-
*/
|
|
18
|
-
export class FilterBuilder<T = any> {
|
|
19
|
-
private conditions: FilterCondition[] = [];
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Equality filter: field = value
|
|
23
|
-
*/
|
|
24
|
-
equals<K extends keyof T>(field: K, value: T[K]): this {
|
|
25
|
-
this.conditions.push([field as string, '=', value]);
|
|
26
|
-
return this;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Not equals filter: field != value
|
|
31
|
-
*/
|
|
32
|
-
notEquals<K extends keyof T>(field: K, value: T[K]): this {
|
|
33
|
-
this.conditions.push([field as string, '!=', value]);
|
|
34
|
-
return this;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Greater than filter: field > value
|
|
39
|
-
*/
|
|
40
|
-
greaterThan<K extends keyof T>(field: K, value: T[K]): this {
|
|
41
|
-
this.conditions.push([field as string, '>', value]);
|
|
42
|
-
return this;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Greater than or equal filter: field >= value
|
|
47
|
-
*/
|
|
48
|
-
greaterThanOrEqual<K extends keyof T>(field: K, value: T[K]): this {
|
|
49
|
-
this.conditions.push([field as string, '>=', value]);
|
|
50
|
-
return this;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Less than filter: field < value
|
|
55
|
-
*/
|
|
56
|
-
lessThan<K extends keyof T>(field: K, value: T[K]): this {
|
|
57
|
-
this.conditions.push([field as string, '<', value]);
|
|
58
|
-
return this;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Less than or equal filter: field <= value
|
|
63
|
-
*/
|
|
64
|
-
lessThanOrEqual<K extends keyof T>(field: K, value: T[K]): this {
|
|
65
|
-
this.conditions.push([field as string, '<=', value]);
|
|
66
|
-
return this;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* IN filter: field IN (value1, value2, ...)
|
|
71
|
-
*/
|
|
72
|
-
in<K extends keyof T>(field: K, values: T[K][]): this {
|
|
73
|
-
this.conditions.push([field as string, 'in', values]);
|
|
74
|
-
return this;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* NOT IN filter: field NOT IN (value1, value2, ...)
|
|
79
|
-
*/
|
|
80
|
-
notIn<K extends keyof T>(field: K, values: T[K][]): this {
|
|
81
|
-
this.conditions.push([field as string, 'not_in', values]);
|
|
82
|
-
return this;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* LIKE filter: field LIKE pattern
|
|
87
|
-
*/
|
|
88
|
-
like<K extends keyof T>(field: K, pattern: string): this {
|
|
89
|
-
this.conditions.push([field as string, 'like', pattern]);
|
|
90
|
-
return this;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* IS NULL filter: field IS NULL
|
|
95
|
-
*/
|
|
96
|
-
isNull<K extends keyof T>(field: K): this {
|
|
97
|
-
this.conditions.push([field as string, 'is_null', null]);
|
|
98
|
-
return this;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* IS NOT NULL filter: field IS NOT NULL
|
|
103
|
-
*/
|
|
104
|
-
isNotNull<K extends keyof T>(field: K): this {
|
|
105
|
-
this.conditions.push([field as string, 'is_not_null', null]);
|
|
106
|
-
return this;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* BETWEEN filter: field BETWEEN min AND max
|
|
111
|
-
*/
|
|
112
|
-
between<K extends keyof T>(field: K, min: T[K], max: T[K]): this {
|
|
113
|
-
this.conditions.push(['and', [field as string, '>=', min], [field as string, '<=', max]] as FilterCondition);
|
|
114
|
-
return this;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* CONTAINS filter: field contains value (case-insensitive LIKE %value%)
|
|
119
|
-
*/
|
|
120
|
-
contains<K extends keyof T>(field: K, value: string): this {
|
|
121
|
-
this.conditions.push([field as string, 'like', `%${value}%`]);
|
|
122
|
-
return this;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* STARTS WITH filter: field starts with value (LIKE value%)
|
|
127
|
-
*/
|
|
128
|
-
startsWith<K extends keyof T>(field: K, value: string): this {
|
|
129
|
-
this.conditions.push([field as string, 'like', `${value}%`]);
|
|
130
|
-
return this;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* ENDS WITH filter: field ends with value (LIKE %value)
|
|
135
|
-
*/
|
|
136
|
-
endsWith<K extends keyof T>(field: K, value: string): this {
|
|
137
|
-
this.conditions.push([field as string, 'like', `%${value}`]);
|
|
138
|
-
return this;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* EXISTS filter: field is not null (alias for isNotNull)
|
|
143
|
-
*/
|
|
144
|
-
exists<K extends keyof T>(field: K): this {
|
|
145
|
-
this.conditions.push([field as string, 'is_not_null', null]);
|
|
146
|
-
return this;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Build the filter condition
|
|
151
|
-
*/
|
|
152
|
-
build(): FilterCondition {
|
|
153
|
-
if (this.conditions.length === 0) {
|
|
154
|
-
throw new Error('Filter builder has no conditions');
|
|
155
|
-
}
|
|
156
|
-
if (this.conditions.length === 1) {
|
|
157
|
-
return this.conditions[0];
|
|
158
|
-
}
|
|
159
|
-
// Combine multiple conditions with AND
|
|
160
|
-
return ['and', ...this.conditions];
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Get raw conditions array
|
|
165
|
-
*/
|
|
166
|
-
getConditions(): FilterCondition[] {
|
|
167
|
-
return this.conditions;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Type-safe query builder
|
|
173
|
-
*/
|
|
174
|
-
export class QueryBuilder<T = any> {
|
|
175
|
-
private query: Partial<QueryAST> = {};
|
|
176
|
-
private _object: string;
|
|
177
|
-
|
|
178
|
-
constructor(object: string) {
|
|
179
|
-
this._object = object;
|
|
180
|
-
this.query.object = object;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Select specific fields
|
|
185
|
-
*/
|
|
186
|
-
select<K extends keyof T>(...fields: K[]): this {
|
|
187
|
-
this.query.fields = fields as string[];
|
|
188
|
-
return this;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Add filters using a builder function
|
|
193
|
-
*/
|
|
194
|
-
where(builderFn: (builder: FilterBuilder<T>) => void): this {
|
|
195
|
-
const builder = new FilterBuilder<T>();
|
|
196
|
-
builderFn(builder);
|
|
197
|
-
const conditions = builder.getConditions();
|
|
198
|
-
|
|
199
|
-
if (conditions.length === 1) {
|
|
200
|
-
this.query.where = conditions[0];
|
|
201
|
-
} else if (conditions.length > 1) {
|
|
202
|
-
this.query.where = ['and', ...conditions] as FilterCondition;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return this;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Add raw filter condition
|
|
210
|
-
*/
|
|
211
|
-
filter(condition: FilterCondition): this {
|
|
212
|
-
this.query.where = condition;
|
|
213
|
-
return this;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Sort by fields
|
|
218
|
-
*/
|
|
219
|
-
orderBy<K extends keyof T>(field: K, order: 'asc' | 'desc' = 'asc'): this {
|
|
220
|
-
if (!this.query.orderBy) {
|
|
221
|
-
this.query.orderBy = [];
|
|
222
|
-
}
|
|
223
|
-
(this.query.orderBy as SortNode[]).push({
|
|
224
|
-
field: field as string,
|
|
225
|
-
order
|
|
226
|
-
});
|
|
227
|
-
return this;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Limit the number of results
|
|
232
|
-
*/
|
|
233
|
-
limit(count: number): this {
|
|
234
|
-
this.query.limit = count;
|
|
235
|
-
return this;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Skip records (for pagination).
|
|
240
|
-
* @deprecated Prefer `.offset()` for alignment with Spec canonical field names.
|
|
241
|
-
*/
|
|
242
|
-
skip(count: number): this {
|
|
243
|
-
this.query.offset = count;
|
|
244
|
-
return this;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Offset records (for pagination) — canonical alias for `.skip()`
|
|
249
|
-
*/
|
|
250
|
-
offset(count: number): this {
|
|
251
|
-
this.query.offset = count;
|
|
252
|
-
return this;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Paginate results
|
|
257
|
-
*/
|
|
258
|
-
paginate(page: number, pageSize: number): this {
|
|
259
|
-
this.query.limit = pageSize;
|
|
260
|
-
this.query.offset = (page - 1) * pageSize;
|
|
261
|
-
return this;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* Group by fields
|
|
266
|
-
*/
|
|
267
|
-
groupBy<K extends keyof T>(...fields: K[]): this {
|
|
268
|
-
this.query.groupBy = fields as string[];
|
|
269
|
-
return this;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Expand (eager-load) a related object with an optional sub-query
|
|
274
|
-
*/
|
|
275
|
-
expand(relation: string, subQuery?: Partial<QueryAST>): this {
|
|
276
|
-
if (!this.query.expand) {
|
|
277
|
-
this.query.expand = {};
|
|
278
|
-
}
|
|
279
|
-
(this.query.expand as Record<string, any>)[relation] = subQuery || {};
|
|
280
|
-
return this;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* Add full-text search
|
|
285
|
-
*/
|
|
286
|
-
search(query: string, options?: { fields?: string[]; fuzzy?: boolean }): this {
|
|
287
|
-
(this.query as any).search = { query, ...options };
|
|
288
|
-
return this;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* Set cursor for keyset pagination
|
|
293
|
-
*/
|
|
294
|
-
cursor(cursor: Record<string, any>): this {
|
|
295
|
-
(this.query as any).cursor = cursor;
|
|
296
|
-
return this;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* Enable SELECT DISTINCT
|
|
301
|
-
*/
|
|
302
|
-
distinct(): this {
|
|
303
|
-
(this.query as any).distinct = true;
|
|
304
|
-
return this;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* Build the final query AST
|
|
309
|
-
*/
|
|
310
|
-
build(): QueryAST {
|
|
311
|
-
return {
|
|
312
|
-
object: this._object,
|
|
313
|
-
...this.query
|
|
314
|
-
} as QueryAST;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* Get the current query state
|
|
319
|
-
*/
|
|
320
|
-
getQuery(): Partial<QueryAST> {
|
|
321
|
-
return { ...this.query };
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* Create a type-safe query builder for an object
|
|
327
|
-
*/
|
|
328
|
-
export function createQuery<T = any>(object: string): QueryBuilder<T> {
|
|
329
|
-
return new QueryBuilder<T>(object);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
/**
|
|
333
|
-
* Create a type-safe filter builder
|
|
334
|
-
*/
|
|
335
|
-
export function createFilter<T = any>(): FilterBuilder<T> {
|
|
336
|
-
return new FilterBuilder<T>();
|
|
337
|
-
}
|
package/src/realtime-api.ts
DELETED
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Realtime API Module for ObjectStackClient
|
|
5
|
-
*
|
|
6
|
-
* Provides real-time event subscription capabilities using long-polling.
|
|
7
|
-
* For production WebSocket/SSE support, extend with transport adapters.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import type { RealtimeEventPayload } from '@objectstack/spec/contracts';
|
|
11
|
-
import type { MetadataEvent, DataEvent } from '@objectstack/spec/api';
|
|
12
|
-
|
|
13
|
-
export interface RealtimeSubscriptionFilter {
|
|
14
|
-
/** Metadata/object type filter */
|
|
15
|
-
type?: string;
|
|
16
|
-
/** Package ID filter */
|
|
17
|
-
packageId?: string;
|
|
18
|
-
/** Event types to listen for */
|
|
19
|
-
eventTypes?: string[];
|
|
20
|
-
/** Record ID filter (for data events) */
|
|
21
|
-
recordId?: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export type RealtimeEventHandler = (event: RealtimeEventPayload) => void;
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Realtime API for subscribing to server events
|
|
28
|
-
*
|
|
29
|
-
* Note: Currently uses in-memory adapter. WebSocket/SSE transport planned for future.
|
|
30
|
-
*/
|
|
31
|
-
export class RealtimeAPI {
|
|
32
|
-
// @ts-expect-error - Reserved for future WebSocket/SSE implementation
|
|
33
|
-
private _baseUrl: string;
|
|
34
|
-
// @ts-expect-error - Reserved for future WebSocket/SSE implementation
|
|
35
|
-
private _token?: string;
|
|
36
|
-
private subscriptions = new Map<string, {
|
|
37
|
-
filter: RealtimeSubscriptionFilter;
|
|
38
|
-
handler: RealtimeEventHandler;
|
|
39
|
-
}>();
|
|
40
|
-
private pollInterval?: ReturnType<typeof setInterval>;
|
|
41
|
-
private eventBuffer: RealtimeEventPayload[] = [];
|
|
42
|
-
|
|
43
|
-
constructor(baseUrl: string, token?: string) {
|
|
44
|
-
this._baseUrl = baseUrl;
|
|
45
|
-
this._token = token;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Subscribe to metadata events
|
|
50
|
-
* Returns an unsubscribe function
|
|
51
|
-
*/
|
|
52
|
-
subscribeMetadata(
|
|
53
|
-
type: string,
|
|
54
|
-
callback: (event: MetadataEvent) => void,
|
|
55
|
-
options?: { packageId?: string }
|
|
56
|
-
): () => void {
|
|
57
|
-
const subscriptionId = `metadata-${type}-${Date.now()}`;
|
|
58
|
-
|
|
59
|
-
this.subscriptions.set(subscriptionId, {
|
|
60
|
-
filter: {
|
|
61
|
-
type,
|
|
62
|
-
packageId: options?.packageId,
|
|
63
|
-
eventTypes: [
|
|
64
|
-
`metadata.${type}.created`,
|
|
65
|
-
`metadata.${type}.updated`,
|
|
66
|
-
`metadata.${type}.deleted`
|
|
67
|
-
]
|
|
68
|
-
},
|
|
69
|
-
handler: (event) => {
|
|
70
|
-
// Type guard and filter
|
|
71
|
-
if (event.type.startsWith('metadata.')) {
|
|
72
|
-
callback(event as any as MetadataEvent);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
// Start polling if not already started
|
|
78
|
-
this.startPolling();
|
|
79
|
-
|
|
80
|
-
// Return unsubscribe function
|
|
81
|
-
return () => {
|
|
82
|
-
this.subscriptions.delete(subscriptionId);
|
|
83
|
-
if (this.subscriptions.size === 0) {
|
|
84
|
-
this.stopPolling();
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Subscribe to data record events
|
|
91
|
-
* Returns an unsubscribe function
|
|
92
|
-
*/
|
|
93
|
-
subscribeData(
|
|
94
|
-
object: string,
|
|
95
|
-
callback: (event: DataEvent) => void,
|
|
96
|
-
options?: { recordId?: string }
|
|
97
|
-
): () => void {
|
|
98
|
-
const subscriptionId = `data-${object}-${Date.now()}`;
|
|
99
|
-
|
|
100
|
-
this.subscriptions.set(subscriptionId, {
|
|
101
|
-
filter: {
|
|
102
|
-
type: object,
|
|
103
|
-
recordId: options?.recordId,
|
|
104
|
-
eventTypes: [
|
|
105
|
-
'data.record.created',
|
|
106
|
-
'data.record.updated',
|
|
107
|
-
'data.record.deleted'
|
|
108
|
-
]
|
|
109
|
-
},
|
|
110
|
-
handler: (event) => {
|
|
111
|
-
// Type guard and filter
|
|
112
|
-
if (event.type.startsWith('data.') && event.object === object) {
|
|
113
|
-
if (!options?.recordId || (event.payload as any)?.recordId === options.recordId) {
|
|
114
|
-
callback(event as any as DataEvent);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
// Start polling if not already started
|
|
121
|
-
this.startPolling();
|
|
122
|
-
|
|
123
|
-
// Return unsubscribe function
|
|
124
|
-
return () => {
|
|
125
|
-
this.subscriptions.delete(subscriptionId);
|
|
126
|
-
if (this.subscriptions.size === 0) {
|
|
127
|
-
this.stopPolling();
|
|
128
|
-
}
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Emit an event to all matching subscriptions (client-side only)
|
|
134
|
-
* This is used for in-process event delivery
|
|
135
|
-
*/
|
|
136
|
-
private emitEvent(event: RealtimeEventPayload): void {
|
|
137
|
-
for (const sub of this.subscriptions.values()) {
|
|
138
|
-
// Check if event matches subscription filters
|
|
139
|
-
const matchesType = !sub.filter.type ||
|
|
140
|
-
event.type.includes(sub.filter.type) ||
|
|
141
|
-
event.object === sub.filter.type;
|
|
142
|
-
|
|
143
|
-
const matchesEventType = !sub.filter.eventTypes?.length ||
|
|
144
|
-
sub.filter.eventTypes.includes(event.type);
|
|
145
|
-
|
|
146
|
-
const matchesPackage = !sub.filter.packageId ||
|
|
147
|
-
(event.payload as any)?.packageId === sub.filter.packageId;
|
|
148
|
-
|
|
149
|
-
if (matchesType && matchesEventType && matchesPackage) {
|
|
150
|
-
try {
|
|
151
|
-
sub.handler(event);
|
|
152
|
-
} catch (error) {
|
|
153
|
-
console.error('Error in realtime event handler:', error);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Start polling for events (fallback mechanism)
|
|
161
|
-
* In production, this would be replaced with WebSocket/SSE
|
|
162
|
-
*/
|
|
163
|
-
private startPolling(): void {
|
|
164
|
-
if (this.pollInterval) return;
|
|
165
|
-
|
|
166
|
-
// For now, we rely on the in-memory adapter within the same process
|
|
167
|
-
// Events are delivered synchronously via the IRealtimeService
|
|
168
|
-
// This polling is a placeholder for future WebSocket/SSE implementation
|
|
169
|
-
|
|
170
|
-
// Poll every 2 seconds for buffered events
|
|
171
|
-
this.pollInterval = setInterval(() => {
|
|
172
|
-
// Process any buffered events
|
|
173
|
-
while (this.eventBuffer.length > 0) {
|
|
174
|
-
const event = this.eventBuffer.shift();
|
|
175
|
-
if (event) {
|
|
176
|
-
this.emitEvent(event);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}, 2000);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Stop polling for events
|
|
184
|
-
*/
|
|
185
|
-
private stopPolling(): void {
|
|
186
|
-
if (this.pollInterval) {
|
|
187
|
-
clearInterval(this.pollInterval);
|
|
188
|
-
this.pollInterval = undefined;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Internal method to buffer events from server
|
|
194
|
-
* This would be called by WebSocket/SSE handlers in production
|
|
195
|
-
*/
|
|
196
|
-
_bufferEvent(event: RealtimeEventPayload): void {
|
|
197
|
-
this.eventBuffer.push(event);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Disconnect and clean up all subscriptions
|
|
202
|
-
*/
|
|
203
|
-
disconnect(): void {
|
|
204
|
-
this.stopPolling();
|
|
205
|
-
this.subscriptions.clear();
|
|
206
|
-
this.eventBuffer = [];
|
|
207
|
-
}
|
|
208
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Integration Test: Discovery & Connection
|
|
3
|
-
*
|
|
4
|
-
* Tests the client's ability to discover and connect to an ObjectStack server.
|
|
5
|
-
* These tests require a running server instance.
|
|
6
|
-
*
|
|
7
|
-
* @see CLIENT_SERVER_INTEGRATION_TESTS.md for full test specification
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { describe, test, expect } from 'vitest';
|
|
11
|
-
import { ObjectStackClient } from '../../src/index';
|
|
12
|
-
|
|
13
|
-
const TEST_SERVER_URL = process.env.TEST_SERVER_URL || 'http://localhost:3000';
|
|
14
|
-
|
|
15
|
-
describe('Discovery & Connection', () => {
|
|
16
|
-
describe('TC-DISC-001: Protocol-standard Discovery via /api/v1/discovery', () => {
|
|
17
|
-
test('should discover API from /api/v1/discovery', async () => {
|
|
18
|
-
const client = new ObjectStackClient({
|
|
19
|
-
baseUrl: TEST_SERVER_URL,
|
|
20
|
-
debug: true
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
const discovery = await client.connect();
|
|
24
|
-
|
|
25
|
-
expect(discovery.version).toBeDefined();
|
|
26
|
-
expect(discovery.apiName).toBeDefined();
|
|
27
|
-
expect(discovery.routes).toBeDefined();
|
|
28
|
-
expect(discovery.services).toBeDefined();
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
describe('TC-DISC-002: Discovery Information', () => {
|
|
33
|
-
test('should provide valid API version information', async () => {
|
|
34
|
-
const client = new ObjectStackClient({ baseUrl: TEST_SERVER_URL });
|
|
35
|
-
const discovery = await client.connect();
|
|
36
|
-
|
|
37
|
-
// Version should be a semantic version or API version string
|
|
38
|
-
expect(discovery.version).toMatch(/^v?\d+/);
|
|
39
|
-
|
|
40
|
-
// API name should be non-empty
|
|
41
|
-
expect(discovery.apiName.length).toBeGreaterThan(0);
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
describe('TC-DISC-003: Connection Failure Handling', () => {
|
|
46
|
-
test('should throw error when server is unreachable', async () => {
|
|
47
|
-
const client = new ObjectStackClient({
|
|
48
|
-
baseUrl: 'http://localhost:9999' // Invalid port
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
await expect(client.connect()).rejects.toThrow();
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
describe('TC-DISC-004: Route Resolution', () => {
|
|
56
|
-
test('should resolve API routes from discovery info', async () => {
|
|
57
|
-
const client = new ObjectStackClient({ baseUrl: TEST_SERVER_URL });
|
|
58
|
-
await client.connect();
|
|
59
|
-
|
|
60
|
-
// After connection, client should have discovery info
|
|
61
|
-
expect(client.discovery).toBeDefined();
|
|
62
|
-
expect(client.discovery?.version).toBeDefined();
|
|
63
|
-
|
|
64
|
-
// Verify that subsequent API calls can be made (routes are resolved)
|
|
65
|
-
// This implicitly tests route resolution
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
});
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
# Client Integration Tests
|
|
2
|
-
|
|
3
|
-
This directory contains integration tests that verify `@objectstack/client` against a live ObjectStack server.
|
|
4
|
-
|
|
5
|
-
## Running Tests
|
|
6
|
-
|
|
7
|
-
### Prerequisites
|
|
8
|
-
|
|
9
|
-
**Note:** Integration tests require a running ObjectStack server with test data. The server is provided by a separate repository and must be set up independently.
|
|
10
|
-
|
|
11
|
-
1. **Start a test server (external dependency):**
|
|
12
|
-
```bash
|
|
13
|
-
# In the ObjectStack server repository (separate from this package)
|
|
14
|
-
# Follow that project's documentation for test server setup
|
|
15
|
-
# Example: cd /path/to/objectstack-server && pnpm dev:test
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
2. **Run integration tests (from this package):**
|
|
19
|
-
```bash
|
|
20
|
-
pnpm test:integration
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
### Environment Variables
|
|
24
|
-
|
|
25
|
-
- `TEST_SERVER_URL` - Base URL of the test server (default: `http://localhost:3000`)
|
|
26
|
-
- `TEST_USER_EMAIL` - Test user email (default: `test@example.com`)
|
|
27
|
-
- `TEST_USER_PASSWORD` - Test user password (default: `TestPassword123!`)
|
|
28
|
-
|
|
29
|
-
## Test Structure
|
|
30
|
-
|
|
31
|
-
Tests are organized by protocol namespace:
|
|
32
|
-
|
|
33
|
-
```
|
|
34
|
-
01-discovery.test.ts # Discovery & connection
|
|
35
|
-
02-auth.test.ts # Authentication flows
|
|
36
|
-
03-metadata.test.ts # Metadata operations
|
|
37
|
-
04-data-crud.test.ts # Basic CRUD operations
|
|
38
|
-
05-data-batch.test.ts # Batch operations
|
|
39
|
-
06-data-query.test.ts # Advanced queries
|
|
40
|
-
07-permissions.test.ts # Permission checking
|
|
41
|
-
08-workflow.test.ts # Workflow operations
|
|
42
|
-
09-realtime.test.ts # Realtime subscriptions
|
|
43
|
-
10-notifications.test.ts # Notifications
|
|
44
|
-
11-ai.test.ts # AI services
|
|
45
|
-
12-i18n.test.ts # Internationalization
|
|
46
|
-
13-analytics.test.ts # Analytics queries
|
|
47
|
-
14-packages.test.ts # Package management
|
|
48
|
-
15-views.test.ts # View management
|
|
49
|
-
16-storage.test.ts # File storage
|
|
50
|
-
17-automation.test.ts # Automation triggers
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
## Test Coverage Goals
|
|
54
|
-
|
|
55
|
-
- Core Services (discovery, meta, data, auth): **100%**
|
|
56
|
-
- Optional Services: **90%**
|
|
57
|
-
- Error Scenarios: **80%**
|
|
58
|
-
- Edge Cases: **70%**
|
|
59
|
-
|
|
60
|
-
## Related Documentation
|
|
61
|
-
|
|
62
|
-
- [Integration Test Specification](../../CLIENT_SERVER_INTEGRATION_TESTS.md)
|
|
63
|
-
- [Client Spec Compliance](../../CLIENT_SPEC_COMPLIANCE.md)
|
|
64
|
-
|
|
65
|
-
## CI/CD
|
|
66
|
-
|
|
67
|
-
Integration tests can be run in CI, but require:
|
|
68
|
-
- A running ObjectStack server instance (from separate repository)
|
|
69
|
-
- Test database with sample data
|
|
70
|
-
- Proper environment configuration
|
|
71
|
-
|
|
72
|
-
See `CLIENT_SERVER_INTEGRATION_TESTS.md` for example CI configuration structure.
|
package/tsconfig.json
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "../../tsconfig.json",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"outDir": "./dist",
|
|
5
|
-
"rootDir": "./src",
|
|
6
|
-
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
7
|
-
"types": ["node"]
|
|
8
|
-
},
|
|
9
|
-
"include": ["src/**/*"],
|
|
10
|
-
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
11
|
-
}
|
package/vitest.config.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from 'vitest/config';
|
|
2
|
-
|
|
3
|
-
export default defineConfig({
|
|
4
|
-
test: {
|
|
5
|
-
// Exclude integration tests that require a running server
|
|
6
|
-
exclude: [
|
|
7
|
-
'**/node_modules/**',
|
|
8
|
-
'**/dist/**',
|
|
9
|
-
'tests/integration/**',
|
|
10
|
-
],
|
|
11
|
-
environment: 'node',
|
|
12
|
-
}
|
|
13
|
-
});
|