@objectstack/client 0.8.2 → 0.9.1

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 CHANGED
@@ -1,5 +1,14 @@
1
1
  # @objectstack/client
2
2
 
3
+ ## 0.9.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Patch release for maintenance and stability improvements. All packages updated with unified versioning.
8
+ - Updated dependencies
9
+ - @objectstack/spec@0.9.1
10
+ - @objectstack/core@0.9.1
11
+
3
12
  ## 0.8.2
4
13
 
5
14
  ### Patch Changes
package/README.md CHANGED
@@ -157,10 +157,48 @@ try {
157
157
  }
158
158
  ```
159
159
 
160
- Common error codes:
161
- - `validation_error`: Input validation failed
162
- - `unauthenticated`: Authentication required
163
- - `permission_denied`: Insufficient permissions
164
- - `resource_not_found`: Resource does not exist
165
- - `rate_limit_exceeded`: Too many requests
160
+ ### Error Code Reference
161
+
162
+ #### Client Errors (4xx)
163
+
164
+ | Error Code | HTTP Status | Retryable | Description |
165
+ |------------|-------------|-----------|-------------|
166
+ | `validation_error` | 400 | No | Input validation failed. Check error.details for field-specific errors |
167
+ | `unauthenticated` | 401 | No | Authentication required. Provide valid token |
168
+ | `permission_denied` | 403 | No | Insufficient permissions for this operation |
169
+ | `resource_not_found` | 404 | No | Resource does not exist. Verify object name or record ID |
170
+ | `conflict` | 409 | No | Resource conflict (e.g., duplicate unique field) |
171
+ | `rate_limit_exceeded` | 429 | Yes | Too many requests. Wait before retrying |
172
+
173
+ #### Server Errors (5xx)
174
+
175
+ | Error Code | HTTP Status | Retryable | Description |
176
+ |------------|-------------|-----------|-------------|
177
+ | `internal_error` | 500 | Yes | Server encountered an error. Retry with backoff |
178
+ | `service_unavailable` | 503 | Yes | Service temporarily unavailable. Retry later |
179
+ | `gateway_timeout` | 504 | Yes | Request timeout. Consider increasing timeout or retrying |
180
+
181
+ **Retry Strategy Example:**
182
+
183
+ ```typescript
184
+ async function retryableRequest<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> {
185
+ for (let i = 0; i < maxRetries; i++) {
186
+ try {
187
+ return await fn();
188
+ } catch (error: any) {
189
+ if (!error.retryable || i === maxRetries - 1) {
190
+ throw error;
191
+ }
192
+ // Exponential backoff
193
+ await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000));
194
+ }
195
+ }
196
+ throw new Error('Max retries exceeded');
197
+ }
198
+
199
+ // Usage
200
+ const data = await retryableRequest(() =>
201
+ client.data.create('todo_task', { subject: 'Task' })
202
+ );
203
+ ```
166
204
 
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { QueryAST, SortNode, AggregationNode } from '@objectstack/spec/data';
2
- import { BatchUpdateRequest, BatchUpdateResponse, BatchOptions, MetadataCacheRequest, MetadataCacheResponse, StandardErrorCode, ErrorCategory } from '@objectstack/spec/api';
2
+ import { BatchUpdateRequest, BatchUpdateResponse, BatchOptions, MetadataCacheRequest, MetadataCacheResponse, StandardErrorCode, ErrorCategory, GetDiscoveryResponse, GetMetaTypesResponse, GetMetaItemsResponse } from '@objectstack/spec/api';
3
3
  import { Logger } from '@objectstack/core';
4
4
  export interface ClientConfig {
5
5
  baseUrl: string;
@@ -17,16 +17,11 @@ export interface ClientConfig {
17
17
  */
18
18
  debug?: boolean;
19
19
  }
20
- export interface DiscoveryResult {
21
- routes: {
22
- discovery: string;
23
- metadata: string;
24
- data: string;
25
- auth: string;
26
- ui: string;
27
- };
28
- capabilities?: Record<string, boolean>;
29
- }
20
+ /**
21
+ * Discovery Result
22
+ * Re-export from @objectstack/spec/api for convenience
23
+ */
24
+ export type DiscoveryResult = GetDiscoveryResponse;
30
25
  export interface QueryOptions {
31
26
  select?: string[];
32
27
  filters?: Record<string, any>;
@@ -52,18 +47,44 @@ export declare class ObjectStackClient {
52
47
  private baseUrl;
53
48
  private token?;
54
49
  private fetchImpl;
55
- private routes?;
50
+ private discoveryInfo?;
56
51
  private logger;
57
52
  constructor(config: ClientConfig);
58
53
  /**
59
- * Initialize the client by discovering server capabilities and routes.
54
+ * Initialize the client by discovering server capabilities.
60
55
  */
61
- connect(): Promise<DiscoveryResult>;
56
+ connect(): Promise<{
57
+ version: string;
58
+ apiName: string;
59
+ capabilities?: string[] | undefined;
60
+ endpoints?: Record<string, string> | undefined;
61
+ }>;
62
62
  /**
63
63
  * Metadata Operations
64
64
  */
65
65
  meta: {
66
+ /**
67
+ * Get all available metadata types
68
+ * Returns types like 'object', 'plugin', 'view', etc.
69
+ */
70
+ getTypes: () => Promise<GetMetaTypesResponse>;
71
+ /**
72
+ * Get all items of a specific metadata type
73
+ * @param type - Metadata type name (e.g., 'object', 'plugin')
74
+ */
75
+ getItems: (type: string) => Promise<GetMetaItemsResponse>;
76
+ /**
77
+ * Get a specific object definition by name
78
+ * @deprecated Use `getItem('object', name)` instead for consistency with spec protocol
79
+ * @param name - Object name (snake_case identifier)
80
+ */
66
81
  getObject: (name: string) => Promise<any>;
82
+ /**
83
+ * Get a specific metadata item by type and name
84
+ * @param type - Metadata type (e.g., 'object', 'plugin')
85
+ * @param name - Item name (snake_case identifier)
86
+ */
87
+ getItem: (type: string, name: string) => Promise<any>;
67
88
  /**
68
89
  * Get object metadata with cache support
69
90
  * Supports ETag-based conditional requests for efficient caching
@@ -111,7 +132,11 @@ export declare class ObjectStackClient {
111
132
  */
112
133
  private isFilterAST;
113
134
  private fetch;
135
+ /**
136
+ * Get the conventional route path for a given API endpoint type
137
+ * ObjectStack uses standard conventions: /api/v1/data, /api/v1/meta, /api/v1/ui
138
+ */
114
139
  private getRoute;
115
140
  }
116
141
  export { QueryBuilder, FilterBuilder, createQuery, createFilter } from './query-builder';
117
- export type { BatchUpdateRequest, BatchUpdateResponse, UpdateManyRequest, DeleteManyRequest, BatchOptions, BatchRecord, BatchOperationResult, MetadataCacheRequest, MetadataCacheResponse, StandardErrorCode, ErrorCategory } from '@objectstack/spec/api';
142
+ export type { BatchUpdateRequest, BatchUpdateResponse, UpdateManyRequest, DeleteManyRequest, BatchOptions, BatchRecord, BatchOperationResult, MetadataCacheRequest, MetadataCacheResponse, StandardErrorCode, ErrorCategory, GetDiscoveryResponse, GetMetaTypesResponse, GetMetaItemsResponse } from '@objectstack/spec/api';
package/dist/index.js CHANGED
@@ -5,11 +5,44 @@ export class ObjectStackClient {
5
5
  * Metadata Operations
6
6
  */
7
7
  this.meta = {
8
+ /**
9
+ * Get all available metadata types
10
+ * Returns types like 'object', 'plugin', 'view', etc.
11
+ */
12
+ getTypes: async () => {
13
+ const route = this.getRoute('metadata');
14
+ const res = await this.fetch(`${this.baseUrl}${route}`);
15
+ return res.json();
16
+ },
17
+ /**
18
+ * Get all items of a specific metadata type
19
+ * @param type - Metadata type name (e.g., 'object', 'plugin')
20
+ */
21
+ getItems: async (type) => {
22
+ const route = this.getRoute('metadata');
23
+ const res = await this.fetch(`${this.baseUrl}${route}/${type}`);
24
+ return res.json();
25
+ },
26
+ /**
27
+ * Get a specific object definition by name
28
+ * @deprecated Use `getItem('object', name)` instead for consistency with spec protocol
29
+ * @param name - Object name (snake_case identifier)
30
+ */
8
31
  getObject: async (name) => {
9
32
  const route = this.getRoute('metadata');
10
33
  const res = await this.fetch(`${this.baseUrl}${route}/object/${name}`);
11
34
  return res.json();
12
35
  },
36
+ /**
37
+ * Get a specific metadata item by type and name
38
+ * @param type - Metadata type (e.g., 'object', 'plugin')
39
+ * @param name - Item name (snake_case identifier)
40
+ */
41
+ getItem: async (type, name) => {
42
+ const route = this.getRoute('metadata');
43
+ const res = await this.fetch(`${this.baseUrl}${route}/${type}/${name}`);
44
+ return res.json();
45
+ },
13
46
  /**
14
47
  * Get object metadata with cache support
15
48
  * Supports ETag-based conditional requests for efficient caching
@@ -212,18 +245,18 @@ export class ObjectStackClient {
212
245
  this.logger.debug('ObjectStack client created', { baseUrl: this.baseUrl });
213
246
  }
214
247
  /**
215
- * Initialize the client by discovering server capabilities and routes.
248
+ * Initialize the client by discovering server capabilities.
216
249
  */
217
250
  async connect() {
218
251
  this.logger.debug('Connecting to ObjectStack server', { baseUrl: this.baseUrl });
219
252
  try {
220
- // Connect to the discovery endpoint
221
- // During boot, we might not know routes, so we check convention /api/v1 first
253
+ // Connect to the discovery endpoint at /api/v1
222
254
  const res = await this.fetch(`${this.baseUrl}/api/v1`);
223
255
  const data = await res.json();
224
- this.routes = data.routes;
256
+ this.discoveryInfo = data;
225
257
  this.logger.info('Connected to ObjectStack server', {
226
- routes: Object.keys(data.routes || {}),
258
+ version: data.version,
259
+ apiName: data.apiName,
227
260
  capabilities: data.capabilities
228
261
  });
229
262
  return data;
@@ -290,16 +323,19 @@ export class ObjectStackClient {
290
323
  }
291
324
  return res;
292
325
  }
293
- getRoute(key) {
294
- if (!this.routes) {
295
- // Fallback for strictness, but we allow bootstrapping
296
- this.logger.warn('Accessing route before connect()', {
297
- route: key,
298
- fallback: `/api/v1/${key}`
299
- });
300
- return `/api/v1/${key}`;
301
- }
302
- return this.routes[key] || `/api/v1/${key}`;
326
+ /**
327
+ * Get the conventional route path for a given API endpoint type
328
+ * ObjectStack uses standard conventions: /api/v1/data, /api/v1/meta, /api/v1/ui
329
+ */
330
+ getRoute(type) {
331
+ // Use conventional ObjectStack API paths
332
+ const routeMap = {
333
+ data: '/api/v1/data',
334
+ metadata: '/api/v1/meta',
335
+ ui: '/api/v1/ui',
336
+ auth: '/api/v1/auth'
337
+ };
338
+ return routeMap[type] || `/api/v1/${type}`;
303
339
  }
304
340
  }
305
341
  // Re-export type-safe query builder
package/package.json CHANGED
@@ -1,17 +1,19 @@
1
1
  {
2
2
  "name": "@objectstack/client",
3
- "version": "0.8.2",
3
+ "version": "0.9.1",
4
4
  "description": "Official Client SDK for ObjectStack Protocol",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "dependencies": {
8
- "@objectstack/spec": "0.8.2",
9
- "@objectstack/core": "0.8.2"
8
+ "@objectstack/spec": "0.9.1",
9
+ "@objectstack/core": "0.9.1"
10
10
  },
11
11
  "devDependencies": {
12
- "typescript": "^5.0.0"
12
+ "typescript": "^5.0.0",
13
+ "vitest": "^4.0.18"
13
14
  },
14
15
  "scripts": {
15
- "build": "tsc"
16
+ "build": "tsc",
17
+ "test": "vitest run"
16
18
  }
17
19
  }
@@ -0,0 +1,91 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { ObjectStackClient } from './index';
3
+
4
+ describe('ObjectStackClient', () => {
5
+ it('should initialize with correct configuration', () => {
6
+ const client = new ObjectStackClient({ baseUrl: 'http://localhost:3000' });
7
+ expect(client).toBeDefined();
8
+ });
9
+
10
+ it('should normalize base URL', () => {
11
+ const client: any = new ObjectStackClient({ baseUrl: 'http://localhost:3000/' });
12
+ expect(client.baseUrl).toBe('http://localhost:3000');
13
+ });
14
+
15
+ it('should make discovery request on connect', async () => {
16
+ const fetchMock = vi.fn().mockResolvedValue({
17
+ ok: true,
18
+ json: async () => ({
19
+ version: 'v1',
20
+ apiName: 'ObjectStack',
21
+ capabilities: ['metadata', 'data', 'ui'],
22
+ endpoints: {}
23
+ })
24
+ });
25
+
26
+ const client = new ObjectStackClient({
27
+ baseUrl: 'http://localhost:3000',
28
+ fetch: fetchMock
29
+ });
30
+
31
+ await client.connect();
32
+ expect(fetchMock).toHaveBeenCalledWith('http://localhost:3000/api/v1', expect.any(Object));
33
+ });
34
+
35
+ it('should get metadata types', async () => {
36
+ const fetchMock = vi.fn().mockResolvedValue({
37
+ ok: true,
38
+ json: async () => ({
39
+ types: ['object', 'plugin', 'view']
40
+ })
41
+ });
42
+
43
+ const client = new ObjectStackClient({
44
+ baseUrl: 'http://localhost:3000',
45
+ fetch: fetchMock
46
+ });
47
+
48
+ const result = await client.meta.getTypes();
49
+ expect(fetchMock).toHaveBeenCalledWith('http://localhost:3000/api/v1/meta', expect.any(Object));
50
+ expect(result.types).toEqual(['object', 'plugin', 'view']);
51
+ });
52
+
53
+ it('should get metadata items by type', async () => {
54
+ const fetchMock = vi.fn().mockResolvedValue({
55
+ ok: true,
56
+ json: async () => ({
57
+ type: 'object',
58
+ items: [{ name: 'customer' }, { name: 'order' }]
59
+ })
60
+ });
61
+
62
+ const client = new ObjectStackClient({
63
+ baseUrl: 'http://localhost:3000',
64
+ fetch: fetchMock
65
+ });
66
+
67
+ const result = await client.meta.getItems('object');
68
+ expect(fetchMock).toHaveBeenCalledWith('http://localhost:3000/api/v1/meta/object', expect.any(Object));
69
+ expect(result.type).toBe('object');
70
+ expect(result.items).toHaveLength(2);
71
+ });
72
+
73
+ it('should get metadata item by type and name', async () => {
74
+ const fetchMock = vi.fn().mockResolvedValue({
75
+ ok: true,
76
+ json: async () => ({
77
+ name: 'customer',
78
+ fields: []
79
+ })
80
+ });
81
+
82
+ const client = new ObjectStackClient({
83
+ baseUrl: 'http://localhost:3000',
84
+ fetch: fetchMock
85
+ });
86
+
87
+ const result = await client.meta.getItem('object', 'customer');
88
+ expect(fetchMock).toHaveBeenCalledWith('http://localhost:3000/api/v1/meta/object/customer', expect.any(Object));
89
+ expect(result.name).toBe('customer');
90
+ });
91
+ });
package/src/index.ts CHANGED
@@ -8,7 +8,10 @@ import {
8
8
  MetadataCacheRequest,
9
9
  MetadataCacheResponse,
10
10
  StandardErrorCode,
11
- ErrorCategory
11
+ ErrorCategory,
12
+ GetDiscoveryResponse,
13
+ GetMetaTypesResponse,
14
+ GetMetaItemsResponse
12
15
  } from '@objectstack/spec/api';
13
16
  import { Logger, createLogger } from '@objectstack/core';
14
17
 
@@ -29,16 +32,11 @@ export interface ClientConfig {
29
32
  debug?: boolean;
30
33
  }
31
34
 
32
- export interface DiscoveryResult {
33
- routes: {
34
- discovery: string;
35
- metadata: string;
36
- data: string;
37
- auth: string;
38
- ui: string;
39
- };
40
- capabilities?: Record<string, boolean>;
41
- }
35
+ /**
36
+ * Discovery Result
37
+ * Re-export from @objectstack/spec/api for convenience
38
+ */
39
+ export type DiscoveryResult = GetDiscoveryResponse;
42
40
 
43
41
  export interface QueryOptions {
44
42
  select?: string[]; // Simplified Selection
@@ -69,7 +67,7 @@ export class ObjectStackClient {
69
67
  private baseUrl: string;
70
68
  private token?: string;
71
69
  private fetchImpl: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
72
- private routes?: DiscoveryResult['routes'];
70
+ private discoveryInfo?: DiscoveryResult;
73
71
  private logger: Logger;
74
72
 
75
73
  constructor(config: ClientConfig) {
@@ -87,21 +85,21 @@ export class ObjectStackClient {
87
85
  }
88
86
 
89
87
  /**
90
- * Initialize the client by discovering server capabilities and routes.
88
+ * Initialize the client by discovering server capabilities.
91
89
  */
92
90
  async connect() {
93
91
  this.logger.debug('Connecting to ObjectStack server', { baseUrl: this.baseUrl });
94
92
 
95
93
  try {
96
- // Connect to the discovery endpoint
97
- // During boot, we might not know routes, so we check convention /api/v1 first
94
+ // Connect to the discovery endpoint at /api/v1
98
95
  const res = await this.fetch(`${this.baseUrl}/api/v1`);
99
96
 
100
97
  const data = await res.json();
101
- this.routes = data.routes;
98
+ this.discoveryInfo = data;
102
99
 
103
100
  this.logger.info('Connected to ObjectStack server', {
104
- routes: Object.keys(data.routes || {}),
101
+ version: data.version,
102
+ apiName: data.apiName,
105
103
  capabilities: data.capabilities
106
104
  });
107
105
 
@@ -116,11 +114,47 @@ export class ObjectStackClient {
116
114
  * Metadata Operations
117
115
  */
118
116
  meta = {
117
+ /**
118
+ * Get all available metadata types
119
+ * Returns types like 'object', 'plugin', 'view', etc.
120
+ */
121
+ getTypes: async (): Promise<GetMetaTypesResponse> => {
122
+ const route = this.getRoute('metadata');
123
+ const res = await this.fetch(`${this.baseUrl}${route}`);
124
+ return res.json();
125
+ },
126
+
127
+ /**
128
+ * Get all items of a specific metadata type
129
+ * @param type - Metadata type name (e.g., 'object', 'plugin')
130
+ */
131
+ getItems: async (type: string): Promise<GetMetaItemsResponse> => {
132
+ const route = this.getRoute('metadata');
133
+ const res = await this.fetch(`${this.baseUrl}${route}/${type}`);
134
+ return res.json();
135
+ },
136
+
137
+ /**
138
+ * Get a specific object definition by name
139
+ * @deprecated Use `getItem('object', name)` instead for consistency with spec protocol
140
+ * @param name - Object name (snake_case identifier)
141
+ */
119
142
  getObject: async (name: string) => {
120
143
  const route = this.getRoute('metadata');
121
144
  const res = await this.fetch(`${this.baseUrl}${route}/object/${name}`);
122
145
  return res.json();
123
146
  },
147
+
148
+ /**
149
+ * Get a specific metadata item by type and name
150
+ * @param type - Metadata type (e.g., 'object', 'plugin')
151
+ * @param name - Item name (snake_case identifier)
152
+ */
153
+ getItem: async (type: string, name: string) => {
154
+ const route = this.getRoute('metadata');
155
+ const res = await this.fetch(`${this.baseUrl}${route}/${type}/${name}`);
156
+ return res.json();
157
+ },
124
158
 
125
159
  /**
126
160
  * Get object metadata with cache support
@@ -407,16 +441,20 @@ export class ObjectStackClient {
407
441
  return res;
408
442
  }
409
443
 
410
- private getRoute(key: keyof DiscoveryResult['routes']): string {
411
- if (!this.routes) {
412
- // Fallback for strictness, but we allow bootstrapping
413
- this.logger.warn('Accessing route before connect()', {
414
- route: key,
415
- fallback: `/api/v1/${key}`
416
- });
417
- return `/api/v1/${key}`;
418
- }
419
- return this.routes[key] || `/api/v1/${key}`;
444
+ /**
445
+ * Get the conventional route path for a given API endpoint type
446
+ * ObjectStack uses standard conventions: /api/v1/data, /api/v1/meta, /api/v1/ui
447
+ */
448
+ private getRoute(type: 'data' | 'metadata' | 'ui' | 'auth'): string {
449
+ // Use conventional ObjectStack API paths
450
+ const routeMap: Record<string, string> = {
451
+ data: '/api/v1/data',
452
+ metadata: '/api/v1/meta',
453
+ ui: '/api/v1/ui',
454
+ auth: '/api/v1/auth'
455
+ };
456
+
457
+ return routeMap[type] || `/api/v1/${type}`;
420
458
  }
421
459
  }
422
460
 
@@ -435,5 +473,8 @@ export type {
435
473
  MetadataCacheRequest,
436
474
  MetadataCacheResponse,
437
475
  StandardErrorCode,
438
- ErrorCategory
476
+ ErrorCategory,
477
+ GetDiscoveryResponse,
478
+ GetMetaTypesResponse,
479
+ GetMetaItemsResponse
439
480
  } from '@objectstack/spec/api';
package/tsconfig.json CHANGED
@@ -9,5 +9,6 @@
9
9
  "esModuleInterop": true,
10
10
  "skipLibCheck": true
11
11
  },
12
- "include": ["src/**/*"]
12
+ "include": ["src/**/*"],
13
+ "exclude": ["node_modules", "dist", "**/*.test.ts"]
13
14
  }