Align client with batch, cache, error, and view-storage specs from PR #380#381
Align client with batch, cache, error, and view-storage specs from PR #380#381
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
- Add batch operations with BatchUpdateRequest/Response schemas - Add metadata caching with ETag support (getCached method) - Implement standardized error handling with error codes and categories - Add view storage API (create, list, update, delete, share views) - Re-export commonly used types from @objectstack/spec/api - Update method signatures to match new API contracts Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
- Document batch operations with transaction support - Document metadata caching with ETag examples - Document view storage API methods - Add error handling section with standardized error codes - Update usage examples with new features Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
- Fix ETag parsing to properly detect weak ETags (check W/ before removing quotes) - Fix 304 response to correctly parse weak ETags from ifNoneMatch - Remove redundant array mapping in updateMany method - Change createMany endpoint to /createMany to avoid conflict with /batch - Remove deprecated tag from updateMany (it's a convenience method) - Update README to clarify batch() is the recommended method Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
|
@copilot 这些函数服务端API都支持了吗 |
目前服务端API还未完全支持这些新功能。当前实现情况: 已支持 (plugin-hono-server):
未实现:
这个PR采用"规范优先"的方法,客户端已对齐PR #380的API规范。服务端实现需要在后续PR中更新 建议下一步:
|
|
@copilot 帮我写下一步操作的AI提示词 |
以下是实现服务端API的AI提示词: 任务:为 ObjectStack 服务端实现 PR #380 的批量操作、元数据缓存和视图存储 API 背景客户端已对齐 PR #380 的 API 规范,但服务端实现滞后。需要扩展协议接口、实现业务逻辑并注册 HTTP 路由。 需要修改的文件1.
|
| import { | ||
| BatchUpdateRequest, | ||
| BatchUpdateResponse, | ||
| UpdateManyRequest, | ||
| DeleteManyRequest, | ||
| BatchOptions, | ||
| MetadataCacheRequest, | ||
| MetadataCacheResponse, | ||
| StandardErrorCode, | ||
| ErrorCategory, | ||
| CreateViewRequest, | ||
| UpdateViewRequest, | ||
| ListViewsRequest, | ||
| SavedView, | ||
| ListViewsResponse, | ||
| ViewResponse | ||
| } from '@objectstack/spec/api'; |
Check notice
Code scanning / CodeQL
Unused variable, import, function or class Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 2 months ago
In general, unused imports should be removed to improve readability and avoid confusion about what is actually used. This does not change runtime behavior when the import is for types or pure declarations.
For this specific case, in packages/client/src/index.ts, we should modify the multi-line import from @objectstack/spec/api to remove SavedView from the named imports, leaving all the other imported symbols untouched. No new methods, imports, or definitions are required; we are only deleting the unused symbol from the import list. The change is localized to that single import block around lines 2–18.
| @@ -12,7 +12,6 @@ | ||
| CreateViewRequest, | ||
| UpdateViewRequest, | ||
| ListViewsRequest, | ||
| SavedView, | ||
| ListViewsResponse, | ||
| ViewResponse | ||
| } from '@objectstack/spec/api'; |
There was a problem hiding this comment.
Pull request overview
This PR aligns the ObjectStack client with the new API specifications introduced in PR #380, adding support for batch operations, metadata caching, standardized error handling, and view storage capabilities.
Changes:
- Added batch operations API with atomic transaction support (
batch, updatedupdateMany/deleteMany, separatedcreateManyendpoint) - Implemented ETag-based metadata caching with HTTP 304 Not Modified support
- Enhanced error handling with machine-readable error codes, categories, and retry guidance
- Added complete view storage API for saving, loading, sharing, and managing UI configurations
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| packages/client/src/index.ts | Implemented batch operations, metadata caching, view storage APIs, and enhanced error handling with standardized error codes |
| packages/client/README.md | Updated documentation with examples for new features including batch operations, metadata caching, view storage, and error handling patterns |
| ### `client.data` | ||
| - `find<T>(object, options)`: Advanced query with filtering, sorting, selection. | ||
| - `get<T>(object, id)`: Get single record by ID. | ||
| - `query<T>(object, ast)`: Execute complex query using full AST. | ||
| - `create<T>(object, data)`: Create record. | ||
| - `createMany<T>(object, data[])`: Batch create records. | ||
| - `batch(object, request)`: **Recommended** - Execute batch operations (create/update/upsert/delete) with full control. | ||
| - `createMany<T>(object, data[])`: Batch create records (convenience method). | ||
| - `update<T>(object, id, data)`: Update record. | ||
| - `updateMany<T>(object, ids[], data)`: Batch update records. | ||
| - `updateMany<T>(object, records[], options?)`: Batch update records (convenience method). | ||
| - `delete(object, id)`: Delete record. | ||
| - `deleteMany(object, ids[])`: Batch delete records. | ||
| - `deleteMany(object, ids[], options?)`: Batch delete records (convenience method). |
There was a problem hiding this comment.
The API documentation has been updated to reflect the new signatures for updateMany and deleteMany, but there's no mention that these are breaking changes from previous versions.
Consider adding a "Migration Guide" or "Breaking Changes" section to the README to help users upgrading from earlier versions. The changes are:
- updateMany: Changed from
(object, data, filters)returningPromise<number>to(object, records[], options?)returningPromise<BatchUpdateResponse> - deleteMany: Changed from
(object, filters)to(object, ids[], options)- now requires explicit IDs instead of accepting filter criteria
This documentation would be especially helpful since deleteMany now has fundamentally different behavior (explicit IDs vs filter-based deletion).
| value: cacheOptions.ifNoneMatch.replace(/^W\/|"/g, ''), | ||
| weak: cacheOptions.ifNoneMatch.startsWith('W/') |
There was a problem hiding this comment.
The regex pattern /^W\/|"/g for stripping ETags is functional but confusing. The pattern matches W/ at the start OR any " character globally, which works but is not immediately clear to readers.
Consider using a more explicit approach for better maintainability:
- First check if the ETag starts with
W/and remove it - Then remove all quote characters
This would make the code's intent clearer and easier to understand.
| // Check for 304 Not Modified | ||
| if (res.status === 304) { | ||
| return { | ||
| notModified: true, | ||
| etag: cacheOptions?.ifNoneMatch ? { | ||
| value: cacheOptions.ifNoneMatch.replace(/^W\/|"/g, ''), | ||
| weak: cacheOptions.ifNoneMatch.startsWith('W/') | ||
| } : undefined | ||
| }; | ||
| } | ||
|
|
||
| const data = await res.json(); | ||
| const etag = res.headers.get('ETag'); | ||
| const lastModified = res.headers.get('Last-Modified'); | ||
|
|
||
| return { | ||
| data, | ||
| etag: etag ? { | ||
| value: etag.replace(/^W\/|"/g, ''), | ||
| weak: etag.startsWith('W/') | ||
| } : undefined, |
There was a problem hiding this comment.
The same regex pattern /^W\/|"/g is duplicated here. If the ETag parsing logic is updated in one place (lines 155-156), it should also be updated here to maintain consistency.
Consider extracting this logic into a private helper method to avoid duplication and ensure consistent ETag parsing throughout the codebase.
| // Check for 304 Not Modified | |
| if (res.status === 304) { | |
| return { | |
| notModified: true, | |
| etag: cacheOptions?.ifNoneMatch ? { | |
| value: cacheOptions.ifNoneMatch.replace(/^W\/|"/g, ''), | |
| weak: cacheOptions.ifNoneMatch.startsWith('W/') | |
| } : undefined | |
| }; | |
| } | |
| const data = await res.json(); | |
| const etag = res.headers.get('ETag'); | |
| const lastModified = res.headers.get('Last-Modified'); | |
| return { | |
| data, | |
| etag: etag ? { | |
| value: etag.replace(/^W\/|"/g, ''), | |
| weak: etag.startsWith('W/') | |
| } : undefined, | |
| const parseEtag = (raw?: string | null) => | |
| raw ? { value: raw.replace(/^W\/|"/g, ''), weak: raw.startsWith('W/') } : undefined; | |
| // Check for 304 Not Modified | |
| if (res.status === 304) { | |
| return { | |
| notModified: true, | |
| etag: parseEtag(cacheOptions?.ifNoneMatch) | |
| }; | |
| } | |
| const data = await res.json(); | |
| const etagHeader = res.headers.get('ETag'); | |
| const lastModified = res.headers.get('Last-Modified'); | |
| return { | |
| data, | |
| etag: parseEtag(etagHeader), |
| object: string, | ||
| records: Array<{ id: string; data: Partial<T> }>, | ||
| options?: BatchOptions | ||
| ): Promise<BatchUpdateResponse> => { | ||
| const route = this.getRoute('data'); | ||
| const request: UpdateManyRequest = { | ||
| records, | ||
| options | ||
| }; | ||
| const res = await this.fetch(`${this.baseUrl}${route}/${object}/updateMany`, { | ||
| method: 'POST', | ||
| body: JSON.stringify(request) | ||
| }); | ||
| return res.json(); |
There was a problem hiding this comment.
This is a breaking API change from the previous signature. The old updateMany accepted (object, data, filters) and returned a Promise<number>, while the new signature accepts (object, records, options) and returns a Promise<BatchUpdateResponse>.
Users upgrading will need to change:
- OLD:
updateMany('todo', { status: 'done' }, { priority: 1 }) - NEW:
updateMany('todo', [{ id: '1', data: { status: 'done' } }], options)
Consider:
- Adding a migration guide to the README or CHANGELOG
- Providing a deprecation path if backward compatibility is needed
- Documenting this as a breaking change in release notes
| object: string, | |
| records: Array<{ id: string; data: Partial<T> }>, | |
| options?: BatchOptions | |
| ): Promise<BatchUpdateResponse> => { | |
| const route = this.getRoute('data'); | |
| const request: UpdateManyRequest = { | |
| records, | |
| options | |
| }; | |
| const res = await this.fetch(`${this.baseUrl}${route}/${object}/updateMany`, { | |
| method: 'POST', | |
| body: JSON.stringify(request) | |
| }); | |
| return res.json(); | |
| object: string, | |
| dataOrRecords: Partial<T> | Array<{ id: string; data: Partial<T> }>, | |
| filtersOrOptions?: Record<string, any> | BatchOptions | |
| ): Promise<BatchUpdateResponse | number> => { | |
| const route = this.getRoute('data'); | |
| // New API shape: records array + BatchOptions → BatchUpdateResponse | |
| if (Array.isArray(dataOrRecords)) { | |
| const request: UpdateManyRequest = { | |
| records: dataOrRecords, | |
| options: filtersOrOptions as BatchOptions | undefined | |
| }; | |
| const res = await this.fetch(`${this.baseUrl}${route}/${object}/updateMany`, { | |
| method: 'POST', | |
| body: JSON.stringify(request) | |
| }); | |
| return res.json(); | |
| } | |
| // Legacy API shape: data + filters → number | |
| const legacyRequest = { | |
| data: dataOrRecords, | |
| filters: filtersOrOptions | |
| }; | |
| const res = await this.fetch(`${this.baseUrl}${route}/${object}/updateMany`, { | |
| method: 'POST', | |
| body: JSON.stringify(legacyRequest) | |
| }); | |
| const result = await res.json(); | |
| // Prefer a plain number if returned; otherwise, try to derive a count | |
| if (typeof result === 'number') { | |
| return result; | |
| } | |
| if (result && typeof result === 'object') { | |
| if (typeof result.updated === 'number') { | |
| return result.updated; | |
| } | |
| if (typeof result.count === 'number') { | |
| return result.count; | |
| } | |
| } | |
| // Fallback: if no clear number can be derived, return 0 for legacy callers | |
| return 0; |
| deleteMany: async(object: string, ids: string[], options?: BatchOptions): Promise<BatchUpdateResponse> => { | ||
| const route = this.getRoute('data'); | ||
| const res = await this.fetch(`${this.baseUrl}${route}/${object}/batch`, { | ||
| method: 'DELETE', | ||
| body: JSON.stringify({ filters }) | ||
| const request: DeleteManyRequest = { | ||
| ids, | ||
| options | ||
| }; | ||
| const res = await this.fetch(`${this.baseUrl}${route}/${object}/deleteMany`, { | ||
| method: 'POST', | ||
| body: JSON.stringify(request) | ||
| }); | ||
| return res.json(); | ||
| } |
There was a problem hiding this comment.
This is a breaking API change from the previous signature. The old deleteMany accepted (object, filters) where filters could match any records, and returned Promise<{ count: number }>. The new signature accepts (object, ids, options) and returns Promise<BatchUpdateResponse>.
Users upgrading will need to change:
- OLD:
deleteMany('todo', { status: 'cancelled' })- deletes all matching records - NEW:
deleteMany('todo', ['id1', 'id2', 'id3'], options)- requires explicit IDs
This is a significant behavioral change as the old API allowed filter-based deletion while the new API requires explicit ID lists. This change reduces the risk of accidental bulk deletions but requires users to first query for IDs before deletion.
Consider:
- Adding a migration guide explaining this breaking change
- Documenting this as a breaking change in release notes
- If filter-based deletion is still needed, consider providing an alternative method or documenting the pattern of using
find()followed bydeleteMany()
PR #380 introduced new API contracts for batch operations, metadata caching, error standardization, and view storage. The client still used legacy endpoints and patterns.
Batch Operations
batch(object, BatchUpdateRequest)method supporting create/update/upsert/delete with atomic transactionscreateManyto/createManyendpoint (was conflicting with/batch)updateMany/deleteManyto useUpdateManyRequest/DeleteManyRequestschemasMetadata Caching
meta.getCached(name, options)with ETag-based conditional requests"abc") and weak (W/"abc") ETags correctlyError Handling
code,category,httpStatus,retryable,detailspropertiesView Storage
views.create/get/list/update/delete/share/setDefaultmethodsType Exports
Re-exported API types (
BatchUpdateRequest,MetadataCacheResponse,StandardErrorCode, etc.) from@objectstack/spec/apifor convenience.Original prompt
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.