@centrali-io/centrali-sdk 3.0.0 → 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 +74 -9
- package/index.ts +168 -17
- 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,23 +1883,48 @@ 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
|
|
1894
|
+
* const activeProducts = await centrali.queryRecords('Product', {
|
|
1895
|
+
* 'data.status': 'active',
|
|
1896
|
+
* sort: '-createdAt',
|
|
1897
|
+
* page: 1,
|
|
1898
|
+
* pageSize: 10
|
|
1899
|
+
* });
|
|
1900
|
+
*
|
|
1901
|
+
* // Filter with operators (bracket notation)
|
|
1851
1902
|
* const products = await centrali.queryRecords('Product', {
|
|
1852
|
-
*
|
|
1903
|
+
* 'data.inStock': true,
|
|
1904
|
+
* 'data.price[lte]': 100,
|
|
1853
1905
|
* sort: '-createdAt',
|
|
1854
|
-
*
|
|
1906
|
+
* pageSize: 10
|
|
1855
1907
|
* });
|
|
1856
1908
|
*
|
|
1857
|
-
* //
|
|
1909
|
+
* // Multiple values with 'in' operator (comma-separated string)
|
|
1858
1910
|
* const orders = await centrali.queryRecords('Order', {
|
|
1859
|
-
*
|
|
1911
|
+
* 'data.status[in]': 'pending,processing',
|
|
1860
1912
|
* expand: 'customer,items'
|
|
1861
1913
|
* });
|
|
1862
1914
|
* // Access expanded data: orders.data[0].data._expanded.customer
|
|
1915
|
+
*
|
|
1916
|
+
* // Range filters
|
|
1917
|
+
* const customers = await centrali.queryRecords('Customer', {
|
|
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
|
|
1927
|
+
* });
|
|
1863
1928
|
*/
|
|
1864
1929
|
queryRecords(recordSlug, queryParams) {
|
|
1865
1930
|
const path = getRecordApiPath(this.options.workspaceId, recordSlug);
|
|
@@ -2166,11 +2231,11 @@ exports.CentraliSDK = CentraliSDK;
|
|
|
2166
2231
|
*
|
|
2167
2232
|
* // Or set a user token:
|
|
2168
2233
|
* client.setToken('<JWT_TOKEN>');
|
|
2169
|
-
* await client.queryRecords('Product', {
|
|
2234
|
+
* await client.queryRecords('Product', { pageSize: 10 });
|
|
2170
2235
|
*
|
|
2171
2236
|
* // Subscribe to realtime events (Initial Sync Pattern):
|
|
2172
|
-
* // 1. First fetch initial data
|
|
2173
|
-
* 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' });
|
|
2174
2239
|
* setOrders(orders.data);
|
|
2175
2240
|
*
|
|
2176
2241
|
* // 2. Then subscribe to realtime updates
|
package/index.ts
CHANGED
|
@@ -566,23 +566,99 @@ export interface ExpandOptions {
|
|
|
566
566
|
*/
|
|
567
567
|
export interface GetRecordOptions extends ExpandOptions {}
|
|
568
568
|
|
|
569
|
+
/**
|
|
570
|
+
* Filter operators for querying records.
|
|
571
|
+
* Pass filters at the TOP LEVEL of query params (not nested under 'filter').
|
|
572
|
+
* Use bracket notation for operators: 'data.field[operator]': value
|
|
573
|
+
*
|
|
574
|
+
* @example
|
|
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' }
|
|
582
|
+
*/
|
|
583
|
+
export interface FilterOperators {
|
|
584
|
+
/** Equal to (default if just a value is provided) */
|
|
585
|
+
eq?: string | number | boolean;
|
|
586
|
+
/** Not equal to */
|
|
587
|
+
ne?: string | number | boolean;
|
|
588
|
+
/** Greater than */
|
|
589
|
+
gt?: number | string;
|
|
590
|
+
/** Greater than or equal to */
|
|
591
|
+
gte?: number | string;
|
|
592
|
+
/** Less than */
|
|
593
|
+
lt?: number | string;
|
|
594
|
+
/** Less than or equal to */
|
|
595
|
+
lte?: number | string;
|
|
596
|
+
/** Value is in the provided array */
|
|
597
|
+
in?: (string | number)[];
|
|
598
|
+
/** Value is not in the provided array */
|
|
599
|
+
nin?: (string | number)[];
|
|
600
|
+
/** String contains substring (case-insensitive) */
|
|
601
|
+
contains?: string;
|
|
602
|
+
/** String starts with (case-insensitive) */
|
|
603
|
+
startswith?: string;
|
|
604
|
+
/** String ends with (case-insensitive) */
|
|
605
|
+
endswith?: string;
|
|
606
|
+
/** Array field contains any of the provided values */
|
|
607
|
+
hasAny?: (string | number)[];
|
|
608
|
+
/** Array field contains all of the provided values */
|
|
609
|
+
hasAll?: (string | number)[];
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Filter value can be a direct value or an object with operators.
|
|
614
|
+
*/
|
|
615
|
+
export type FilterValue = string | number | boolean | null | FilterOperators;
|
|
616
|
+
|
|
569
617
|
/**
|
|
570
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' }
|
|
571
638
|
*/
|
|
572
639
|
export interface QueryRecordOptions extends ExpandOptions {
|
|
573
|
-
/** CFL filter expression (e.g., 'status = "active" AND price > 100') */
|
|
574
|
-
filter?: string;
|
|
575
640
|
/** Sort field with optional direction prefix (e.g., '-createdAt' for descending) */
|
|
576
641
|
sort?: string;
|
|
577
|
-
/**
|
|
578
|
-
limit?: number;
|
|
579
|
-
/** Number of records to skip (for pagination) */
|
|
580
|
-
skip?: number;
|
|
581
|
-
/** Page number (alternative to skip) */
|
|
642
|
+
/** Page number (1-indexed, default: 1) */
|
|
582
643
|
page?: number;
|
|
644
|
+
/** Number of records per page (default: 50, max: 500) */
|
|
645
|
+
pageSize?: number;
|
|
646
|
+
/** Alias for pageSize */
|
|
647
|
+
limit?: number;
|
|
583
648
|
/** Include archived (soft-deleted) records */
|
|
584
649
|
includeArchived?: boolean;
|
|
585
|
-
/**
|
|
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
|
+
*/
|
|
586
662
|
[key: string]: any;
|
|
587
663
|
}
|
|
588
664
|
|
|
@@ -3199,6 +3275,8 @@ export class CentraliSDK {
|
|
|
3199
3275
|
private _anomalyInsights: AnomalyInsightsManager | null = null;
|
|
3200
3276
|
private _validation: ValidationManager | null = null;
|
|
3201
3277
|
private _orchestrations: OrchestrationsManager | null = null;
|
|
3278
|
+
private isRefreshingToken: boolean = false;
|
|
3279
|
+
private tokenRefreshPromise: Promise<string> | null = null;
|
|
3202
3280
|
|
|
3203
3281
|
constructor(options: CentraliSDKOptions) {
|
|
3204
3282
|
this.options = options;
|
|
@@ -3229,6 +3307,54 @@ export class CentraliSDK {
|
|
|
3229
3307
|
},
|
|
3230
3308
|
(error) => Promise.reject(error)
|
|
3231
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
|
+
);
|
|
3232
3358
|
}
|
|
3233
3359
|
|
|
3234
3360
|
/**
|
|
@@ -3557,23 +3683,48 @@ export class CentraliSDK {
|
|
|
3557
3683
|
/**
|
|
3558
3684
|
* Query records with filters, pagination, sorting, and reference expansion.
|
|
3559
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
|
+
*
|
|
3560
3689
|
* @param recordSlug - The structure's record slug
|
|
3561
|
-
* @param queryParams - Query parameters
|
|
3690
|
+
* @param queryParams - Query parameters (filters at top level, plus sort, pagination, expand)
|
|
3562
3691
|
*
|
|
3563
3692
|
* @example
|
|
3564
|
-
* //
|
|
3693
|
+
* // Simple equality filter
|
|
3694
|
+
* const activeProducts = await centrali.queryRecords('Product', {
|
|
3695
|
+
* 'data.status': 'active',
|
|
3696
|
+
* sort: '-createdAt',
|
|
3697
|
+
* page: 1,
|
|
3698
|
+
* pageSize: 10
|
|
3699
|
+
* });
|
|
3700
|
+
*
|
|
3701
|
+
* // Filter with operators (bracket notation)
|
|
3565
3702
|
* const products = await centrali.queryRecords('Product', {
|
|
3566
|
-
*
|
|
3703
|
+
* 'data.inStock': true,
|
|
3704
|
+
* 'data.price[lte]': 100,
|
|
3567
3705
|
* sort: '-createdAt',
|
|
3568
|
-
*
|
|
3706
|
+
* pageSize: 10
|
|
3569
3707
|
* });
|
|
3570
3708
|
*
|
|
3571
|
-
* //
|
|
3709
|
+
* // Multiple values with 'in' operator (comma-separated string)
|
|
3572
3710
|
* const orders = await centrali.queryRecords('Order', {
|
|
3573
|
-
*
|
|
3711
|
+
* 'data.status[in]': 'pending,processing',
|
|
3574
3712
|
* expand: 'customer,items'
|
|
3575
3713
|
* });
|
|
3576
3714
|
* // Access expanded data: orders.data[0].data._expanded.customer
|
|
3715
|
+
*
|
|
3716
|
+
* // Range filters
|
|
3717
|
+
* const customers = await centrali.queryRecords('Customer', {
|
|
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
|
|
3727
|
+
* });
|
|
3577
3728
|
*/
|
|
3578
3729
|
public queryRecords<T = any>(
|
|
3579
3730
|
recordSlug: string,
|
|
@@ -3947,11 +4098,11 @@ export class CentraliSDK {
|
|
|
3947
4098
|
*
|
|
3948
4099
|
* // Or set a user token:
|
|
3949
4100
|
* client.setToken('<JWT_TOKEN>');
|
|
3950
|
-
* await client.queryRecords('Product', {
|
|
4101
|
+
* await client.queryRecords('Product', { pageSize: 10 });
|
|
3951
4102
|
*
|
|
3952
4103
|
* // Subscribe to realtime events (Initial Sync Pattern):
|
|
3953
|
-
* // 1. First fetch initial data
|
|
3954
|
-
* 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' });
|
|
3955
4106
|
* setOrders(orders.data);
|
|
3956
4107
|
*
|
|
3957
4108
|
* // 2. Then subscribe to realtime updates
|