@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 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,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 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
- * // Basic query with filter and sort
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
- * filter: 'inStock = true AND price < 100',
1903
+ * 'data.inStock': true,
1904
+ * 'data.price[lte]': 100,
1853
1905
  * sort: '-createdAt',
1854
- * limit: 10
1906
+ * pageSize: 10
1855
1907
  * });
1856
1908
  *
1857
- * // Query with expanded references
1909
+ * // Multiple values with 'in' operator (comma-separated string)
1858
1910
  * const orders = await centrali.queryRecords('Order', {
1859
- * filter: 'status = "pending"',
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', { limit: 10 });
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', { 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' });
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
- /** Maximum number of records to return */
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
- /** 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
+ */
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 including filter, sort, pagination, and expand
3690
+ * @param queryParams - Query parameters (filters at top level, plus sort, pagination, expand)
3562
3691
  *
3563
3692
  * @example
3564
- * // Basic query with filter and sort
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
- * filter: 'inStock = true AND price < 100',
3703
+ * 'data.inStock': true,
3704
+ * 'data.price[lte]': 100,
3567
3705
  * sort: '-createdAt',
3568
- * limit: 10
3706
+ * pageSize: 10
3569
3707
  * });
3570
3708
  *
3571
- * // Query with expanded references
3709
+ * // Multiple values with 'in' operator (comma-separated string)
3572
3710
  * const orders = await centrali.queryRecords('Order', {
3573
- * filter: 'status = "pending"',
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', { limit: 10 });
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', { 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' });
3955
4106
  * setOrders(orders.data);
3956
4107
  *
3957
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.0",
3
+ "version": "3.0.2",
4
4
  "description": "Centrali Node SDK",
5
5
  "main": "dist/index.js",
6
6
  "type": "commonjs",