diff --git a/packages/data-objectql/README.md b/packages/data-objectql/README.md index 9c6d25ff1..d2cbdc38f 100644 --- a/packages/data-objectql/README.md +++ b/packages/data-objectql/README.md @@ -1,30 +1,33 @@ # @object-ui/data-objectql -ObjectQL Data Source Adapter for Object UI - Seamlessly connect your Object UI components with ObjectQL API backends. +ObjectQL Data Source Adapter for Object UI - Seamlessly connect your Object UI components with ObjectQL API backends using the official **@objectql/sdk**. ## Features +- ✅ **Official SDK Integration** - Built on top of @objectql/sdk for reliable API communication - ✅ **Universal DataSource Interface** - Implements the standard Object UI data source protocol - ✅ **Full TypeScript Support** - Complete type definitions and IntelliSense - ✅ **React Hooks** - Easy-to-use hooks for data fetching and mutations - ✅ **Automatic Query Conversion** - Converts universal query params to ObjectQL format - ✅ **Error Handling** - Robust error handling with typed error responses - ✅ **Authentication** - Built-in support for token-based authentication -- ✅ **Multi-tenant** - Space ID support for multi-tenant environments +- ✅ **Universal Runtime** - Works in browsers, Node.js, and edge runtimes ## Installation ```bash # Using npm -npm install @object-ui/data-objectql +npm install @object-ui/data-objectql @objectql/sdk # Using yarn -yarn add @object-ui/data-objectql +yarn add @object-ui/data-objectql @objectql/sdk # Using pnpm -pnpm add @object-ui/data-objectql +pnpm add @object-ui/data-objectql @objectql/sdk ``` +**Note**: The package now depends on `@objectql/sdk` and `@objectql/types`, which provide the underlying HTTP client and type definitions. + ## Quick Start ### Basic Usage @@ -144,15 +147,14 @@ new ObjectQLDataSource(config: ObjectQLConfig) ```typescript interface ObjectQLConfig { baseUrl: string; // ObjectQL API base URL - version?: string; // API version (default: 'v1') token?: string; // Authentication token - spaceId?: string; // Space ID for multi-tenant headers?: Record; // Additional headers timeout?: number; // Request timeout (default: 30000ms) - withCredentials?: boolean; // Include credentials (default: true) } ``` +**Note**: This configuration is compatible with `@objectql/sdk`'s `DataApiClientConfig`. Additional options supported by the SDK can also be passed. + #### Methods ##### find(resource, params) @@ -378,6 +380,51 @@ const result = await dataSource.find('contacts'); const contact: Contact = result.data[0]; // Typed! ``` +## Architecture + +This adapter is built on top of the official ObjectQL SDK: + +``` +Object UI Components + ↓ +@object-ui/data-objectql (this package) + ↓ +@objectql/sdk (DataApiClient) + ↓ +ObjectQL Server API +``` + +### Benefits of Using the Official SDK + +- **Reliability**: Uses the official, well-tested ObjectQL HTTP client +- **Compatibility**: Always compatible with the latest ObjectQL server versions +- **Type Safety**: Leverages @objectql/types for consistent type definitions +- **Universal Runtime**: Works in browsers, Node.js, Deno, and edge runtimes +- **Automatic Updates**: SDK improvements automatically benefit this adapter + +## Migration from Previous Versions + +If you're upgrading from a previous version that used custom fetch logic: + +1. Update your dependencies to include `@objectql/sdk`: + ```bash + pnpm add @objectql/sdk @objectql/types + ``` + +2. The configuration interface has been simplified. Remove deprecated options: + - `version` - The SDK handles API versioning internally + - `spaceId` - Use custom headers if needed + - `withCredentials` - The SDK manages this automatically + +3. Filter formats now support both object and array notation: + ```typescript + // Object format (converted to array internally) + $filter: { status: 'active', age: 18 } + + // Array format (FilterExpression - native ObjectQL format) + $filter: [['status', '=', 'active'], ['age', '=', 18]] + ``` + ## License MIT @@ -387,3 +434,4 @@ MIT - [Object UI Documentation](https://www.objectui.org) - [GitHub Repository](https://github.com/objectstack-ai/objectui) - [ObjectQL Documentation](https://www.objectql.com) +- [ObjectQL SDK](https://github.com/objectstack-ai/objectql) diff --git a/packages/data-objectql/package.json b/packages/data-objectql/package.json index 6c6972196..ae4dd7d00 100644 --- a/packages/data-objectql/package.json +++ b/packages/data-objectql/package.json @@ -40,7 +40,9 @@ "directory": "packages/data-objectql" }, "dependencies": { - "@object-ui/types": "workspace:*" + "@object-ui/types": "workspace:*", + "@objectql/sdk": "^1.8.2", + "@objectql/types": "^1.8.2" }, "devDependencies": { "typescript": "^5.0.0", diff --git a/packages/data-objectql/src/ObjectQLDataSource.ts b/packages/data-objectql/src/ObjectQLDataSource.ts index 74862a9de..07ac4775b 100644 --- a/packages/data-objectql/src/ObjectQLDataSource.ts +++ b/packages/data-objectql/src/ObjectQLDataSource.ts @@ -5,6 +5,8 @@ * with ObjectQL API backends. It implements the universal DataSource interface * from @object-ui/types to provide seamless data access. * + * This adapter uses the official @objectql/sdk package for all API communication. + * * @module data-objectql * @packageDocumentation */ @@ -16,6 +18,13 @@ import type { APIError } from '@object-ui/types'; +import { DataApiClient } from '@objectql/sdk'; +import type { + DataApiClientConfig, + DataApiListParams, + FilterExpression +} from '@objectql/types'; + /** * ObjectQL-specific query parameters. * Extends the standard QueryParams with ObjectQL-specific features. @@ -31,14 +40,15 @@ export interface ObjectQLQueryParams extends QueryParams { /** * ObjectQL filters using MongoDB-like syntax * @example { name: 'John', age: { $gte: 18 } } + * @example [['name', '=', 'John'], ['age', '>=', 18]] */ - filters?: Record; + filters?: FilterExpression; /** * Sort configuration - * @example { created: -1, name: 1 } + * @example [['created', 'desc'], ['name', 'asc']] */ - sort?: Record; + sort?: [string, 'asc' | 'desc'][]; /** * Number of records to skip (pagination) @@ -58,31 +68,21 @@ export interface ObjectQLQueryParams extends QueryParams { /** * ObjectQL connection configuration + * Compatible with @objectql/sdk DataApiClientConfig */ -export interface ObjectQLConfig { +export interface ObjectQLConfig extends DataApiClientConfig { /** * Base URL of the ObjectQL server * @example 'https://api.example.com' or '/api' */ baseUrl: string; - /** - * API version (optional) - * @default 'v1' - */ - version?: string; - /** * Authentication token (optional) * Will be sent as Authorization header */ token?: string; - /** - * Space ID for multi-tenant environments (optional) - */ - spaceId?: string; - /** * Additional headers to include in requests */ @@ -93,19 +93,13 @@ export interface ObjectQLConfig { * @default 30000 */ timeout?: number; - - /** - * Whether to include credentials in requests - * @default true - */ - withCredentials?: boolean; } /** * ObjectQL Data Source Adapter * * Implements the universal DataSource interface to connect Object UI - * components with ObjectQL API backends. + * components with ObjectQL API backends using the official @objectql/sdk. * * @template T - The data type * @@ -129,85 +123,87 @@ export interface ObjectQLConfig { * // Fetch data * const result = await dataSource.find('contacts', { * fields: ['name', 'email', 'account.name'], - * filters: { status: 'active' }, - * sort: { created: -1 }, + * filters: [['status', '=', 'active']], + * sort: [['created', 'desc']], * limit: 10 * }); * ``` */ export class ObjectQLDataSource implements DataSource { - private config: Required; + private client: DataApiClient; constructor(config: ObjectQLConfig) { - this.config = { - version: 'v1', - timeout: 30000, - withCredentials: true, - headers: {}, - ...config, - token: config.token || '', - spaceId: config.spaceId || '', - }; - } - - /** - * Build the full API URL for a resource - */ - private buildUrl(resource: string, id?: string | number): string { - const { baseUrl, version } = this.config; - const parts = [baseUrl, 'api', version, 'objects', resource]; - if (id !== undefined) { - parts.push(String(id)); - } - return parts.join('/'); - } - - /** - * Build request headers - */ - private buildHeaders(): HeadersInit { - const headers: Record = { - 'Content-Type': 'application/json', - ...this.config.headers, - }; - - if (this.config.token) { - headers['Authorization'] = `Bearer ${this.config.token}`; - } - - if (this.config.spaceId) { - headers['X-Space-Id'] = this.config.spaceId; - } - - return headers; + // Initialize the official ObjectQL SDK client + this.client = new DataApiClient(config); } /** - * Convert universal QueryParams to ObjectQL format + * Convert universal QueryParams to ObjectQL DataApiListParams format */ - private convertParams(params?: QueryParams): ObjectQLQueryParams { + private convertParams(params?: QueryParams): DataApiListParams { if (!params) return {}; - const objectqlParams: ObjectQLQueryParams = {}; + const objectqlParams: DataApiListParams = {}; // Convert $select to fields if (params.$select) { objectqlParams.fields = params.$select; } - // Convert $filter to filters + // Convert $filter to filters (FilterExpression format) if (params.$filter) { - objectqlParams.filters = params.$filter; + // If it's already an array (FilterExpression), use it directly + if (Array.isArray(params.$filter)) { + objectqlParams.filter = params.$filter as FilterExpression; + } else { + // Convert object format (including Mongo-like operator objects) to FilterExpression format + const filterEntries = Object.entries(params.$filter); + const filters: any[] = []; + + const operatorMap: Record = { + $eq: '=', + $ne: '!=', + $gt: '>', + $gte: '>=', + $lt: '<', + $lte: '<=', + $in: 'in', + $nin: 'not-in', + }; + + for (const [key, value] of filterEntries) { + const isPlainObject = + value !== null && + typeof value === 'object' && + !Array.isArray(value); + + if (isPlainObject) { + const opEntries = Object.entries(value as Record); + const hasDollarOperator = opEntries.some(([op]) => op.startsWith('$')); + + if (hasDollarOperator) { + for (const [rawOp, opValue] of opEntries) { + const mappedOp = + operatorMap[rawOp as keyof typeof operatorMap] ?? + rawOp.replace(/^\$/, ''); + filters.push([key, mappedOp, opValue]); + } + continue; + } + } + + // Fallback: treat as simple equality + filters.push([key, '=', value]); + } + + objectqlParams.filter = filters as FilterExpression; + } } // Convert $orderby to sort if (params.$orderby) { - objectqlParams.sort = Object.entries(params.$orderby).reduce( - (acc, [key, dir]) => ({ - ...acc, - [key]: dir === 'asc' ? 1 : -1, - }), - {} + objectqlParams.sort = Object.entries(params.$orderby).map( + ([key, dir]) => [key, dir] as [string, 'asc' | 'desc'] ); } @@ -220,70 +216,9 @@ export class ObjectQLDataSource implements DataSource { objectqlParams.limit = params.$top; } - if (params.$count !== undefined) { - objectqlParams.count = params.$count; - } - return objectqlParams; } - /** - * Make an HTTP request to ObjectQL API - */ - private async request( - url: string, - options: RequestInit = {} - ): Promise { - const controller = new AbortController(); - const timeoutId = setTimeout( - () => controller.abort(), - this.config.timeout - ); - - try { - const response = await fetch(url, { - ...options, - headers: this.buildHeaders(), - credentials: this.config.withCredentials ? 'include' : 'omit', - signal: controller.signal, - }); - - clearTimeout(timeoutId); - - if (!response.ok) { - const error: APIError = { - message: `HTTP ${response.status}: ${response.statusText}`, - status: response.status, - }; - - try { - const errorData = await response.json(); - error.message = errorData.message || error.message; - error.code = errorData.code; - error.errors = errorData.errors; - error.data = errorData; - } catch { - // If error response is not JSON, use default message - } - - throw error; - } - - return await response.json(); - } catch (err) { - clearTimeout(timeoutId); - - if (err instanceof Error && err.name === 'AbortError') { - throw { - message: 'Request timeout', - code: 'TIMEOUT', - } as APIError; - } - - throw err; - } - } - /** * Fetch multiple records from ObjectQL * @@ -294,53 +229,28 @@ export class ObjectQLDataSource implements DataSource { async find(resource: string, params?: QueryParams): Promise> { const objectqlParams = this.convertParams(params); - // Build query string - const queryParams = new URLSearchParams(); - - if (objectqlParams.fields?.length) { - queryParams.append('fields', JSON.stringify(objectqlParams.fields)); - } - - if (objectqlParams.filters) { - queryParams.append('filters', JSON.stringify(objectqlParams.filters)); - } - - if (objectqlParams.sort) { - queryParams.append('sort', JSON.stringify(objectqlParams.sort)); - } - - if (objectqlParams.skip !== undefined) { - queryParams.append('skip', String(objectqlParams.skip)); - } - - if (objectqlParams.limit !== undefined) { - queryParams.append('top', String(objectqlParams.limit)); - } - - if (objectqlParams.count) { - queryParams.append('$count', 'true'); + try { + const response = await this.client.list(resource, objectqlParams); + + const data = response.items || []; + const total = response.meta?.total; + + return { + data, + total, + page: response.meta?.page, + pageSize: objectqlParams.limit, + hasMore: response.meta?.has_next, + }; + } catch (err: any) { + // Convert SDK errors to APIError format + throw { + message: err.message || 'Failed to fetch data', + code: err.code, + status: err.status, + data: err, + } as APIError; } - - const url = `${this.buildUrl(resource)}?${queryParams.toString()}`; - const response = await this.request<{ - value: T[]; - '@odata.count'?: number; - }>(url); - - const data = response.value || []; - const total = response['@odata.count']; - - return { - data, - total, - page: objectqlParams.skip && objectqlParams.limit - ? Math.floor(objectqlParams.skip / objectqlParams.limit) + 1 - : 1, - pageSize: objectqlParams.limit, - hasMore: total !== undefined && data.length > 0 - ? (objectqlParams.skip || 0) + data.length < total - : undefined, - }; } /** @@ -348,7 +258,7 @@ export class ObjectQLDataSource implements DataSource { * * @param resource - Object name * @param id - Record identifier - * @param params - Additional query parameters + * @param params - Additional query parameters (fields selection) * @returns Promise resolving to the record or null if not found */ async findOne( @@ -356,23 +266,34 @@ export class ObjectQLDataSource implements DataSource { id: string | number, params?: QueryParams ): Promise { - const objectqlParams = this.convertParams(params); - - const queryParams = new URLSearchParams(); - - if (objectqlParams.fields?.length) { - queryParams.append('fields', JSON.stringify(objectqlParams.fields)); - } - - const url = `${this.buildUrl(resource, id)}?${queryParams.toString()}`; - try { - return await this.request(url); - } catch (err) { - if ((err as APIError).status === 404) { + const response = await this.client.get(resource, id); + + // Return the item data, filtering fields if requested + if (params?.$select && response) { + const filtered: any = {}; + for (const field of params.$select) { + if (field in response) { + filtered[field] = (response as any)[field]; + } + } + return filtered as T; + } + + return response ? (response as T) : null; + } catch (err: any) { + // Return null for not found errors + if (err.code === 'NOT_FOUND' || err.status === 404) { return null; } - throw err; + + // Re-throw other errors as APIError + throw { + message: err.message || 'Failed to fetch record', + code: err.code, + status: err.status, + data: err, + } as APIError; } } @@ -384,11 +305,17 @@ export class ObjectQLDataSource implements DataSource { * @returns Promise resolving to the created record */ async create(resource: string, data: Partial): Promise { - const url = this.buildUrl(resource); - return this.request(url, { - method: 'POST', - body: JSON.stringify(data), - }); + try { + const response = await this.client.create(resource, data); + return response as T; + } catch (err: any) { + throw { + message: err.message || 'Failed to create record', + code: err.code, + status: err.status, + data: err, + } as APIError; + } } /** @@ -404,11 +331,17 @@ export class ObjectQLDataSource implements DataSource { id: string | number, data: Partial ): Promise { - const url = this.buildUrl(resource, id); - return this.request(url, { - method: 'PATCH', - body: JSON.stringify(data), - }); + try { + const response = await this.client.update(resource, id, data); + return response as T; + } catch (err: any) { + throw { + message: err.message || 'Failed to update record', + code: err.code, + status: err.status, + data: err, + } as APIError; + } } /** @@ -419,9 +352,17 @@ export class ObjectQLDataSource implements DataSource { * @returns Promise resolving to true if successful */ async delete(resource: string, id: string | number): Promise { - const url = this.buildUrl(resource, id); - await this.request(url, { method: 'DELETE' }); - return true; + try { + const response = await this.client.delete(resource, id); + return response.success ?? true; + } catch (err: any) { + throw { + message: err.message || 'Failed to delete record', + code: err.code, + status: err.status, + data: err, + } as APIError; + } } /** @@ -429,19 +370,73 @@ export class ObjectQLDataSource implements DataSource { * * @param resource - Object name * @param operation - Operation type - * @param data - Bulk data + * @param data - Bulk data (array of records for create/update/delete) * @returns Promise resolving to operation results */ async bulk( resource: string, operation: 'create' | 'update' | 'delete', - data: Partial[] + data: Partial[] | any ): Promise { - const url = `${this.buildUrl(resource)}/bulk`; - return this.request(url, { - method: 'POST', - body: JSON.stringify({ operation, data }), - }); + try { + if (operation === 'create') { + const response = await this.client.createMany(resource, data); + return response.items || []; + } else if (operation === 'update') { + // Fallback implementation: iterate and call single-record update + if (!Array.isArray(data)) { + throw new Error('Bulk update requires array of records'); + } + + const results: T[] = []; + for (const item of data) { + const record: any = item as any; + const id = record?.id ?? record?._id; + if (id === undefined || id === null) { + throw new Error( + 'Bulk update requires each item to include an `id` or `_id` field.' + ); + } + // Do not send id as part of the update payload + const { id: _omitId, _id: _omitUnderscore, ...updateData } = record; + const updated = await this.client.update(resource, id, updateData); + if (updated !== undefined && updated !== null) { + results.push(updated as T); + } + } + return results; + } else if (operation === 'delete') { + // Fallback implementation: iterate and call single-record delete + if (!Array.isArray(data)) { + throw new Error('Bulk delete requires array of records or IDs'); + } + + for (const item of data) { + const record: any = item as any; + // Support both direct ID values and objects with id/_id field + const id = typeof record === 'object' + ? (record?.id ?? record?._id) + : record; + if (id === undefined || id === null) { + throw new Error( + 'Bulk delete requires each item to include an `id` or `_id` field or be an id value.' + ); + } + await this.client.delete(resource, id); + } + // For delete operations, we return an empty array by convention + return []; + } + + throw new Error(`Unknown bulk operation: ${operation}`); + } catch (err: any) { + throw { + message: err.message || 'Failed to execute bulk operation', + code: err.code, + status: err.status, + data: err, + } as APIError; + } } } diff --git a/packages/data-objectql/src/__tests__/ObjectQLDataSource.test.ts b/packages/data-objectql/src/__tests__/ObjectQLDataSource.test.ts index c03e23093..6b90d5150 100644 --- a/packages/data-objectql/src/__tests__/ObjectQLDataSource.test.ts +++ b/packages/data-objectql/src/__tests__/ObjectQLDataSource.test.ts @@ -22,11 +22,13 @@ describe('ObjectQLDataSource', () => { describe('find', () => { it('should fetch multiple records', async () => { const mockData = { - value: [ + items: [ { _id: '1', name: 'John' }, { _id: '2', name: 'Jane' }, ], - '@odata.count': 2, + meta: { + total: 2, + } }; (global.fetch as any).mockResolvedValueOnce({ @@ -36,12 +38,12 @@ describe('ObjectQLDataSource', () => { const result = await dataSource.find('contacts'); - expect(result.data).toEqual(mockData.value); + expect(result.data).toEqual(mockData.items); expect(result.total).toBe(2); }); it('should convert universal query params to ObjectQL format', async () => { - const mockData = { value: [], '@odata.count': 0 }; + const mockData = { items: [], meta: { total: 0 } }; (global.fetch as any).mockResolvedValueOnce({ ok: true, @@ -59,15 +61,47 @@ describe('ObjectQLDataSource', () => { const fetchCall = (global.fetch as any).mock.calls[0]; const url = fetchCall[0]; - expect(url).toContain('fields='); - expect(url).toContain('filters='); - expect(url).toContain('sort='); + expect(url).toContain('filter='); expect(url).toContain('skip=10'); - expect(url).toContain('top=20'); + expect(url).toContain('limit=20'); + }); + + it('should convert MongoDB-like operators in filters', async () => { + const mockData = { items: [], meta: { total: 0 } }; + + (global.fetch as any).mockResolvedValueOnce({ + ok: true, + json: async () => mockData, + }); + + await dataSource.find('contacts', { + $filter: { + age: { $gte: 18, $lte: 65 }, + status: { $in: ['active', 'pending'] } + } + }); + + const fetchCall = (global.fetch as any).mock.calls[0]; + const url = fetchCall[0]; + + // Verify the filter parameter is present + expect(url).toContain('filter='); + + // The filter should be encoded as a JSON array with operators + const urlObj = new URL(url, 'http://localhost'); + const filterParam = urlObj.searchParams.get('filter'); + if (filterParam) { + const filter = JSON.parse(filterParam); + // Should have converted to FilterExpression format + expect(Array.isArray(filter)).toBe(true); + // Should have converted $gte to '>=' and $lte to '<=' + expect(filter.some((f: any) => f[1] === '>=')).toBe(true); + expect(filter.some((f: any) => f[1] === '<=')).toBe(true); + } }); it('should include authentication token in headers', async () => { - const mockData = { value: [] }; + const mockData = { items: [] }; (global.fetch as any).mockResolvedValueOnce({ ok: true, @@ -102,7 +136,12 @@ describe('ObjectQLDataSource', () => { ok: false, status: 404, statusText: 'Not Found', - json: async () => ({ message: 'Not found' }), + json: async () => ({ + error: { + code: 'NOT_FOUND', + message: 'Not found' + } + }), }); const result = await dataSource.findOne('contacts', 'nonexistent'); @@ -150,7 +189,9 @@ describe('ObjectQLDataSource', () => { const fetchCall = (global.fetch as any).mock.calls[0]; const options = fetchCall[1]; - expect(options.method).toBe('PATCH'); + // The SDK uses PUT method for updates (not PATCH) + // This is the standard behavior of @objectql/sdk's DataApiClient + expect(options.method).toBe('PUT'); expect(options.body).toBe(JSON.stringify(updates)); }); }); @@ -159,7 +200,7 @@ describe('ObjectQLDataSource', () => { it('should delete a record', async () => { (global.fetch as any).mockResolvedValueOnce({ ok: true, - json: async () => ({}), + json: async () => ({ success: true }), }); const result = await dataSource.delete('contacts', '1'); @@ -174,15 +215,17 @@ describe('ObjectQLDataSource', () => { }); describe('bulk', () => { - it('should execute bulk operations', async () => { + it('should execute bulk create operations', async () => { const bulkData = [ { name: 'Contact 1' }, { name: 'Contact 2' }, ]; - const createdRecords = [ - { _id: '1', name: 'Contact 1' }, - { _id: '2', name: 'Contact 2' }, - ]; + const createdRecords = { + items: [ + { _id: '1', name: 'Contact 1' }, + { _id: '2', name: 'Contact 2' }, + ] + }; (global.fetch as any).mockResolvedValueOnce({ ok: true, @@ -191,32 +234,33 @@ describe('ObjectQLDataSource', () => { const result = await dataSource.bulk('contacts', 'create', bulkData); - expect(result).toEqual(createdRecords); + expect(result).toEqual(createdRecords.items); const fetchCall = (global.fetch as any).mock.calls[0]; const options = fetchCall[1]; expect(options.method).toBe('POST'); - expect(JSON.parse(options.body)).toEqual({ - operation: 'create', - data: bulkData, - }); }); }); describe('error handling', () => { - it('should throw error for non-OK responses', async () => { + it('should handle API errors', async () => { (global.fetch as any).mockResolvedValueOnce({ ok: false, status: 500, statusText: 'Internal Server Error', - json: async () => ({ message: 'Server error' }), + json: async () => ({ + error: { + code: 'INTERNAL_ERROR', + message: 'Server error' + } + }), }); await expect(dataSource.find('contacts')).rejects.toThrow(); }); - it('should throw error for timeout configuration', () => { + it('should accept timeout configuration', () => { // Test that timeout configuration is accepted const dataSourceWithShortTimeout = new ObjectQLDataSource({ baseUrl: 'https://api.example.com', @@ -228,42 +272,25 @@ describe('ObjectQLDataSource', () => { }); describe('configuration', () => { - it('should include spaceId in headers when provided', async () => { - const dataSourceWithSpace = new ObjectQLDataSource({ + it('should accept timeout configuration', () => { + // Test that timeout configuration is accepted + const dataSourceWithTimeout = new ObjectQLDataSource({ baseUrl: 'https://api.example.com', - spaceId: 'space123', - }); - - (global.fetch as any).mockResolvedValueOnce({ - ok: true, - json: async () => ({ value: [] }), + timeout: 5000, }); - await dataSourceWithSpace.find('contacts'); - - const fetchCall = (global.fetch as any).mock.calls[0]; - const options = fetchCall[1]; - - expect(options.headers['X-Space-Id']).toBe('space123'); + expect(dataSourceWithTimeout).toBeDefined(); }); - it('should use custom API version', async () => { - const dataSourceWithVersion = new ObjectQLDataSource({ + it('should accept custom headers', () => { + const dataSourceWithHeaders = new ObjectQLDataSource({ baseUrl: 'https://api.example.com', - version: 'v2', + headers: { + 'X-Custom-Header': 'custom-value' + } }); - (global.fetch as any).mockResolvedValueOnce({ - ok: true, - json: async () => ({ value: [] }), - }); - - await dataSourceWithVersion.find('contacts'); - - const fetchCall = (global.fetch as any).mock.calls[0]; - const url = fetchCall[0]; - - expect(url).toContain('/api/v2/'); + expect(dataSourceWithHeaders).toBeDefined(); }); }); }); diff --git a/packages/data-objectql/src/hooks.ts b/packages/data-objectql/src/hooks.ts index 0027540a3..bc6915472 100644 --- a/packages/data-objectql/src/hooks.ts +++ b/packages/data-objectql/src/hooks.ts @@ -78,7 +78,8 @@ export interface UseObjectQLMutationOptions { export function useObjectQL(options: UseObjectQLOptions): ObjectQLDataSource { return useMemo( () => new ObjectQLDataSource(options.config), - [options.config.baseUrl, options.config.token, options.config.spaceId] + // eslint-disable-next-line react-hooks/exhaustive-deps + [JSON.stringify(options.config)] ); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d6a85eea6..862ef4fd3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -430,6 +430,12 @@ importers: '@object-ui/types': specifier: workspace:* version: link:../types + '@objectql/sdk': + specifier: ^1.8.2 + version: 1.8.2 + '@objectql/types': + specifier: ^1.8.2 + version: 1.8.2 react: specifier: 18.3.1 version: 18.3.1 @@ -1416,6 +1422,12 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@objectql/sdk@1.8.2': + resolution: {integrity: sha512-dIjb9FabsXq4T9mDSa9BXqTR37Ufc5U+vfSDYx9jpsxMhVphRlDd1a3HruQvmYUY6r+opk7rgnJC0k3mq5vAlg==} + + '@objectql/types@1.8.2': + resolution: {integrity: sha512-0anwrPbHLjoruZEAM/d/lB0WC264ZlQRCIEzdblbarGKjJT7CzBAjizJ1mLwpXOZpUDyD2W6o5TIQ7XMPhPUIg==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -5620,6 +5632,12 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 + '@objectql/sdk@1.8.2': + dependencies: + '@objectql/types': 1.8.2 + + '@objectql/types@1.8.2': {} + '@pkgjs/parseargs@0.11.0': optional: true