@centrali-io/centrali-sdk 3.0.1 → 3.0.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/README.md +1 -1
- package/dist/index.js +65 -12
- package/index.ts +120 -38
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -704,7 +704,7 @@ try {
|
|
|
704
704
|
- [Complete SDK Guide](https://docs.centrali.io/guides/centrali-sdk)
|
|
705
705
|
- [API Reference](https://docs.centrali.io/api-reference/overview)
|
|
706
706
|
- [Quick Start Tutorial](https://docs.centrali.io/getting-started/02-quickstart)
|
|
707
|
-
- [Compute Functions Guide](https://docs.centrali.io/guides/
|
|
707
|
+
- [Compute Functions Guide](https://docs.centrali.io/guides/FUNCTION_CODE_GUIDE)
|
|
708
708
|
|
|
709
709
|
## Examples
|
|
710
710
|
|
package/dist/index.js
CHANGED
|
@@ -1558,6 +1558,8 @@ class CentraliSDK {
|
|
|
1558
1558
|
this._anomalyInsights = null;
|
|
1559
1559
|
this._validation = null;
|
|
1560
1560
|
this._orchestrations = null;
|
|
1561
|
+
this.isRefreshingToken = false;
|
|
1562
|
+
this.tokenRefreshPromise = null;
|
|
1561
1563
|
this.options = options;
|
|
1562
1564
|
this.token = options.token || null;
|
|
1563
1565
|
const apiUrl = getApiUrl(options.baseUrl);
|
|
@@ -1572,6 +1574,44 @@ class CentraliSDK {
|
|
|
1572
1574
|
}
|
|
1573
1575
|
return config;
|
|
1574
1576
|
}), (error) => Promise.reject(error));
|
|
1577
|
+
// Response interceptor for automatic token refresh on 401/403
|
|
1578
|
+
this.axios.interceptors.response.use((response) => response, (error) => __awaiter(this, void 0, void 0, function* () {
|
|
1579
|
+
var _a, _b;
|
|
1580
|
+
const originalRequest = error.config;
|
|
1581
|
+
// Only attempt refresh for 401/403 errors when using client credentials
|
|
1582
|
+
const isAuthError = ((_a = error.response) === null || _a === void 0 ? void 0 : _a.status) === 401 || ((_b = error.response) === null || _b === void 0 ? void 0 : _b.status) === 403;
|
|
1583
|
+
const hasClientCredentials = this.options.clientId && this.options.clientSecret;
|
|
1584
|
+
const hasNotRetried = !originalRequest._hasRetried;
|
|
1585
|
+
if (isAuthError && hasClientCredentials && hasNotRetried) {
|
|
1586
|
+
// Mark request as retried to prevent infinite loops
|
|
1587
|
+
originalRequest._hasRetried = true;
|
|
1588
|
+
try {
|
|
1589
|
+
// If already refreshing, wait for the existing refresh to complete
|
|
1590
|
+
if (this.isRefreshingToken && this.tokenRefreshPromise) {
|
|
1591
|
+
yield this.tokenRefreshPromise;
|
|
1592
|
+
}
|
|
1593
|
+
else {
|
|
1594
|
+
// Start a new token refresh
|
|
1595
|
+
this.isRefreshingToken = true;
|
|
1596
|
+
this.tokenRefreshPromise = fetchClientToken(this.options.clientId, this.options.clientSecret, this.options.baseUrl);
|
|
1597
|
+
this.token = yield this.tokenRefreshPromise;
|
|
1598
|
+
this.isRefreshingToken = false;
|
|
1599
|
+
this.tokenRefreshPromise = null;
|
|
1600
|
+
}
|
|
1601
|
+
// Retry the original request with the new token
|
|
1602
|
+
originalRequest.headers.Authorization = `Bearer ${this.token}`;
|
|
1603
|
+
return this.axios(originalRequest);
|
|
1604
|
+
}
|
|
1605
|
+
catch (refreshError) {
|
|
1606
|
+
// Token refresh failed, clear state and reject
|
|
1607
|
+
this.isRefreshingToken = false;
|
|
1608
|
+
this.tokenRefreshPromise = null;
|
|
1609
|
+
this.token = null;
|
|
1610
|
+
return Promise.reject(refreshError);
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
return Promise.reject(error);
|
|
1614
|
+
}));
|
|
1575
1615
|
}
|
|
1576
1616
|
/**
|
|
1577
1617
|
* Realtime namespace for subscribing to SSE events.
|
|
@@ -1843,34 +1883,47 @@ class CentraliSDK {
|
|
|
1843
1883
|
/**
|
|
1844
1884
|
* Query records with filters, pagination, sorting, and reference expansion.
|
|
1845
1885
|
*
|
|
1886
|
+
* IMPORTANT: Filters are passed at the TOP LEVEL, not nested under 'filter'.
|
|
1887
|
+
* Use 'data.' prefix for custom fields and bracket notation for operators.
|
|
1888
|
+
*
|
|
1846
1889
|
* @param recordSlug - The structure's record slug
|
|
1847
|
-
* @param queryParams - Query parameters
|
|
1890
|
+
* @param queryParams - Query parameters (filters at top level, plus sort, pagination, expand)
|
|
1848
1891
|
*
|
|
1849
1892
|
* @example
|
|
1850
1893
|
* // Simple equality filter
|
|
1851
1894
|
* const activeProducts = await centrali.queryRecords('Product', {
|
|
1852
|
-
*
|
|
1895
|
+
* 'data.status': 'active',
|
|
1853
1896
|
* sort: '-createdAt',
|
|
1854
|
-
*
|
|
1897
|
+
* page: 1,
|
|
1898
|
+
* pageSize: 10
|
|
1855
1899
|
* });
|
|
1856
1900
|
*
|
|
1857
|
-
* // Filter with operators
|
|
1901
|
+
* // Filter with operators (bracket notation)
|
|
1858
1902
|
* const products = await centrali.queryRecords('Product', {
|
|
1859
|
-
*
|
|
1903
|
+
* 'data.inStock': true,
|
|
1904
|
+
* 'data.price[lte]': 100,
|
|
1860
1905
|
* sort: '-createdAt',
|
|
1861
|
-
*
|
|
1906
|
+
* pageSize: 10
|
|
1862
1907
|
* });
|
|
1863
1908
|
*
|
|
1864
|
-
* // Multiple values with 'in' operator
|
|
1909
|
+
* // Multiple values with 'in' operator (comma-separated string)
|
|
1865
1910
|
* const orders = await centrali.queryRecords('Order', {
|
|
1866
|
-
*
|
|
1911
|
+
* 'data.status[in]': 'pending,processing',
|
|
1867
1912
|
* expand: 'customer,items'
|
|
1868
1913
|
* });
|
|
1869
1914
|
* // Access expanded data: orders.data[0].data._expanded.customer
|
|
1870
1915
|
*
|
|
1871
1916
|
* // Range filters
|
|
1872
1917
|
* const customers = await centrali.queryRecords('Customer', {
|
|
1873
|
-
*
|
|
1918
|
+
* 'data.age[gte]': 18,
|
|
1919
|
+
* 'data.age[lte]': 65,
|
|
1920
|
+
* 'data.verified': true
|
|
1921
|
+
* });
|
|
1922
|
+
*
|
|
1923
|
+
* // Filter with 'ne' (not equal)
|
|
1924
|
+
* const availableItems = await centrali.queryRecords('Product', {
|
|
1925
|
+
* 'data.status[ne]': 'discontinued',
|
|
1926
|
+
* pageSize: 100
|
|
1874
1927
|
* });
|
|
1875
1928
|
*/
|
|
1876
1929
|
queryRecords(recordSlug, queryParams) {
|
|
@@ -2178,11 +2231,11 @@ exports.CentraliSDK = CentraliSDK;
|
|
|
2178
2231
|
*
|
|
2179
2232
|
* // Or set a user token:
|
|
2180
2233
|
* client.setToken('<JWT_TOKEN>');
|
|
2181
|
-
* await client.queryRecords('Product', {
|
|
2234
|
+
* await client.queryRecords('Product', { pageSize: 10 });
|
|
2182
2235
|
*
|
|
2183
2236
|
* // Subscribe to realtime events (Initial Sync Pattern):
|
|
2184
|
-
* // 1. First fetch initial data
|
|
2185
|
-
* const orders = await client.queryRecords('Order', {
|
|
2237
|
+
* // 1. First fetch initial data (filters at TOP LEVEL, not nested)
|
|
2238
|
+
* const orders = await client.queryRecords('Order', { 'data.status': 'pending' });
|
|
2186
2239
|
* setOrders(orders.data);
|
|
2187
2240
|
*
|
|
2188
2241
|
* // 2. Then subscribe to realtime updates
|
package/index.ts
CHANGED
|
@@ -568,13 +568,17 @@ export interface GetRecordOptions extends ExpandOptions {}
|
|
|
568
568
|
|
|
569
569
|
/**
|
|
570
570
|
* Filter operators for querying records.
|
|
571
|
-
*
|
|
571
|
+
* Pass filters at the TOP LEVEL of query params (not nested under 'filter').
|
|
572
|
+
* Use bracket notation for operators: 'data.field[operator]': value
|
|
572
573
|
*
|
|
573
574
|
* @example
|
|
574
|
-
* //
|
|
575
|
-
* {
|
|
576
|
-
*
|
|
577
|
-
*
|
|
575
|
+
* // Simple equality - just use the field name
|
|
576
|
+
* { 'data.status': 'active' }
|
|
577
|
+
*
|
|
578
|
+
* // With operators - use bracket notation
|
|
579
|
+
* { 'data.age[gte]': 18, 'data.age[lte]': 65 }
|
|
580
|
+
* { 'data.status[in]': 'pending,processing' } // comma-separated for 'in'
|
|
581
|
+
* { 'data.email[contains]': '@gmail.com' }
|
|
578
582
|
*/
|
|
579
583
|
export interface FilterOperators {
|
|
580
584
|
/** Equal to (default if just a value is provided) */
|
|
@@ -612,34 +616,49 @@ export type FilterValue = string | number | boolean | null | FilterOperators;
|
|
|
612
616
|
|
|
613
617
|
/**
|
|
614
618
|
* Options for querying records.
|
|
619
|
+
*
|
|
620
|
+
* IMPORTANT: Filters are passed at the TOP LEVEL, not nested under a 'filter' key.
|
|
621
|
+
* Use 'data.' prefix for custom fields, and bracket notation for operators.
|
|
622
|
+
*
|
|
623
|
+
* @example
|
|
624
|
+
* // Simple equality filter
|
|
625
|
+
* { 'data.status': 'active', pageSize: 10 }
|
|
626
|
+
*
|
|
627
|
+
* // Filter with operators (bracket notation)
|
|
628
|
+
* { 'data.price[lte]': 100, 'data.status[ne]': 'discontinued' }
|
|
629
|
+
*
|
|
630
|
+
* // Multiple values with 'in' operator (comma-separated string)
|
|
631
|
+
* { 'data.status[in]': 'pending,processing', pageSize: 50 }
|
|
632
|
+
*
|
|
633
|
+
* // Range filters
|
|
634
|
+
* { 'data.age[gte]': 18, 'data.age[lte]': 65 }
|
|
635
|
+
*
|
|
636
|
+
* // Filter on top-level record fields (no 'data.' prefix)
|
|
637
|
+
* { 'createdAt[gte]': '2024-01-01', sort: '-createdAt' }
|
|
615
638
|
*/
|
|
616
639
|
export interface QueryRecordOptions extends ExpandOptions {
|
|
617
|
-
/**
|
|
618
|
-
* Filter object for querying records.
|
|
619
|
-
* Keys are field names, values are either direct values (for equality) or operator objects.
|
|
620
|
-
*
|
|
621
|
-
* @example
|
|
622
|
-
* // Simple equality filter
|
|
623
|
-
* { filter: { status: 'active' } }
|
|
624
|
-
*
|
|
625
|
-
* // Filter with operators
|
|
626
|
-
* { filter: { age: { gte: 18 }, status: { in: ['active', 'pending'] } } }
|
|
627
|
-
*
|
|
628
|
-
* // Multiple conditions (AND)
|
|
629
|
-
* { filter: { status: 'active', inStock: true, price: { lte: 100 } } }
|
|
630
|
-
*/
|
|
631
|
-
filter?: Record<string, FilterValue>;
|
|
632
640
|
/** Sort field with optional direction prefix (e.g., '-createdAt' for descending) */
|
|
633
641
|
sort?: string;
|
|
634
|
-
/**
|
|
635
|
-
limit?: number;
|
|
636
|
-
/** Number of records to skip (for pagination) */
|
|
637
|
-
skip?: number;
|
|
638
|
-
/** Page number (alternative to skip) */
|
|
642
|
+
/** Page number (1-indexed, default: 1) */
|
|
639
643
|
page?: number;
|
|
644
|
+
/** Number of records per page (default: 50, max: 500) */
|
|
645
|
+
pageSize?: number;
|
|
646
|
+
/** Alias for pageSize */
|
|
647
|
+
limit?: number;
|
|
640
648
|
/** Include archived (soft-deleted) records */
|
|
641
649
|
includeArchived?: boolean;
|
|
642
|
-
/**
|
|
650
|
+
/** Include total count in response */
|
|
651
|
+
includeTotal?: boolean;
|
|
652
|
+
/** Search query string */
|
|
653
|
+
search?: string;
|
|
654
|
+
/** Field(s) to search in (comma-separated or single field) */
|
|
655
|
+
searchField?: string;
|
|
656
|
+
/**
|
|
657
|
+
* Filter fields - pass at TOP LEVEL with 'data.' prefix for custom fields.
|
|
658
|
+
* Use bracket notation for operators: 'data.field[operator]': value
|
|
659
|
+
*
|
|
660
|
+
* Supported operators: eq, ne, gt, gte, lt, lte, in, nin, contains, startswith, endswith, hasAny, hasAll
|
|
661
|
+
*/
|
|
643
662
|
[key: string]: any;
|
|
644
663
|
}
|
|
645
664
|
|
|
@@ -3256,6 +3275,8 @@ export class CentraliSDK {
|
|
|
3256
3275
|
private _anomalyInsights: AnomalyInsightsManager | null = null;
|
|
3257
3276
|
private _validation: ValidationManager | null = null;
|
|
3258
3277
|
private _orchestrations: OrchestrationsManager | null = null;
|
|
3278
|
+
private isRefreshingToken: boolean = false;
|
|
3279
|
+
private tokenRefreshPromise: Promise<string> | null = null;
|
|
3259
3280
|
|
|
3260
3281
|
constructor(options: CentraliSDKOptions) {
|
|
3261
3282
|
this.options = options;
|
|
@@ -3286,6 +3307,54 @@ export class CentraliSDK {
|
|
|
3286
3307
|
},
|
|
3287
3308
|
(error) => Promise.reject(error)
|
|
3288
3309
|
);
|
|
3310
|
+
|
|
3311
|
+
// Response interceptor for automatic token refresh on 401/403
|
|
3312
|
+
this.axios.interceptors.response.use(
|
|
3313
|
+
(response) => response,
|
|
3314
|
+
async (error) => {
|
|
3315
|
+
const originalRequest = error.config;
|
|
3316
|
+
|
|
3317
|
+
// Only attempt refresh for 401/403 errors when using client credentials
|
|
3318
|
+
const isAuthError = error.response?.status === 401 || error.response?.status === 403;
|
|
3319
|
+
const hasClientCredentials = this.options.clientId && this.options.clientSecret;
|
|
3320
|
+
const hasNotRetried = !originalRequest._hasRetried;
|
|
3321
|
+
|
|
3322
|
+
if (isAuthError && hasClientCredentials && hasNotRetried) {
|
|
3323
|
+
// Mark request as retried to prevent infinite loops
|
|
3324
|
+
originalRequest._hasRetried = true;
|
|
3325
|
+
|
|
3326
|
+
try {
|
|
3327
|
+
// If already refreshing, wait for the existing refresh to complete
|
|
3328
|
+
if (this.isRefreshingToken && this.tokenRefreshPromise) {
|
|
3329
|
+
await this.tokenRefreshPromise;
|
|
3330
|
+
} else {
|
|
3331
|
+
// Start a new token refresh
|
|
3332
|
+
this.isRefreshingToken = true;
|
|
3333
|
+
this.tokenRefreshPromise = fetchClientToken(
|
|
3334
|
+
this.options.clientId!,
|
|
3335
|
+
this.options.clientSecret!,
|
|
3336
|
+
this.options.baseUrl
|
|
3337
|
+
);
|
|
3338
|
+
this.token = await this.tokenRefreshPromise;
|
|
3339
|
+
this.isRefreshingToken = false;
|
|
3340
|
+
this.tokenRefreshPromise = null;
|
|
3341
|
+
}
|
|
3342
|
+
|
|
3343
|
+
// Retry the original request with the new token
|
|
3344
|
+
originalRequest.headers.Authorization = `Bearer ${this.token}`;
|
|
3345
|
+
return this.axios(originalRequest);
|
|
3346
|
+
} catch (refreshError) {
|
|
3347
|
+
// Token refresh failed, clear state and reject
|
|
3348
|
+
this.isRefreshingToken = false;
|
|
3349
|
+
this.tokenRefreshPromise = null;
|
|
3350
|
+
this.token = null;
|
|
3351
|
+
return Promise.reject(refreshError);
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
|
|
3355
|
+
return Promise.reject(error);
|
|
3356
|
+
}
|
|
3357
|
+
);
|
|
3289
3358
|
}
|
|
3290
3359
|
|
|
3291
3360
|
/**
|
|
@@ -3614,34 +3683,47 @@ export class CentraliSDK {
|
|
|
3614
3683
|
/**
|
|
3615
3684
|
* Query records with filters, pagination, sorting, and reference expansion.
|
|
3616
3685
|
*
|
|
3686
|
+
* IMPORTANT: Filters are passed at the TOP LEVEL, not nested under 'filter'.
|
|
3687
|
+
* Use 'data.' prefix for custom fields and bracket notation for operators.
|
|
3688
|
+
*
|
|
3617
3689
|
* @param recordSlug - The structure's record slug
|
|
3618
|
-
* @param queryParams - Query parameters
|
|
3690
|
+
* @param queryParams - Query parameters (filters at top level, plus sort, pagination, expand)
|
|
3619
3691
|
*
|
|
3620
3692
|
* @example
|
|
3621
3693
|
* // Simple equality filter
|
|
3622
3694
|
* const activeProducts = await centrali.queryRecords('Product', {
|
|
3623
|
-
*
|
|
3695
|
+
* 'data.status': 'active',
|
|
3624
3696
|
* sort: '-createdAt',
|
|
3625
|
-
*
|
|
3697
|
+
* page: 1,
|
|
3698
|
+
* pageSize: 10
|
|
3626
3699
|
* });
|
|
3627
3700
|
*
|
|
3628
|
-
* // Filter with operators
|
|
3701
|
+
* // Filter with operators (bracket notation)
|
|
3629
3702
|
* const products = await centrali.queryRecords('Product', {
|
|
3630
|
-
*
|
|
3703
|
+
* 'data.inStock': true,
|
|
3704
|
+
* 'data.price[lte]': 100,
|
|
3631
3705
|
* sort: '-createdAt',
|
|
3632
|
-
*
|
|
3706
|
+
* pageSize: 10
|
|
3633
3707
|
* });
|
|
3634
3708
|
*
|
|
3635
|
-
* // Multiple values with 'in' operator
|
|
3709
|
+
* // Multiple values with 'in' operator (comma-separated string)
|
|
3636
3710
|
* const orders = await centrali.queryRecords('Order', {
|
|
3637
|
-
*
|
|
3711
|
+
* 'data.status[in]': 'pending,processing',
|
|
3638
3712
|
* expand: 'customer,items'
|
|
3639
3713
|
* });
|
|
3640
3714
|
* // Access expanded data: orders.data[0].data._expanded.customer
|
|
3641
3715
|
*
|
|
3642
3716
|
* // Range filters
|
|
3643
3717
|
* const customers = await centrali.queryRecords('Customer', {
|
|
3644
|
-
*
|
|
3718
|
+
* 'data.age[gte]': 18,
|
|
3719
|
+
* 'data.age[lte]': 65,
|
|
3720
|
+
* 'data.verified': true
|
|
3721
|
+
* });
|
|
3722
|
+
*
|
|
3723
|
+
* // Filter with 'ne' (not equal)
|
|
3724
|
+
* const availableItems = await centrali.queryRecords('Product', {
|
|
3725
|
+
* 'data.status[ne]': 'discontinued',
|
|
3726
|
+
* pageSize: 100
|
|
3645
3727
|
* });
|
|
3646
3728
|
*/
|
|
3647
3729
|
public queryRecords<T = any>(
|
|
@@ -4016,11 +4098,11 @@ export class CentraliSDK {
|
|
|
4016
4098
|
*
|
|
4017
4099
|
* // Or set a user token:
|
|
4018
4100
|
* client.setToken('<JWT_TOKEN>');
|
|
4019
|
-
* await client.queryRecords('Product', {
|
|
4101
|
+
* await client.queryRecords('Product', { pageSize: 10 });
|
|
4020
4102
|
*
|
|
4021
4103
|
* // Subscribe to realtime events (Initial Sync Pattern):
|
|
4022
|
-
* // 1. First fetch initial data
|
|
4023
|
-
* const orders = await client.queryRecords('Order', {
|
|
4104
|
+
* // 1. First fetch initial data (filters at TOP LEVEL, not nested)
|
|
4105
|
+
* const orders = await client.queryRecords('Order', { 'data.status': 'pending' });
|
|
4024
4106
|
* setOrders(orders.data);
|
|
4025
4107
|
*
|
|
4026
4108
|
* // 2. Then subscribe to realtime updates
|