@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 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/compute-functions-guide)
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 including filter, sort, pagination, and expand
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
- * filter: { status: 'active' },
1895
+ * 'data.status': 'active',
1853
1896
  * sort: '-createdAt',
1854
- * limit: 10
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
- * filter: { inStock: true, price: { lte: 100 } },
1903
+ * 'data.inStock': true,
1904
+ * 'data.price[lte]': 100,
1860
1905
  * sort: '-createdAt',
1861
- * limit: 10
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
- * filter: { status: { in: ['pending', 'processing'] } },
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
- * filter: { age: { gte: 18, lte: 65 }, verified: true }
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', { limit: 10 });
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', { filter: { status: 'pending' } });
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
- * Use these within filter objects to apply comparison operations.
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
- * // Filter with operators
575
- * { age: { gte: 18, lte: 65 } }
576
- * { status: { in: ['active', 'pending'] } }
577
- * { email: { contains: '@gmail.com' } }
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
- /** Maximum number of records to return */
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
- /** Additional query parameters */
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 including filter, sort, pagination, and expand
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
- * filter: { status: 'active' },
3695
+ * 'data.status': 'active',
3624
3696
  * sort: '-createdAt',
3625
- * limit: 10
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
- * filter: { inStock: true, price: { lte: 100 } },
3703
+ * 'data.inStock': true,
3704
+ * 'data.price[lte]': 100,
3631
3705
  * sort: '-createdAt',
3632
- * limit: 10
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
- * filter: { status: { in: ['pending', 'processing'] } },
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
- * filter: { age: { gte: 18, lte: 65 }, verified: true }
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', { limit: 10 });
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', { filter: { status: 'pending' } });
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@centrali-io/centrali-sdk",
3
- "version": "3.0.1",
3
+ "version": "3.0.2",
4
4
  "description": "Centrali Node SDK",
5
5
  "main": "dist/index.js",
6
6
  "type": "commonjs",