@objectstack/client 1.0.4 → 1.0.5

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