@objectstack/client 0.6.1 → 0.7.2
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 +18 -0
- package/README.md +89 -5
- package/dist/index.d.ts +78 -3
- package/dist/index.js +217 -12
- package/dist/query-builder.d.ts +124 -0
- package/dist/query-builder.js +221 -0
- package/package.json +3 -2
- package/src/index.ts +304 -13
- package/src/query-builder.ts +251 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @objectstack/client
|
|
2
2
|
|
|
3
|
+
## 0.7.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- fb41cc0: Patch release: Updated documentation and JSON schemas
|
|
8
|
+
- Updated dependencies [fb41cc0]
|
|
9
|
+
- @objectstack/spec@0.7.2
|
|
10
|
+
- @objectstack/core@0.7.2
|
|
11
|
+
|
|
12
|
+
## 0.7.1
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- Patch release for maintenance and stability improvements
|
|
17
|
+
- Updated dependencies
|
|
18
|
+
- @objectstack/spec@0.7.1
|
|
19
|
+
- @objectstack/core@0.7.1
|
|
20
|
+
|
|
3
21
|
## 0.6.1
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -6,7 +6,11 @@ The official TypeScript client for ObjectStack.
|
|
|
6
6
|
|
|
7
7
|
- **Auto-Discovery**: Connects to your ObjectStack server and automatically configures API endpoints.
|
|
8
8
|
- **Typed Metadata**: Retrieve Object and View definitions with full type support.
|
|
9
|
+
- **Metadata Caching**: ETag-based conditional requests for efficient metadata caching.
|
|
9
10
|
- **Unified Data Access**: Simple CRUD operations for any object in your schema.
|
|
11
|
+
- **Batch Operations**: Efficient bulk create/update/delete with transaction support.
|
|
12
|
+
- **View Storage**: Save, load, and share custom UI view configurations.
|
|
13
|
+
- **Standardized Errors**: Machine-readable error codes with retry guidance.
|
|
10
14
|
|
|
11
15
|
## Installation
|
|
12
16
|
|
|
@@ -47,8 +51,48 @@ async function main() {
|
|
|
47
51
|
priority: 1
|
|
48
52
|
});
|
|
49
53
|
|
|
50
|
-
// 6. Batch Operations
|
|
51
|
-
await client.data.
|
|
54
|
+
// 6. Batch Operations (New!)
|
|
55
|
+
const batchResult = await client.data.batch('todo_task', {
|
|
56
|
+
operation: 'update',
|
|
57
|
+
records: [
|
|
58
|
+
{ id: '1', data: { status: 'active' } },
|
|
59
|
+
{ id: '2', data: { status: 'active' } }
|
|
60
|
+
],
|
|
61
|
+
options: {
|
|
62
|
+
atomic: true, // Rollback on any failure
|
|
63
|
+
returnRecords: true // Include full records in response
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
console.log(`Updated ${batchResult.succeeded} records`);
|
|
67
|
+
|
|
68
|
+
// 7. Metadata Caching (New!)
|
|
69
|
+
const cachedObject = await client.meta.getCached('todo_task', {
|
|
70
|
+
ifNoneMatch: '"686897696a7c876b7e"' // ETag from previous request
|
|
71
|
+
});
|
|
72
|
+
if (cachedObject.notModified) {
|
|
73
|
+
console.log('Using cached metadata');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 8. View Storage (New!)
|
|
77
|
+
const view = await client.views.create({
|
|
78
|
+
name: 'active_tasks',
|
|
79
|
+
label: 'Active Tasks',
|
|
80
|
+
object: 'todo_task',
|
|
81
|
+
type: 'list',
|
|
82
|
+
visibility: 'public',
|
|
83
|
+
query: {
|
|
84
|
+
object: 'todo_task',
|
|
85
|
+
where: { status: 'active' },
|
|
86
|
+
orderBy: [{ field: 'priority', order: 'desc' }],
|
|
87
|
+
limit: 50
|
|
88
|
+
},
|
|
89
|
+
layout: {
|
|
90
|
+
columns: [
|
|
91
|
+
{ field: 'subject', label: 'Task', width: 200 },
|
|
92
|
+
{ field: 'priority', label: 'Priority', width: 100 }
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
});
|
|
52
96
|
}
|
|
53
97
|
```
|
|
54
98
|
|
|
@@ -59,6 +103,7 @@ Initializes the client by fetching the system discovery manifest from `/api/v1`.
|
|
|
59
103
|
|
|
60
104
|
### `client.meta`
|
|
61
105
|
- `getObject(name: string)`: Get object schema.
|
|
106
|
+
- `getCached(name: string, options?)`: Get object schema with ETag-based caching.
|
|
62
107
|
- `getView(name: string)`: Get view configuration.
|
|
63
108
|
|
|
64
109
|
### `client.data`
|
|
@@ -66,11 +111,21 @@ Initializes the client by fetching the system discovery manifest from `/api/v1`.
|
|
|
66
111
|
- `get<T>(object, id)`: Get single record by ID.
|
|
67
112
|
- `query<T>(object, ast)`: Execute complex query using full AST.
|
|
68
113
|
- `create<T>(object, data)`: Create record.
|
|
69
|
-
- `
|
|
114
|
+
- `batch(object, request)`: **Recommended** - Execute batch operations (create/update/upsert/delete) with full control.
|
|
115
|
+
- `createMany<T>(object, data[])`: Batch create records (convenience method).
|
|
70
116
|
- `update<T>(object, id, data)`: Update record.
|
|
71
|
-
- `updateMany<T>(object,
|
|
117
|
+
- `updateMany<T>(object, records[], options?)`: Batch update records (convenience method).
|
|
72
118
|
- `delete(object, id)`: Delete record.
|
|
73
|
-
- `deleteMany(object, ids[])`: Batch delete records.
|
|
119
|
+
- `deleteMany(object, ids[], options?)`: Batch delete records (convenience method).
|
|
120
|
+
|
|
121
|
+
### `client.views` (New!)
|
|
122
|
+
- `create(request)`: Create a new saved view.
|
|
123
|
+
- `get(id)`: Get a saved view by ID.
|
|
124
|
+
- `list(request?)`: List saved views with optional filters.
|
|
125
|
+
- `update(request)`: Update an existing view.
|
|
126
|
+
- `delete(id)`: Delete a saved view.
|
|
127
|
+
- `share(id, userIds[])`: Share a view with users/teams.
|
|
128
|
+
- `setDefault(id, object)`: Set a view as default for an object.
|
|
74
129
|
|
|
75
130
|
### Query Options
|
|
76
131
|
The `find` method accepts an options object:
|
|
@@ -80,3 +135,32 @@ The `find` method accepts an options object:
|
|
|
80
135
|
- `top`: Limit number of records.
|
|
81
136
|
- `skip`: Offset for pagination.
|
|
82
137
|
|
|
138
|
+
### Batch Options
|
|
139
|
+
Batch operations support the following options:
|
|
140
|
+
- `atomic`: If true, rollback entire batch on any failure (default: true).
|
|
141
|
+
- `returnRecords`: If true, return full record data in response (default: false).
|
|
142
|
+
- `continueOnError`: If true (and atomic=false), continue processing remaining records after errors.
|
|
143
|
+
- `validateOnly`: If true, validate records without persisting changes (dry-run mode).
|
|
144
|
+
|
|
145
|
+
### Error Handling
|
|
146
|
+
The client provides standardized error handling with machine-readable error codes:
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
try {
|
|
150
|
+
await client.data.create('todo_task', { subject: '' });
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.error('Error code:', error.code); // e.g., 'validation_error'
|
|
153
|
+
console.error('Category:', error.category); // e.g., 'validation'
|
|
154
|
+
console.error('HTTP status:', error.httpStatus); // e.g., 400
|
|
155
|
+
console.error('Retryable:', error.retryable); // e.g., false
|
|
156
|
+
console.error('Details:', error.details); // Additional error info
|
|
157
|
+
}
|
|
158
|
+
```
|
|
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
|
|
166
|
+
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { QueryAST, SortNode, AggregationNode } from '@objectstack/spec/data';
|
|
2
|
+
import { BatchUpdateRequest, BatchUpdateResponse, BatchOptions, MetadataCacheRequest, MetadataCacheResponse, StandardErrorCode, ErrorCategory, CreateViewRequest, UpdateViewRequest, ListViewsRequest, ListViewsResponse, ViewResponse } from '@objectstack/spec/api';
|
|
3
|
+
import { Logger } from '@objectstack/core';
|
|
2
4
|
export interface ClientConfig {
|
|
3
5
|
baseUrl: string;
|
|
4
6
|
token?: string;
|
|
@@ -6,6 +8,14 @@ export interface ClientConfig {
|
|
|
6
8
|
* Custom fetch implementation (e.g. node-fetch or for Next.js caching)
|
|
7
9
|
*/
|
|
8
10
|
fetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
11
|
+
/**
|
|
12
|
+
* Logger instance for debugging
|
|
13
|
+
*/
|
|
14
|
+
logger?: Logger;
|
|
15
|
+
/**
|
|
16
|
+
* Enable debug logging
|
|
17
|
+
*/
|
|
18
|
+
debug?: boolean;
|
|
9
19
|
}
|
|
10
20
|
export interface DiscoveryResult {
|
|
11
21
|
routes: {
|
|
@@ -30,11 +40,20 @@ export interface PaginatedResult<T = any> {
|
|
|
30
40
|
value: T[];
|
|
31
41
|
count: number;
|
|
32
42
|
}
|
|
43
|
+
export interface StandardError {
|
|
44
|
+
code: StandardErrorCode;
|
|
45
|
+
message: string;
|
|
46
|
+
category: ErrorCategory;
|
|
47
|
+
httpStatus: number;
|
|
48
|
+
retryable: boolean;
|
|
49
|
+
details?: Record<string, any>;
|
|
50
|
+
}
|
|
33
51
|
export declare class ObjectStackClient {
|
|
34
52
|
private baseUrl;
|
|
35
53
|
private token?;
|
|
36
54
|
private fetchImpl;
|
|
37
55
|
private routes?;
|
|
56
|
+
private logger;
|
|
38
57
|
constructor(config: ClientConfig);
|
|
39
58
|
/**
|
|
40
59
|
* Initialize the client by discovering server capabilities and routes.
|
|
@@ -45,6 +64,11 @@ export declare class ObjectStackClient {
|
|
|
45
64
|
*/
|
|
46
65
|
meta: {
|
|
47
66
|
getObject: (name: string) => Promise<any>;
|
|
67
|
+
/**
|
|
68
|
+
* Get object metadata with cache support
|
|
69
|
+
* Supports ETag-based conditional requests for efficient caching
|
|
70
|
+
*/
|
|
71
|
+
getCached: (name: string, cacheOptions?: MetadataCacheRequest) => Promise<MetadataCacheResponse>;
|
|
48
72
|
getView: (object: string, type?: "list" | "form") => Promise<any>;
|
|
49
73
|
};
|
|
50
74
|
/**
|
|
@@ -61,13 +85,62 @@ export declare class ObjectStackClient {
|
|
|
61
85
|
create: <T = any>(object: string, data: Partial<T>) => Promise<T>;
|
|
62
86
|
createMany: <T = any>(object: string, data: Partial<T>[]) => Promise<T[]>;
|
|
63
87
|
update: <T = any>(object: string, id: string, data: Partial<T>) => Promise<T>;
|
|
64
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Batch update multiple records
|
|
90
|
+
* Uses the new BatchUpdateRequest schema with full control over options
|
|
91
|
+
*/
|
|
92
|
+
batch: (object: string, request: BatchUpdateRequest) => Promise<BatchUpdateResponse>;
|
|
93
|
+
/**
|
|
94
|
+
* Update multiple records (simplified batch update)
|
|
95
|
+
* Convenience method for batch updates without full BatchUpdateRequest
|
|
96
|
+
*/
|
|
97
|
+
updateMany: <T = any>(object: string, records: Array<{
|
|
98
|
+
id: string;
|
|
99
|
+
data: Partial<T>;
|
|
100
|
+
}>, options?: BatchOptions) => Promise<BatchUpdateResponse>;
|
|
65
101
|
delete: (object: string, id: string) => Promise<{
|
|
66
102
|
success: boolean;
|
|
67
103
|
}>;
|
|
68
|
-
|
|
69
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Delete multiple records by IDs
|
|
106
|
+
*/
|
|
107
|
+
deleteMany: (object: string, ids: string[], options?: BatchOptions) => Promise<BatchUpdateResponse>;
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* View Storage Operations
|
|
111
|
+
* Save, load, and manage UI view configurations
|
|
112
|
+
*/
|
|
113
|
+
views: {
|
|
114
|
+
/**
|
|
115
|
+
* Create a new saved view
|
|
116
|
+
*/
|
|
117
|
+
create: (request: CreateViewRequest) => Promise<ViewResponse>;
|
|
118
|
+
/**
|
|
119
|
+
* Get a saved view by ID
|
|
120
|
+
*/
|
|
121
|
+
get: (id: string) => Promise<ViewResponse>;
|
|
122
|
+
/**
|
|
123
|
+
* List saved views with optional filters
|
|
124
|
+
*/
|
|
125
|
+
list: (request?: ListViewsRequest) => Promise<ListViewsResponse>;
|
|
126
|
+
/**
|
|
127
|
+
* Update an existing view
|
|
128
|
+
*/
|
|
129
|
+
update: (request: UpdateViewRequest) => Promise<ViewResponse>;
|
|
130
|
+
/**
|
|
131
|
+
* Delete a saved view
|
|
132
|
+
*/
|
|
133
|
+
delete: (id: string) => Promise<{
|
|
134
|
+
success: boolean;
|
|
70
135
|
}>;
|
|
136
|
+
/**
|
|
137
|
+
* Share a view with users/teams
|
|
138
|
+
*/
|
|
139
|
+
share: (id: string, userIds: string[]) => Promise<ViewResponse>;
|
|
140
|
+
/**
|
|
141
|
+
* Set a view as default for an object
|
|
142
|
+
*/
|
|
143
|
+
setDefault: (id: string, object: string) => Promise<ViewResponse>;
|
|
71
144
|
};
|
|
72
145
|
/**
|
|
73
146
|
* Private Helpers
|
|
@@ -76,3 +149,5 @@ export declare class ObjectStackClient {
|
|
|
76
149
|
private fetch;
|
|
77
150
|
private getRoute;
|
|
78
151
|
}
|
|
152
|
+
export { QueryBuilder, FilterBuilder, createQuery, createFilter } from './query-builder';
|
|
153
|
+
export type { BatchUpdateRequest, BatchUpdateResponse, UpdateManyRequest, DeleteManyRequest, BatchOptions, BatchRecord, BatchOperationResult, MetadataCacheRequest, MetadataCacheResponse, StandardErrorCode, ErrorCategory, CreateViewRequest, UpdateViewRequest, ListViewsRequest, SavedView, ViewResponse, ListViewsResponse, ViewType, ViewVisibility, ViewColumn, ViewLayout } from '@objectstack/spec/api';
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createLogger } from '@objectstack/core';
|
|
1
2
|
export class ObjectStackClient {
|
|
2
3
|
constructor(config) {
|
|
3
4
|
/**
|
|
@@ -9,6 +10,45 @@ export class ObjectStackClient {
|
|
|
9
10
|
const res = await this.fetch(`${this.baseUrl}${route}/object/${name}`);
|
|
10
11
|
return res.json();
|
|
11
12
|
},
|
|
13
|
+
/**
|
|
14
|
+
* Get object metadata with cache support
|
|
15
|
+
* Supports ETag-based conditional requests for efficient caching
|
|
16
|
+
*/
|
|
17
|
+
getCached: async (name, cacheOptions) => {
|
|
18
|
+
const route = this.getRoute('metadata');
|
|
19
|
+
const headers = {};
|
|
20
|
+
if (cacheOptions?.ifNoneMatch) {
|
|
21
|
+
headers['If-None-Match'] = cacheOptions.ifNoneMatch;
|
|
22
|
+
}
|
|
23
|
+
if (cacheOptions?.ifModifiedSince) {
|
|
24
|
+
headers['If-Modified-Since'] = cacheOptions.ifModifiedSince;
|
|
25
|
+
}
|
|
26
|
+
const res = await this.fetch(`${this.baseUrl}${route}/object/${name}`, {
|
|
27
|
+
headers
|
|
28
|
+
});
|
|
29
|
+
// Check for 304 Not Modified
|
|
30
|
+
if (res.status === 304) {
|
|
31
|
+
return {
|
|
32
|
+
notModified: true,
|
|
33
|
+
etag: cacheOptions?.ifNoneMatch ? {
|
|
34
|
+
value: cacheOptions.ifNoneMatch.replace(/^W\/|"/g, ''),
|
|
35
|
+
weak: cacheOptions.ifNoneMatch.startsWith('W/')
|
|
36
|
+
} : undefined
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const data = await res.json();
|
|
40
|
+
const etag = res.headers.get('ETag');
|
|
41
|
+
const lastModified = res.headers.get('Last-Modified');
|
|
42
|
+
return {
|
|
43
|
+
data,
|
|
44
|
+
etag: etag ? {
|
|
45
|
+
value: etag.replace(/^W\/|"/g, ''),
|
|
46
|
+
weak: etag.startsWith('W/')
|
|
47
|
+
} : undefined,
|
|
48
|
+
lastModified: lastModified || undefined,
|
|
49
|
+
notModified: false
|
|
50
|
+
};
|
|
51
|
+
},
|
|
12
52
|
getView: async (object, type = 'list') => {
|
|
13
53
|
const route = this.getRoute('ui');
|
|
14
54
|
const res = await this.fetch(`${this.baseUrl}${route}/view/${object}?type=${type}`);
|
|
@@ -96,7 +136,7 @@ export class ObjectStackClient {
|
|
|
96
136
|
},
|
|
97
137
|
createMany: async (object, data) => {
|
|
98
138
|
const route = this.getRoute('data');
|
|
99
|
-
const res = await this.fetch(`${this.baseUrl}${route}/${object}/
|
|
139
|
+
const res = await this.fetch(`${this.baseUrl}${route}/${object}/createMany`, {
|
|
100
140
|
method: 'POST',
|
|
101
141
|
body: JSON.stringify(data)
|
|
102
142
|
});
|
|
@@ -110,13 +150,33 @@ export class ObjectStackClient {
|
|
|
110
150
|
});
|
|
111
151
|
return res.json();
|
|
112
152
|
},
|
|
113
|
-
|
|
153
|
+
/**
|
|
154
|
+
* Batch update multiple records
|
|
155
|
+
* Uses the new BatchUpdateRequest schema with full control over options
|
|
156
|
+
*/
|
|
157
|
+
batch: async (object, request) => {
|
|
114
158
|
const route = this.getRoute('data');
|
|
115
159
|
const res = await this.fetch(`${this.baseUrl}${route}/${object}/batch`, {
|
|
116
|
-
method: '
|
|
117
|
-
body: JSON.stringify(
|
|
160
|
+
method: 'POST',
|
|
161
|
+
body: JSON.stringify(request)
|
|
118
162
|
});
|
|
119
|
-
return res.json();
|
|
163
|
+
return res.json();
|
|
164
|
+
},
|
|
165
|
+
/**
|
|
166
|
+
* Update multiple records (simplified batch update)
|
|
167
|
+
* Convenience method for batch updates without full BatchUpdateRequest
|
|
168
|
+
*/
|
|
169
|
+
updateMany: async (object, records, options) => {
|
|
170
|
+
const route = this.getRoute('data');
|
|
171
|
+
const request = {
|
|
172
|
+
records,
|
|
173
|
+
options
|
|
174
|
+
};
|
|
175
|
+
const res = await this.fetch(`${this.baseUrl}${route}/${object}/updateMany`, {
|
|
176
|
+
method: 'POST',
|
|
177
|
+
body: JSON.stringify(request)
|
|
178
|
+
});
|
|
179
|
+
return res.json();
|
|
120
180
|
},
|
|
121
181
|
delete: async (object, id) => {
|
|
122
182
|
const route = this.getRoute('data');
|
|
@@ -125,11 +185,113 @@ export class ObjectStackClient {
|
|
|
125
185
|
});
|
|
126
186
|
return res.json();
|
|
127
187
|
},
|
|
128
|
-
|
|
188
|
+
/**
|
|
189
|
+
* Delete multiple records by IDs
|
|
190
|
+
*/
|
|
191
|
+
deleteMany: async (object, ids, options) => {
|
|
129
192
|
const route = this.getRoute('data');
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
193
|
+
const request = {
|
|
194
|
+
ids,
|
|
195
|
+
options
|
|
196
|
+
};
|
|
197
|
+
const res = await this.fetch(`${this.baseUrl}${route}/${object}/deleteMany`, {
|
|
198
|
+
method: 'POST',
|
|
199
|
+
body: JSON.stringify(request)
|
|
200
|
+
});
|
|
201
|
+
return res.json();
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
/**
|
|
205
|
+
* View Storage Operations
|
|
206
|
+
* Save, load, and manage UI view configurations
|
|
207
|
+
*/
|
|
208
|
+
this.views = {
|
|
209
|
+
/**
|
|
210
|
+
* Create a new saved view
|
|
211
|
+
*/
|
|
212
|
+
create: async (request) => {
|
|
213
|
+
const route = this.getRoute('ui');
|
|
214
|
+
const res = await this.fetch(`${this.baseUrl}${route}/views`, {
|
|
215
|
+
method: 'POST',
|
|
216
|
+
body: JSON.stringify(request)
|
|
217
|
+
});
|
|
218
|
+
return res.json();
|
|
219
|
+
},
|
|
220
|
+
/**
|
|
221
|
+
* Get a saved view by ID
|
|
222
|
+
*/
|
|
223
|
+
get: async (id) => {
|
|
224
|
+
const route = this.getRoute('ui');
|
|
225
|
+
const res = await this.fetch(`${this.baseUrl}${route}/views/${id}`);
|
|
226
|
+
return res.json();
|
|
227
|
+
},
|
|
228
|
+
/**
|
|
229
|
+
* List saved views with optional filters
|
|
230
|
+
*/
|
|
231
|
+
list: async (request) => {
|
|
232
|
+
const route = this.getRoute('ui');
|
|
233
|
+
const queryParams = new URLSearchParams();
|
|
234
|
+
if (request?.object)
|
|
235
|
+
queryParams.set('object', request.object);
|
|
236
|
+
if (request?.type)
|
|
237
|
+
queryParams.set('type', request.type);
|
|
238
|
+
if (request?.visibility)
|
|
239
|
+
queryParams.set('visibility', request.visibility);
|
|
240
|
+
if (request?.createdBy)
|
|
241
|
+
queryParams.set('createdBy', request.createdBy);
|
|
242
|
+
if (request?.isDefault !== undefined)
|
|
243
|
+
queryParams.set('isDefault', String(request.isDefault));
|
|
244
|
+
if (request?.limit)
|
|
245
|
+
queryParams.set('limit', String(request.limit));
|
|
246
|
+
if (request?.offset)
|
|
247
|
+
queryParams.set('offset', String(request.offset));
|
|
248
|
+
const url = queryParams.toString()
|
|
249
|
+
? `${this.baseUrl}${route}/views?${queryParams.toString()}`
|
|
250
|
+
: `${this.baseUrl}${route}/views`;
|
|
251
|
+
const res = await this.fetch(url);
|
|
252
|
+
return res.json();
|
|
253
|
+
},
|
|
254
|
+
/**
|
|
255
|
+
* Update an existing view
|
|
256
|
+
*/
|
|
257
|
+
update: async (request) => {
|
|
258
|
+
const route = this.getRoute('ui');
|
|
259
|
+
const { id, ...updateData } = request;
|
|
260
|
+
const res = await this.fetch(`${this.baseUrl}${route}/views/${id}`, {
|
|
261
|
+
method: 'PATCH',
|
|
262
|
+
body: JSON.stringify(updateData)
|
|
263
|
+
});
|
|
264
|
+
return res.json();
|
|
265
|
+
},
|
|
266
|
+
/**
|
|
267
|
+
* Delete a saved view
|
|
268
|
+
*/
|
|
269
|
+
delete: async (id) => {
|
|
270
|
+
const route = this.getRoute('ui');
|
|
271
|
+
const res = await this.fetch(`${this.baseUrl}${route}/views/${id}`, {
|
|
272
|
+
method: 'DELETE'
|
|
273
|
+
});
|
|
274
|
+
return res.json();
|
|
275
|
+
},
|
|
276
|
+
/**
|
|
277
|
+
* Share a view with users/teams
|
|
278
|
+
*/
|
|
279
|
+
share: async (id, userIds) => {
|
|
280
|
+
const route = this.getRoute('ui');
|
|
281
|
+
const res = await this.fetch(`${this.baseUrl}${route}/views/${id}/share`, {
|
|
282
|
+
method: 'POST',
|
|
283
|
+
body: JSON.stringify({ sharedWith: userIds })
|
|
284
|
+
});
|
|
285
|
+
return res.json();
|
|
286
|
+
},
|
|
287
|
+
/**
|
|
288
|
+
* Set a view as default for an object
|
|
289
|
+
*/
|
|
290
|
+
setDefault: async (id, object) => {
|
|
291
|
+
const route = this.getRoute('ui');
|
|
292
|
+
const res = await this.fetch(`${this.baseUrl}${route}/views/${id}/set-default`, {
|
|
293
|
+
method: 'POST',
|
|
294
|
+
body: JSON.stringify({ object })
|
|
133
295
|
});
|
|
134
296
|
return res.json();
|
|
135
297
|
}
|
|
@@ -137,21 +299,32 @@ export class ObjectStackClient {
|
|
|
137
299
|
this.baseUrl = config.baseUrl.replace(/\/$/, ''); // Remove trailing slash
|
|
138
300
|
this.token = config.token;
|
|
139
301
|
this.fetchImpl = config.fetch || globalThis.fetch.bind(globalThis);
|
|
302
|
+
// Initialize logger
|
|
303
|
+
this.logger = config.logger || createLogger({
|
|
304
|
+
level: config.debug ? 'debug' : 'info',
|
|
305
|
+
format: 'pretty'
|
|
306
|
+
});
|
|
307
|
+
this.logger.debug('ObjectStack client created', { baseUrl: this.baseUrl });
|
|
140
308
|
}
|
|
141
309
|
/**
|
|
142
310
|
* Initialize the client by discovering server capabilities and routes.
|
|
143
311
|
*/
|
|
144
312
|
async connect() {
|
|
313
|
+
this.logger.debug('Connecting to ObjectStack server', { baseUrl: this.baseUrl });
|
|
145
314
|
try {
|
|
146
315
|
// Connect to the discovery endpoint
|
|
147
316
|
// During boot, we might not know routes, so we check convention /api/v1 first
|
|
148
317
|
const res = await this.fetch(`${this.baseUrl}/api/v1`);
|
|
149
318
|
const data = await res.json();
|
|
150
319
|
this.routes = data.routes;
|
|
320
|
+
this.logger.info('Connected to ObjectStack server', {
|
|
321
|
+
routes: Object.keys(data.routes || {}),
|
|
322
|
+
capabilities: data.capabilities
|
|
323
|
+
});
|
|
151
324
|
return data;
|
|
152
325
|
}
|
|
153
326
|
catch (e) {
|
|
154
|
-
|
|
327
|
+
this.logger.error('Failed to connect to ObjectStack server', e, { baseUrl: this.baseUrl });
|
|
155
328
|
throw e;
|
|
156
329
|
}
|
|
157
330
|
}
|
|
@@ -165,6 +338,11 @@ export class ObjectStackClient {
|
|
|
165
338
|
return Array.isArray(filter);
|
|
166
339
|
}
|
|
167
340
|
async fetch(url, options = {}) {
|
|
341
|
+
this.logger.debug('HTTP request', {
|
|
342
|
+
method: options.method || 'GET',
|
|
343
|
+
url,
|
|
344
|
+
hasBody: !!options.body
|
|
345
|
+
});
|
|
168
346
|
const headers = {
|
|
169
347
|
'Content-Type': 'application/json',
|
|
170
348
|
...(options.headers || {}),
|
|
@@ -173,6 +351,12 @@ export class ObjectStackClient {
|
|
|
173
351
|
headers['Authorization'] = `Bearer ${this.token}`;
|
|
174
352
|
}
|
|
175
353
|
const res = await this.fetchImpl(url, { ...options, headers });
|
|
354
|
+
this.logger.debug('HTTP response', {
|
|
355
|
+
method: options.method || 'GET',
|
|
356
|
+
url,
|
|
357
|
+
status: res.status,
|
|
358
|
+
ok: res.ok
|
|
359
|
+
});
|
|
176
360
|
if (!res.ok) {
|
|
177
361
|
let errorBody;
|
|
178
362
|
try {
|
|
@@ -181,16 +365,37 @@ export class ObjectStackClient {
|
|
|
181
365
|
catch {
|
|
182
366
|
errorBody = { message: res.statusText };
|
|
183
367
|
}
|
|
184
|
-
|
|
368
|
+
this.logger.error('HTTP request failed', undefined, {
|
|
369
|
+
method: options.method || 'GET',
|
|
370
|
+
url,
|
|
371
|
+
status: res.status,
|
|
372
|
+
error: errorBody
|
|
373
|
+
});
|
|
374
|
+
// Create a standardized error if the response includes error details
|
|
375
|
+
const errorMessage = errorBody?.message || errorBody?.error?.message || res.statusText;
|
|
376
|
+
const errorCode = errorBody?.code || errorBody?.error?.code;
|
|
377
|
+
const error = new Error(`[ObjectStack] ${errorCode ? `${errorCode}: ` : ''}${errorMessage}`);
|
|
378
|
+
// Attach error details for programmatic access
|
|
379
|
+
error.code = errorCode;
|
|
380
|
+
error.category = errorBody?.category;
|
|
381
|
+
error.httpStatus = res.status;
|
|
382
|
+
error.retryable = errorBody?.retryable;
|
|
383
|
+
error.details = errorBody?.details || errorBody;
|
|
384
|
+
throw error;
|
|
185
385
|
}
|
|
186
386
|
return res;
|
|
187
387
|
}
|
|
188
388
|
getRoute(key) {
|
|
189
389
|
if (!this.routes) {
|
|
190
390
|
// Fallback for strictness, but we allow bootstrapping
|
|
191
|
-
|
|
391
|
+
this.logger.warn('Accessing route before connect()', {
|
|
392
|
+
route: key,
|
|
393
|
+
fallback: `/api/v1/${key}`
|
|
394
|
+
});
|
|
192
395
|
return `/api/v1/${key}`;
|
|
193
396
|
}
|
|
194
397
|
return this.routes[key] || `/api/v1/${key}`;
|
|
195
398
|
}
|
|
196
399
|
}
|
|
400
|
+
// Re-export type-safe query builder
|
|
401
|
+
export { QueryBuilder, FilterBuilder, createQuery, createFilter } from './query-builder';
|