@objectstack/client 1.0.4 → 1.0.6

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.js CHANGED
@@ -1,508 +1,713 @@
1
- import { createLogger } from '@objectstack/core';
2
- export class ObjectStackClient {
3
- constructor(config) {
4
- /**
5
- * Metadata Operations
6
- */
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
- */
31
- getObject: async (name) => {
32
- const route = this.getRoute('metadata');
33
- const res = await this.fetch(`${this.baseUrl}${route}/object/${name}`);
34
- return res.json();
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
- },
46
- /**
47
- * Save a metadata item
48
- * @param type - Metadata type (e.g., 'object', 'plugin')
49
- * @param name - Item name
50
- * @param item - The metadata content to save
51
- */
52
- saveItem: async (type, name, item) => {
53
- const route = this.getRoute('metadata');
54
- const res = await this.fetch(`${this.baseUrl}${route}/${type}/${name}`, {
55
- method: 'PUT',
56
- body: JSON.stringify(item)
57
- });
58
- return res.json();
59
- },
60
- /**
61
- * Get object metadata with cache support
62
- * Supports ETag-based conditional requests for efficient caching
63
- */
64
- getCached: async (name, cacheOptions) => {
65
- const route = this.getRoute('metadata');
66
- const headers = {};
67
- if (cacheOptions?.ifNoneMatch) {
68
- headers['If-None-Match'] = cacheOptions.ifNoneMatch;
69
- }
70
- if (cacheOptions?.ifModifiedSince) {
71
- headers['If-Modified-Since'] = cacheOptions.ifModifiedSince;
72
- }
73
- const res = await this.fetch(`${this.baseUrl}${route}/object/${name}`, {
74
- headers
75
- });
76
- // Check for 304 Not Modified
77
- if (res.status === 304) {
78
- return {
79
- notModified: true,
80
- etag: cacheOptions?.ifNoneMatch ? {
81
- value: cacheOptions.ifNoneMatch.replace(/^W\/|"/g, ''),
82
- weak: cacheOptions.ifNoneMatch.startsWith('W/')
83
- } : undefined
84
- };
85
- }
86
- const data = await res.json();
87
- const etag = res.headers.get('ETag');
88
- const lastModified = res.headers.get('Last-Modified');
89
- return {
90
- data,
91
- etag: etag ? {
92
- value: etag.replace(/^W\/|"/g, ''),
93
- weak: etag.startsWith('W/')
94
- } : undefined,
95
- lastModified: lastModified || undefined,
96
- notModified: false
97
- };
98
- },
99
- getView: async (object, type = 'list') => {
100
- const route = this.getRoute('ui');
101
- const res = await this.fetch(`${this.baseUrl}${route}/view/${object}?type=${type}`);
102
- return res.json();
103
- }
104
- };
105
- /**
106
- * Analytics Services
107
- */
108
- this.analytics = {
109
- query: async (payload) => {
110
- const route = this.getRoute('analytics');
111
- const res = await this.fetch(`${this.baseUrl}${route}/query`, {
112
- method: 'POST',
113
- body: JSON.stringify(payload)
114
- });
115
- return res.json();
116
- },
117
- meta: async (cube) => {
118
- const route = this.getRoute('analytics');
119
- const res = await this.fetch(`${this.baseUrl}${route}/meta/${cube}`);
120
- return res.json();
121
- },
122
- explain: async (payload) => {
123
- const route = this.getRoute('analytics');
124
- const res = await this.fetch(`${this.baseUrl}${route}/explain`, {
125
- method: 'POST',
126
- body: JSON.stringify(payload)
127
- });
128
- return res.json();
129
- }
130
- };
131
- /**
132
- * Hub Management Services
133
- */
134
- this.hub = {
135
- spaces: {
136
- list: async () => {
137
- const route = this.getRoute('hub');
138
- const res = await this.fetch(`${this.baseUrl}${route}/spaces`);
139
- return res.json();
140
- },
141
- create: async (payload) => {
142
- const route = this.getRoute('hub');
143
- const res = await this.fetch(`${this.baseUrl}${route}/spaces`, {
144
- method: 'POST',
145
- body: JSON.stringify(payload)
146
- });
147
- return res.json();
148
- }
149
- },
150
- plugins: {
151
- install: async (pkg, version) => {
152
- const route = this.getRoute('hub');
153
- const res = await this.fetch(`${this.baseUrl}${route}/plugins/install`, {
154
- method: 'POST',
155
- body: JSON.stringify({ pkg, version })
156
- });
157
- return res.json();
158
- }
159
- }
160
- };
161
- /**
162
- * Authentication Services
163
- */
164
- this.auth = {
165
- login: async (request) => {
166
- const route = this.getRoute('auth');
167
- const res = await this.fetch(`${this.baseUrl}${route}/login`, {
168
- method: 'POST',
169
- body: JSON.stringify(request)
170
- });
171
- const data = await res.json();
172
- // Auto-set token if present in response
173
- if (data.data?.token) {
174
- this.token = data.data.token;
175
- }
176
- return data;
177
- },
178
- logout: async () => {
179
- const route = this.getRoute('auth');
180
- await this.fetch(`${this.baseUrl}${route}/logout`, { method: 'POST' });
181
- this.token = undefined;
182
- },
183
- me: async () => {
184
- const route = this.getRoute('auth');
185
- const res = await this.fetch(`${this.baseUrl}${route}/me`);
186
- return res.json();
187
- }
188
- };
189
- /**
190
- * Storage Services
191
- */
192
- this.storage = {
193
- upload: async (file, scope = 'user') => {
194
- // 1. Get Presigned URL
195
- const presignedReq = {
196
- filename: file.name,
197
- mimeType: file.type,
198
- size: file.size,
199
- scope
200
- };
201
- const route = this.getRoute('storage');
202
- const presignedRes = await this.fetch(`${this.baseUrl}${route}/upload/presigned`, {
203
- method: 'POST',
204
- body: JSON.stringify(presignedReq)
205
- });
206
- const { data: presigned } = await presignedRes.json();
207
- // 2. Upload to Cloud directly (Bypass API Middleware to avoid Auth headers if using S3)
208
- // Use fetchImpl directly
209
- const uploadRes = await this.fetchImpl(presigned.uploadUrl, {
210
- method: presigned.method,
211
- headers: presigned.headers,
212
- body: file
213
- });
214
- if (!uploadRes.ok) {
215
- throw new Error(`Storage Upload Failed: ${uploadRes.statusText}`);
216
- }
217
- // 3. Complete Upload
218
- const completeReq = {
219
- fileId: presigned.fileId
220
- };
221
- const completeRes = await this.fetch(`${this.baseUrl}${route}/upload/complete`, {
222
- method: 'POST',
223
- body: JSON.stringify(completeReq)
224
- });
225
- return completeRes.json();
226
- },
227
- getDownloadUrl: async (fileId) => {
228
- const route = this.getRoute('storage');
229
- const res = await this.fetch(`${this.baseUrl}${route}/files/${fileId}/url`);
230
- const data = await res.json();
231
- return data.url;
232
- }
233
- };
234
- /**
235
- * Automation Services
236
- */
237
- this.automation = {
238
- trigger: async (triggerName, payload) => {
239
- const route = this.getRoute('automation');
240
- const res = await this.fetch(`${this.baseUrl}${route}/trigger/${triggerName}`, {
241
- method: 'POST',
242
- body: JSON.stringify(payload)
243
- });
244
- return res.json();
245
- }
246
- };
247
- /**
248
- * Data Operations
249
- */
250
- this.data = {
251
- /**
252
- * Advanced Query using ObjectStack Query Protocol
253
- * Supports both simplified options and full AST
254
- */
255
- query: async (object, query) => {
256
- const route = this.getRoute('data');
257
- // POST for complex query to avoid URL length limits and allow clean JSON AST
258
- // Convention: POST /api/v1/data/:object/query
259
- const res = await this.fetch(`${this.baseUrl}${route}/${object}/query`, {
260
- method: 'POST',
261
- body: JSON.stringify(query)
262
- });
263
- return res.json();
264
- },
265
- find: async (object, options = {}) => {
266
- const route = this.getRoute('data');
267
- const queryParams = new URLSearchParams();
268
- // 1. Handle Pagination
269
- if (options.top)
270
- queryParams.set('top', options.top.toString());
271
- if (options.skip)
272
- queryParams.set('skip', options.skip.toString());
273
- // 2. Handle Sort
274
- if (options.sort) {
275
- // Check if it's AST
276
- if (Array.isArray(options.sort) && typeof options.sort[0] === 'object') {
277
- queryParams.set('sort', JSON.stringify(options.sort));
278
- }
279
- else {
280
- const sortVal = Array.isArray(options.sort) ? options.sort.join(',') : options.sort;
281
- queryParams.set('sort', sortVal);
282
- }
283
- }
284
- // 3. Handle Select
285
- if (options.select) {
286
- queryParams.set('select', options.select.join(','));
287
- }
288
- // 4. Handle Filters (Simple vs AST)
289
- if (options.filters) {
290
- // If looks like AST (not plain object map)
291
- // TODO: robust check. safely assuming map for simplified find, and recommending .query() for AST
292
- if (this.isFilterAST(options.filters)) {
293
- queryParams.set('filters', JSON.stringify(options.filters));
294
- }
295
- else {
296
- Object.entries(options.filters).forEach(([k, v]) => {
297
- if (v !== undefined && v !== null) {
298
- queryParams.append(k, String(v));
299
- }
300
- });
301
- }
302
- }
303
- // 5. Handle Aggregations & GroupBy (Pass through as JSON if present)
304
- if (options.aggregations) {
305
- queryParams.set('aggregations', JSON.stringify(options.aggregations));
306
- }
307
- if (options.groupBy) {
308
- queryParams.set('groupBy', options.groupBy.join(','));
309
- }
310
- const res = await this.fetch(`${this.baseUrl}${route}/${object}?${queryParams.toString()}`);
311
- return res.json();
312
- },
313
- get: async (object, id) => {
314
- const route = this.getRoute('data');
315
- const res = await this.fetch(`${this.baseUrl}${route}/${object}/${id}`);
316
- return res.json();
317
- },
318
- create: async (object, data) => {
319
- const route = this.getRoute('data');
320
- const res = await this.fetch(`${this.baseUrl}${route}/${object}`, {
321
- method: 'POST',
322
- body: JSON.stringify(data)
323
- });
324
- return res.json();
325
- },
326
- createMany: async (object, data) => {
327
- const route = this.getRoute('data');
328
- const res = await this.fetch(`${this.baseUrl}${route}/${object}/createMany`, {
329
- method: 'POST',
330
- body: JSON.stringify(data)
331
- });
332
- return res.json();
333
- },
334
- update: async (object, id, data) => {
335
- const route = this.getRoute('data');
336
- const res = await this.fetch(`${this.baseUrl}${route}/${object}/${id}`, {
337
- method: 'PATCH',
338
- body: JSON.stringify(data)
339
- });
340
- return res.json();
341
- },
342
- /**
343
- * Batch update multiple records
344
- * Uses the new BatchUpdateRequest schema with full control over options
345
- */
346
- batch: async (object, request) => {
347
- const route = this.getRoute('data');
348
- const res = await this.fetch(`${this.baseUrl}${route}/${object}/batch`, {
349
- method: 'POST',
350
- body: JSON.stringify(request)
351
- });
352
- return res.json();
353
- },
354
- /**
355
- * Update multiple records (simplified batch update)
356
- * Convenience method for batch updates without full BatchUpdateRequest
357
- */
358
- updateMany: async (object, records, options) => {
359
- const route = this.getRoute('data');
360
- const request = {
361
- records,
362
- options
363
- };
364
- const res = await this.fetch(`${this.baseUrl}${route}/${object}/updateMany`, {
365
- method: 'POST',
366
- body: JSON.stringify(request)
367
- });
368
- return res.json();
369
- },
370
- delete: async (object, id) => {
371
- const route = this.getRoute('data');
372
- const res = await this.fetch(`${this.baseUrl}${route}/${object}/${id}`, {
373
- method: 'DELETE'
374
- });
375
- return res.json();
376
- },
377
- /**
378
- * Delete multiple records by IDs
379
- */
380
- deleteMany: async (object, ids, options) => {
381
- const route = this.getRoute('data');
382
- const request = {
383
- ids,
384
- options
385
- };
386
- const res = await this.fetch(`${this.baseUrl}${route}/${object}/deleteMany`, {
387
- method: 'POST',
388
- body: JSON.stringify(request)
389
- });
390
- return res.json();
391
- }
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ FilterBuilder: () => FilterBuilder,
24
+ ObjectStackClient: () => ObjectStackClient,
25
+ QueryBuilder: () => QueryBuilder,
26
+ createFilter: () => createFilter,
27
+ createQuery: () => createQuery
28
+ });
29
+ module.exports = __toCommonJS(index_exports);
30
+ var import_core = require("@objectstack/core");
31
+
32
+ // src/query-builder.ts
33
+ var FilterBuilder = class {
34
+ constructor() {
35
+ this.conditions = [];
36
+ }
37
+ /**
38
+ * Equality filter: field = value
39
+ */
40
+ equals(field, value) {
41
+ this.conditions.push([field, "=", value]);
42
+ return this;
43
+ }
44
+ /**
45
+ * Not equals filter: field != value
46
+ */
47
+ notEquals(field, value) {
48
+ this.conditions.push([field, "!=", value]);
49
+ return this;
50
+ }
51
+ /**
52
+ * Greater than filter: field > value
53
+ */
54
+ greaterThan(field, value) {
55
+ this.conditions.push([field, ">", value]);
56
+ return this;
57
+ }
58
+ /**
59
+ * Greater than or equal filter: field >= value
60
+ */
61
+ greaterThanOrEqual(field, value) {
62
+ this.conditions.push([field, ">=", value]);
63
+ return this;
64
+ }
65
+ /**
66
+ * Less than filter: field < value
67
+ */
68
+ lessThan(field, value) {
69
+ this.conditions.push([field, "<", value]);
70
+ return this;
71
+ }
72
+ /**
73
+ * Less than or equal filter: field <= value
74
+ */
75
+ lessThanOrEqual(field, value) {
76
+ this.conditions.push([field, "<=", value]);
77
+ return this;
78
+ }
79
+ /**
80
+ * IN filter: field IN (value1, value2, ...)
81
+ */
82
+ in(field, values) {
83
+ this.conditions.push([field, "in", values]);
84
+ return this;
85
+ }
86
+ /**
87
+ * NOT IN filter: field NOT IN (value1, value2, ...)
88
+ */
89
+ notIn(field, values) {
90
+ this.conditions.push([field, "not_in", values]);
91
+ return this;
92
+ }
93
+ /**
94
+ * LIKE filter: field LIKE pattern
95
+ */
96
+ like(field, pattern) {
97
+ this.conditions.push([field, "like", pattern]);
98
+ return this;
99
+ }
100
+ /**
101
+ * IS NULL filter: field IS NULL
102
+ */
103
+ isNull(field) {
104
+ this.conditions.push([field, "is_null", null]);
105
+ return this;
106
+ }
107
+ /**
108
+ * IS NOT NULL filter: field IS NOT NULL
109
+ */
110
+ isNotNull(field) {
111
+ this.conditions.push([field, "is_not_null", null]);
112
+ return this;
113
+ }
114
+ /**
115
+ * Build the filter condition
116
+ */
117
+ build() {
118
+ if (this.conditions.length === 0) {
119
+ throw new Error("Filter builder has no conditions");
120
+ }
121
+ if (this.conditions.length === 1) {
122
+ return this.conditions[0];
123
+ }
124
+ return ["and", ...this.conditions];
125
+ }
126
+ /**
127
+ * Get raw conditions array
128
+ */
129
+ getConditions() {
130
+ return this.conditions;
131
+ }
132
+ };
133
+ var QueryBuilder = class {
134
+ constructor(object) {
135
+ this.query = {};
136
+ this._object = object;
137
+ this.query.object = object;
138
+ }
139
+ /**
140
+ * Select specific fields
141
+ */
142
+ select(...fields) {
143
+ this.query.fields = fields;
144
+ return this;
145
+ }
146
+ /**
147
+ * Add filters using a builder function
148
+ */
149
+ where(builderFn) {
150
+ const builder = new FilterBuilder();
151
+ builderFn(builder);
152
+ const conditions = builder.getConditions();
153
+ if (conditions.length === 1) {
154
+ this.query.where = conditions[0];
155
+ } else if (conditions.length > 1) {
156
+ this.query.where = ["and", ...conditions];
157
+ }
158
+ return this;
159
+ }
160
+ /**
161
+ * Add raw filter condition
162
+ */
163
+ filter(condition) {
164
+ this.query.where = condition;
165
+ return this;
166
+ }
167
+ /**
168
+ * Sort by fields
169
+ */
170
+ orderBy(field, order = "asc") {
171
+ if (!this.query.orderBy) {
172
+ this.query.orderBy = [];
173
+ }
174
+ this.query.orderBy.push({
175
+ field,
176
+ order
177
+ });
178
+ return this;
179
+ }
180
+ /**
181
+ * Limit the number of results
182
+ */
183
+ limit(count) {
184
+ this.query.limit = count;
185
+ return this;
186
+ }
187
+ /**
188
+ * Skip records (for pagination)
189
+ */
190
+ skip(count) {
191
+ this.query.offset = count;
192
+ return this;
193
+ }
194
+ /**
195
+ * Paginate results
196
+ */
197
+ paginate(page, pageSize) {
198
+ this.query.limit = pageSize;
199
+ this.query.offset = (page - 1) * pageSize;
200
+ return this;
201
+ }
202
+ /**
203
+ * Group by fields
204
+ */
205
+ groupBy(...fields) {
206
+ this.query.groupBy = fields;
207
+ return this;
208
+ }
209
+ /**
210
+ * Build the final query AST
211
+ */
212
+ build() {
213
+ return {
214
+ object: this._object,
215
+ ...this.query
216
+ };
217
+ }
218
+ /**
219
+ * Get the current query state
220
+ */
221
+ getQuery() {
222
+ return { ...this.query };
223
+ }
224
+ };
225
+ function createQuery(object) {
226
+ return new QueryBuilder(object);
227
+ }
228
+ function createFilter() {
229
+ return new FilterBuilder();
230
+ }
231
+
232
+ // src/index.ts
233
+ var ObjectStackClient = class {
234
+ constructor(config) {
235
+ /**
236
+ * Metadata Operations
237
+ */
238
+ this.meta = {
239
+ /**
240
+ * Get all available metadata types
241
+ * Returns types like 'object', 'plugin', 'view', etc.
242
+ */
243
+ getTypes: async () => {
244
+ const route = this.getRoute("metadata");
245
+ const res = await this.fetch(`${this.baseUrl}${route}`);
246
+ return res.json();
247
+ },
248
+ /**
249
+ * Get all items of a specific metadata type
250
+ * @param type - Metadata type name (e.g., 'object', 'plugin')
251
+ */
252
+ getItems: async (type) => {
253
+ const route = this.getRoute("metadata");
254
+ const res = await this.fetch(`${this.baseUrl}${route}/${type}`);
255
+ return res.json();
256
+ },
257
+ /**
258
+ * Get a specific object definition by name
259
+ * @deprecated Use `getItem('object', name)` instead for consistency with spec protocol
260
+ * @param name - Object name (snake_case identifier)
261
+ */
262
+ getObject: async (name) => {
263
+ const route = this.getRoute("metadata");
264
+ const res = await this.fetch(`${this.baseUrl}${route}/object/${name}`);
265
+ return res.json();
266
+ },
267
+ /**
268
+ * Get a specific metadata item by type and name
269
+ * @param type - Metadata type (e.g., 'object', 'plugin')
270
+ * @param name - Item name (snake_case identifier)
271
+ */
272
+ getItem: async (type, name) => {
273
+ const route = this.getRoute("metadata");
274
+ const res = await this.fetch(`${this.baseUrl}${route}/${type}/${name}`);
275
+ return res.json();
276
+ },
277
+ /**
278
+ * Save a metadata item
279
+ * @param type - Metadata type (e.g., 'object', 'plugin')
280
+ * @param name - Item name
281
+ * @param item - The metadata content to save
282
+ */
283
+ saveItem: async (type, name, item) => {
284
+ const route = this.getRoute("metadata");
285
+ const res = await this.fetch(`${this.baseUrl}${route}/${type}/${name}`, {
286
+ method: "PUT",
287
+ body: JSON.stringify(item)
288
+ });
289
+ return res.json();
290
+ },
291
+ /**
292
+ * Get object metadata with cache support
293
+ * Supports ETag-based conditional requests for efficient caching
294
+ */
295
+ getCached: async (name, cacheOptions) => {
296
+ const route = this.getRoute("metadata");
297
+ const headers = {};
298
+ if (cacheOptions?.ifNoneMatch) {
299
+ headers["If-None-Match"] = cacheOptions.ifNoneMatch;
300
+ }
301
+ if (cacheOptions?.ifModifiedSince) {
302
+ headers["If-Modified-Since"] = cacheOptions.ifModifiedSince;
303
+ }
304
+ const res = await this.fetch(`${this.baseUrl}${route}/object/${name}`, {
305
+ headers
306
+ });
307
+ if (res.status === 304) {
308
+ return {
309
+ notModified: true,
310
+ etag: cacheOptions?.ifNoneMatch ? {
311
+ value: cacheOptions.ifNoneMatch.replace(/^W\/|"/g, ""),
312
+ weak: cacheOptions.ifNoneMatch.startsWith("W/")
313
+ } : void 0
314
+ };
315
+ }
316
+ const data = await res.json();
317
+ const etag = res.headers.get("ETag");
318
+ const lastModified = res.headers.get("Last-Modified");
319
+ return {
320
+ data,
321
+ etag: etag ? {
322
+ value: etag.replace(/^W\/|"/g, ""),
323
+ weak: etag.startsWith("W/")
324
+ } : void 0,
325
+ lastModified: lastModified || void 0,
326
+ notModified: false
392
327
  };
393
- this.baseUrl = config.baseUrl.replace(/\/$/, ''); // Remove trailing slash
394
- this.token = config.token;
395
- this.fetchImpl = config.fetch || globalThis.fetch.bind(globalThis);
396
- // Initialize logger
397
- this.logger = config.logger || createLogger({
398
- level: config.debug ? 'debug' : 'info',
399
- format: 'pretty'
328
+ },
329
+ getView: async (object, type = "list") => {
330
+ const route = this.getRoute("ui");
331
+ const res = await this.fetch(`${this.baseUrl}${route}/view/${object}?type=${type}`);
332
+ return res.json();
333
+ }
334
+ };
335
+ /**
336
+ * Analytics Services
337
+ */
338
+ this.analytics = {
339
+ query: async (payload) => {
340
+ const route = this.getRoute("analytics");
341
+ const res = await this.fetch(`${this.baseUrl}${route}/query`, {
342
+ method: "POST",
343
+ body: JSON.stringify(payload)
400
344
  });
401
- this.logger.debug('ObjectStack client created', { baseUrl: this.baseUrl });
402
- }
345
+ return res.json();
346
+ },
347
+ meta: async (cube) => {
348
+ const route = this.getRoute("analytics");
349
+ const res = await this.fetch(`${this.baseUrl}${route}/meta/${cube}`);
350
+ return res.json();
351
+ },
352
+ explain: async (payload) => {
353
+ const route = this.getRoute("analytics");
354
+ const res = await this.fetch(`${this.baseUrl}${route}/explain`, {
355
+ method: "POST",
356
+ body: JSON.stringify(payload)
357
+ });
358
+ return res.json();
359
+ }
360
+ };
403
361
  /**
404
- * Initialize the client by discovering server capabilities.
362
+ * Hub Management Services
405
363
  */
406
- async connect() {
407
- this.logger.debug('Connecting to ObjectStack server', { baseUrl: this.baseUrl });
408
- try {
409
- // Connect to the discovery endpoint at /api/v1
410
- const res = await this.fetch(`${this.baseUrl}/api/v1`);
411
- const data = await res.json();
412
- this.discoveryInfo = data;
413
- this.logger.info('Connected to ObjectStack server', {
414
- version: data.version,
415
- apiName: data.apiName,
416
- capabilities: data.capabilities
417
- });
418
- return data;
364
+ this.hub = {
365
+ spaces: {
366
+ list: async () => {
367
+ const route = this.getRoute("hub");
368
+ const res = await this.fetch(`${this.baseUrl}${route}/spaces`);
369
+ return res.json();
370
+ },
371
+ create: async (payload) => {
372
+ const route = this.getRoute("hub");
373
+ const res = await this.fetch(`${this.baseUrl}${route}/spaces`, {
374
+ method: "POST",
375
+ body: JSON.stringify(payload)
376
+ });
377
+ return res.json();
419
378
  }
420
- catch (e) {
421
- this.logger.error('Failed to connect to ObjectStack server', e, { baseUrl: this.baseUrl });
422
- throw e;
379
+ },
380
+ plugins: {
381
+ install: async (pkg, version) => {
382
+ const route = this.getRoute("hub");
383
+ const res = await this.fetch(`${this.baseUrl}${route}/plugins/install`, {
384
+ method: "POST",
385
+ body: JSON.stringify({ pkg, version })
386
+ });
387
+ return res.json();
423
388
  }
424
- }
389
+ }
390
+ };
425
391
  /**
426
- * Private Helpers
392
+ * Authentication Services
427
393
  */
428
- isFilterAST(filter) {
429
- // Basic check: if array, it's [field, op, val] or [logic, node, node]
430
- // If object but not basic KV map... harder to tell without schema
431
- // For now, assume if it passes Array.isArray it's an AST root
432
- return Array.isArray(filter);
433
- }
434
- async fetch(url, options = {}) {
435
- this.logger.debug('HTTP request', {
436
- method: options.method || 'GET',
437
- url,
438
- hasBody: !!options.body
394
+ this.auth = {
395
+ login: async (request) => {
396
+ const route = this.getRoute("auth");
397
+ const res = await this.fetch(`${this.baseUrl}${route}/login`, {
398
+ method: "POST",
399
+ body: JSON.stringify(request)
439
400
  });
440
- const headers = {
441
- 'Content-Type': 'application/json',
442
- ...(options.headers || {}),
443
- };
444
- if (this.token) {
445
- headers['Authorization'] = `Bearer ${this.token}`;
401
+ const data = await res.json();
402
+ if (data.data?.token) {
403
+ this.token = data.data.token;
446
404
  }
447
- const res = await this.fetchImpl(url, { ...options, headers });
448
- this.logger.debug('HTTP response', {
449
- method: options.method || 'GET',
450
- url,
451
- status: res.status,
452
- ok: res.ok
405
+ return data;
406
+ },
407
+ logout: async () => {
408
+ const route = this.getRoute("auth");
409
+ await this.fetch(`${this.baseUrl}${route}/logout`, { method: "POST" });
410
+ this.token = void 0;
411
+ },
412
+ me: async () => {
413
+ const route = this.getRoute("auth");
414
+ const res = await this.fetch(`${this.baseUrl}${route}/me`);
415
+ return res.json();
416
+ }
417
+ };
418
+ /**
419
+ * Storage Services
420
+ */
421
+ this.storage = {
422
+ upload: async (file, scope = "user") => {
423
+ const presignedReq = {
424
+ filename: file.name,
425
+ mimeType: file.type,
426
+ size: file.size,
427
+ scope
428
+ };
429
+ const route = this.getRoute("storage");
430
+ const presignedRes = await this.fetch(`${this.baseUrl}${route}/upload/presigned`, {
431
+ method: "POST",
432
+ body: JSON.stringify(presignedReq)
453
433
  });
454
- if (!res.ok) {
455
- let errorBody;
456
- try {
457
- errorBody = await res.json();
458
- }
459
- catch {
460
- errorBody = { message: res.statusText };
461
- }
462
- this.logger.error('HTTP request failed', undefined, {
463
- method: options.method || 'GET',
464
- url,
465
- status: res.status,
466
- error: errorBody
467
- });
468
- // Create a standardized error if the response includes error details
469
- const errorMessage = errorBody?.message || errorBody?.error?.message || res.statusText;
470
- const errorCode = errorBody?.code || errorBody?.error?.code;
471
- const error = new Error(`[ObjectStack] ${errorCode ? `${errorCode}: ` : ''}${errorMessage}`);
472
- // Attach error details for programmatic access
473
- error.code = errorCode;
474
- error.category = errorBody?.category;
475
- error.httpStatus = res.status;
476
- error.retryable = errorBody?.retryable;
477
- error.details = errorBody?.details || errorBody;
478
- throw error;
434
+ const { data: presigned } = await presignedRes.json();
435
+ const uploadRes = await this.fetchImpl(presigned.uploadUrl, {
436
+ method: presigned.method,
437
+ headers: presigned.headers,
438
+ body: file
439
+ });
440
+ if (!uploadRes.ok) {
441
+ throw new Error(`Storage Upload Failed: ${uploadRes.statusText}`);
479
442
  }
480
- return res;
481
- }
443
+ const completeReq = {
444
+ fileId: presigned.fileId
445
+ };
446
+ const completeRes = await this.fetch(`${this.baseUrl}${route}/upload/complete`, {
447
+ method: "POST",
448
+ body: JSON.stringify(completeReq)
449
+ });
450
+ return completeRes.json();
451
+ },
452
+ getDownloadUrl: async (fileId) => {
453
+ const route = this.getRoute("storage");
454
+ const res = await this.fetch(`${this.baseUrl}${route}/files/${fileId}/url`);
455
+ const data = await res.json();
456
+ return data.url;
457
+ }
458
+ };
482
459
  /**
483
- * Get the conventional route path for a given API endpoint type
484
- * ObjectStack uses standard conventions: /api/v1/data, /api/v1/metadata, /api/v1/ui
460
+ * Automation Services
485
461
  */
486
- getRoute(type) {
487
- // 1. Use discovered routes if available
488
- // Note: Spec uses 'endpoints', mapped dynamically
489
- if (this.discoveryInfo?.endpoints && this.discoveryInfo.endpoints[type]) {
490
- return this.discoveryInfo.endpoints[type];
462
+ this.automation = {
463
+ trigger: async (triggerName, payload) => {
464
+ const route = this.getRoute("automation");
465
+ const res = await this.fetch(`${this.baseUrl}${route}/trigger/${triggerName}`, {
466
+ method: "POST",
467
+ body: JSON.stringify(payload)
468
+ });
469
+ return res.json();
470
+ }
471
+ };
472
+ /**
473
+ * Data Operations
474
+ */
475
+ this.data = {
476
+ /**
477
+ * Advanced Query using ObjectStack Query Protocol
478
+ * Supports both simplified options and full AST
479
+ */
480
+ query: async (object, query) => {
481
+ const route = this.getRoute("data");
482
+ const res = await this.fetch(`${this.baseUrl}${route}/${object}/query`, {
483
+ method: "POST",
484
+ body: JSON.stringify(query)
485
+ });
486
+ return res.json();
487
+ },
488
+ find: async (object, options = {}) => {
489
+ const route = this.getRoute("data");
490
+ const queryParams = new URLSearchParams();
491
+ if (options.top) queryParams.set("top", options.top.toString());
492
+ if (options.skip) queryParams.set("skip", options.skip.toString());
493
+ if (options.sort) {
494
+ if (Array.isArray(options.sort) && typeof options.sort[0] === "object") {
495
+ queryParams.set("sort", JSON.stringify(options.sort));
496
+ } else {
497
+ const sortVal = Array.isArray(options.sort) ? options.sort.join(",") : options.sort;
498
+ queryParams.set("sort", sortVal);
499
+ }
500
+ }
501
+ if (options.select) {
502
+ queryParams.set("select", options.select.join(","));
503
+ }
504
+ if (options.filters) {
505
+ if (this.isFilterAST(options.filters)) {
506
+ queryParams.set("filters", JSON.stringify(options.filters));
507
+ } else {
508
+ Object.entries(options.filters).forEach(([k, v]) => {
509
+ if (v !== void 0 && v !== null) {
510
+ queryParams.append(k, String(v));
511
+ }
512
+ });
513
+ }
491
514
  }
492
- // 2. Fallback to conventions
493
- // Note: HttpDispatcher expects /metadata, not /meta
494
- const routeMap = {
495
- data: '/api/v1/data',
496
- metadata: '/api/v1/metadata',
497
- ui: '/api/v1/ui',
498
- auth: '/api/v1/auth',
499
- analytics: '/api/v1/analytics',
500
- hub: '/api/v1/hub',
501
- storage: '/api/v1/storage',
502
- automation: '/api/v1/automation'
515
+ if (options.aggregations) {
516
+ queryParams.set("aggregations", JSON.stringify(options.aggregations));
517
+ }
518
+ if (options.groupBy) {
519
+ queryParams.set("groupBy", options.groupBy.join(","));
520
+ }
521
+ const res = await this.fetch(`${this.baseUrl}${route}/${object}?${queryParams.toString()}`);
522
+ return res.json();
523
+ },
524
+ get: async (object, id) => {
525
+ const route = this.getRoute("data");
526
+ const res = await this.fetch(`${this.baseUrl}${route}/${object}/${id}`);
527
+ return res.json();
528
+ },
529
+ create: async (object, data) => {
530
+ const route = this.getRoute("data");
531
+ const res = await this.fetch(`${this.baseUrl}${route}/${object}`, {
532
+ method: "POST",
533
+ body: JSON.stringify(data)
534
+ });
535
+ return res.json();
536
+ },
537
+ createMany: async (object, data) => {
538
+ const route = this.getRoute("data");
539
+ const res = await this.fetch(`${this.baseUrl}${route}/${object}/createMany`, {
540
+ method: "POST",
541
+ body: JSON.stringify(data)
542
+ });
543
+ return res.json();
544
+ },
545
+ update: async (object, id, data) => {
546
+ const route = this.getRoute("data");
547
+ const res = await this.fetch(`${this.baseUrl}${route}/${object}/${id}`, {
548
+ method: "PATCH",
549
+ body: JSON.stringify(data)
550
+ });
551
+ return res.json();
552
+ },
553
+ /**
554
+ * Batch update multiple records
555
+ * Uses the new BatchUpdateRequest schema with full control over options
556
+ */
557
+ batch: async (object, request) => {
558
+ const route = this.getRoute("data");
559
+ const res = await this.fetch(`${this.baseUrl}${route}/${object}/batch`, {
560
+ method: "POST",
561
+ body: JSON.stringify(request)
562
+ });
563
+ return res.json();
564
+ },
565
+ /**
566
+ * Update multiple records (simplified batch update)
567
+ * Convenience method for batch updates without full BatchUpdateRequest
568
+ */
569
+ updateMany: async (object, records, options) => {
570
+ const route = this.getRoute("data");
571
+ const request = {
572
+ records,
573
+ options
574
+ };
575
+ const res = await this.fetch(`${this.baseUrl}${route}/${object}/updateMany`, {
576
+ method: "POST",
577
+ body: JSON.stringify(request)
578
+ });
579
+ return res.json();
580
+ },
581
+ delete: async (object, id) => {
582
+ const route = this.getRoute("data");
583
+ const res = await this.fetch(`${this.baseUrl}${route}/${object}/${id}`, {
584
+ method: "DELETE"
585
+ });
586
+ return res.json();
587
+ },
588
+ /**
589
+ * Delete multiple records by IDs
590
+ */
591
+ deleteMany: async (object, ids, options) => {
592
+ const route = this.getRoute("data");
593
+ const request = {
594
+ ids,
595
+ options
503
596
  };
504
- return routeMap[type] || `/api/v1/${type}`;
597
+ const res = await this.fetch(`${this.baseUrl}${route}/${object}/deleteMany`, {
598
+ method: "POST",
599
+ body: JSON.stringify(request)
600
+ });
601
+ return res.json();
602
+ }
603
+ };
604
+ this.baseUrl = config.baseUrl.replace(/\/$/, "");
605
+ this.token = config.token;
606
+ this.fetchImpl = config.fetch || globalThis.fetch.bind(globalThis);
607
+ this.logger = config.logger || (0, import_core.createLogger)({
608
+ level: config.debug ? "debug" : "info",
609
+ format: "pretty"
610
+ });
611
+ this.logger.debug("ObjectStack client created", { baseUrl: this.baseUrl });
612
+ }
613
+ /**
614
+ * Initialize the client by discovering server capabilities.
615
+ */
616
+ async connect() {
617
+ this.logger.debug("Connecting to ObjectStack server", { baseUrl: this.baseUrl });
618
+ try {
619
+ const res = await this.fetch(`${this.baseUrl}/api/v1`);
620
+ const data = await res.json();
621
+ this.discoveryInfo = data;
622
+ this.logger.info("Connected to ObjectStack server", {
623
+ version: data.version,
624
+ apiName: data.apiName,
625
+ capabilities: data.capabilities
626
+ });
627
+ return data;
628
+ } catch (e) {
629
+ this.logger.error("Failed to connect to ObjectStack server", e, { baseUrl: this.baseUrl });
630
+ throw e;
505
631
  }
506
- }
507
- // Re-export type-safe query builder
508
- export { QueryBuilder, FilterBuilder, createQuery, createFilter } from './query-builder';
632
+ }
633
+ /**
634
+ * Private Helpers
635
+ */
636
+ isFilterAST(filter) {
637
+ return Array.isArray(filter);
638
+ }
639
+ async fetch(url, options = {}) {
640
+ this.logger.debug("HTTP request", {
641
+ method: options.method || "GET",
642
+ url,
643
+ hasBody: !!options.body
644
+ });
645
+ const headers = {
646
+ "Content-Type": "application/json",
647
+ ...options.headers || {}
648
+ };
649
+ if (this.token) {
650
+ headers["Authorization"] = `Bearer ${this.token}`;
651
+ }
652
+ const res = await this.fetchImpl(url, { ...options, headers });
653
+ this.logger.debug("HTTP response", {
654
+ method: options.method || "GET",
655
+ url,
656
+ status: res.status,
657
+ ok: res.ok
658
+ });
659
+ if (!res.ok) {
660
+ let errorBody;
661
+ try {
662
+ errorBody = await res.json();
663
+ } catch {
664
+ errorBody = { message: res.statusText };
665
+ }
666
+ this.logger.error("HTTP request failed", void 0, {
667
+ method: options.method || "GET",
668
+ url,
669
+ status: res.status,
670
+ error: errorBody
671
+ });
672
+ const errorMessage = errorBody?.message || errorBody?.error?.message || res.statusText;
673
+ const errorCode = errorBody?.code || errorBody?.error?.code;
674
+ const error = new Error(`[ObjectStack] ${errorCode ? `${errorCode}: ` : ""}${errorMessage}`);
675
+ error.code = errorCode;
676
+ error.category = errorBody?.category;
677
+ error.httpStatus = res.status;
678
+ error.retryable = errorBody?.retryable;
679
+ error.details = errorBody?.details || errorBody;
680
+ throw error;
681
+ }
682
+ return res;
683
+ }
684
+ /**
685
+ * Get the conventional route path for a given API endpoint type
686
+ * ObjectStack uses standard conventions: /api/v1/data, /api/v1/metadata, /api/v1/ui
687
+ */
688
+ getRoute(type) {
689
+ if (this.discoveryInfo?.endpoints && this.discoveryInfo.endpoints[type]) {
690
+ return this.discoveryInfo.endpoints[type];
691
+ }
692
+ const routeMap = {
693
+ data: "/api/v1/data",
694
+ metadata: "/api/v1/metadata",
695
+ ui: "/api/v1/ui",
696
+ auth: "/api/v1/auth",
697
+ analytics: "/api/v1/analytics",
698
+ hub: "/api/v1/hub",
699
+ storage: "/api/v1/storage",
700
+ automation: "/api/v1/automation"
701
+ };
702
+ return routeMap[type] || `/api/v1/${type}`;
703
+ }
704
+ };
705
+ // Annotate the CommonJS export names for ESM import in node:
706
+ 0 && (module.exports = {
707
+ FilterBuilder,
708
+ ObjectStackClient,
709
+ QueryBuilder,
710
+ createFilter,
711
+ createQuery
712
+ });
713
+ //# sourceMappingURL=index.js.map