Skip to content

Align client with batch, cache, error, and view-storage specs from PR #380#381

Merged
hotlong merged 4 commits intomainfrom
copilot/adjust-api-and-client-spec
Jan 30, 2026
Merged

Align client with batch, cache, error, and view-storage specs from PR #380#381
hotlong merged 4 commits intomainfrom
copilot/adjust-api-and-client-spec

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Jan 30, 2026

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

  • Added batch(object, BatchUpdateRequest) method supporting create/update/upsert/delete with atomic transactions
  • Migrated createMany to /createMany endpoint (was conflicting with /batch)
  • Updated updateMany/deleteMany to use UpdateManyRequest/DeleteManyRequest schemas
await client.data.batch('contacts', {
  operation: 'update',
  records: [
    { id: '1', data: { status: 'active' } },
    { id: '2', data: { status: 'active' } }
  ],
  options: { atomic: true, returnRecords: true }
});

Metadata Caching

  • Added meta.getCached(name, options) with ETag-based conditional requests
  • Handles 304 Not Modified responses
  • Parses both strong ("abc") and weak (W/"abc") ETags correctly
const result = await client.meta.getCached('account', {
  ifNoneMatch: '"686897696a7c876b7e"'
});
if (result.notModified) { /* use cache */ }

Error Handling

  • Enhanced errors with code, category, httpStatus, retryable, details properties
  • Enables programmatic error handling based on machine-readable error codes

View Storage

  • Added views.create/get/list/update/delete/share/setDefault methods
  • Supports saving custom UI configurations (filters, columns, layouts)
await client.views.create({
  name: 'active_contacts',
  object: 'contact',
  type: 'list',
  visibility: 'public',
  query: { where: { status: 'active' } },
  layout: { columns: [...] }
});

Type Exports

Re-exported API types (BatchUpdateRequest, MetadataCacheResponse, StandardErrorCode, etc.) from @objectstack/spec/api for convenience.

Original prompt

拉取请求: https://github.com/objectstack-ai/spec/pull/380基于规范调整api和 client


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

@vercel
Copy link
Copy Markdown

vercel bot commented Jan 30, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jan 30, 2026 10:22am

Request Review

Copilot AI and others added 3 commits January 30, 2026 10:16
- 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 AI changed the title [WIP] Adjust API and client based on specifications Align client with batch, cache, error, and view-storage specs from PR #380 Jan 30, 2026
Copilot AI requested a review from hotlong January 30, 2026 10:23
@hotlong
Copy link
Copy Markdown
Contributor

hotlong commented Jan 30, 2026

@copilot 这些函数服务端API都支持了吗

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Jan 30, 2026

@copilot 这些函数服务端API都支持了吗

目前服务端API还未完全支持这些新功能。当前实现情况:

已支持 (plugin-hono-server):

  • 基础CRUD: find, get, create, update, delete
  • 元数据查询: getObject, getView

未实现:

  • ❌ 批量操作: /batch, /createMany, /updateMany, /deleteMany
  • ❌ 元数据缓存: ETag 支持
  • ❌ 视图存储: /ui/views CRUD endpoints
  • ❌ 标准化错误响应格式

这个PR采用"规范优先"的方法,客户端已对齐PR #380的API规范。服务端实现需要在后续PR中更新 IObjectStackProtocol 接口和 plugin-hono-server 路由。

建议下一步:

  1. 扩展 IObjectStackProtocol 接口添加批量操作方法
  2. ObjectStackProtocolImplementation 实现这些方法
  3. plugin-hono-server 注册新的路由端点

@hotlong
Copy link
Copy Markdown
Contributor

hotlong commented Jan 30, 2026

@copilot 帮我写下一步操作的AI提示词

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Jan 30, 2026

@copilot 帮我写下一步操作的AI提示词

以下是实现服务端API的AI提示词:


任务:为 ObjectStack 服务端实现 PR #380 的批量操作、元数据缓存和视图存储 API

背景

客户端已对齐 PR #380 的 API 规范,但服务端实现滞后。需要扩展协议接口、实现业务逻辑并注册 HTTP 路由。

需要修改的文件

1. packages/spec/src/api/protocol.ts

扩展 IObjectStackProtocol 接口,添加:

  • batchData(object: string, request: BatchUpdateRequest): Promise<BatchUpdateResponse>
  • createManyData(object: string, records: any[]): Promise<any[]>
  • updateManyData(object: string, request: UpdateManyRequest): Promise<BatchUpdateResponse>
  • deleteManyData(object: string, request: DeleteManyRequest): Promise<BatchUpdateResponse>
  • getMetaItemCached(type: string, name: string, cacheRequest?: MetadataCacheRequest): Promise<MetadataCacheResponse>
  • View storage methods: createView, getView, listViews, updateView, deleteView

2. packages/objectql/src/protocol.ts

ObjectStackProtocolImplementation 类中实现上述方法:

  • 批量操作需要支持 atomic 事务(如果底层 driver 支持)
  • 元数据缓存需要计算 ETag(可用 MD5 hash)并检查 ifNoneMatch
  • 错误处理需要返回标准化的错误格式(参考 packages/spec/src/api/errors.zod.ts

3. packages/plugins/plugin-hono-server/src/hono-plugin.ts

start() 方法中注册新路由:

// Batch operations
this.server.post('/api/v1/data/:object/batch', async (req, res) => { ... });
this.server.post('/api/v1/data/:object/createMany', async (req, res) => { ... });
this.server.post('/api/v1/data/:object/updateMany', async (req, res) => { ... });
this.server.post('/api/v1/data/:object/deleteMany', async (req, res) => { ... });

// Metadata caching (enhance existing route)
this.server.get('/api/v1/meta/:type/:name', async (req, res) => {
  // Check If-None-Match header, return 304 if matches
});

// View storage
this.server.post('/api/v1/ui/views', async (req, res) => { ... });
this.server.get('/api/v1/ui/views/:id', async (req, res) => { ... });
this.server.get('/api/v1/ui/views', async (req, res) => { ... });
this.server.patch('/api/v1/ui/views/:id', async (req, res) => { ... });
this.server.delete('/api/v1/ui/views/:id', async (req, res) => { ... });

技术要求

  • 所有请求/响应需要符合 packages/spec/src/api/batch.zod.tscache.zod.tsview-storage.zod.ts 中的 schema
  • 错误响应使用 StandardErrorCodeErrorCategory(参考 errors.zod.ts
  • 批量操作的 atomic: true 模式需要支持事务回滚(如果 driver 支持)
  • ETag 生成使用内容的 MD5 hash,格式为 "<hash>"W/"<hash>"
  • View storage 需要持久化(可先用内存存储,后续扩展到数据库)

验证步骤

  1. TypeScript 编译通过
  2. 使用 @objectstack/client 测试所有新 API 端点
  3. 验证批量操作的 atomic 和 non-atomic 模式
  4. 验证 ETag 缓存(304 响应)
  5. 验证标准化错误格式

编码规范

  • 遵循 ObjectStack 命名约定:配置属性用 camelCase,机器名用 snake_case
  • 使用 Zod schema 进行请求验证
  • 添加详细的日志记录(使用 ctx.logger
  • 参考现有 CRUD 实现的错误处理模式

使用此提示词可指导 AI 或开发者完成服务端实现。

@github-actions github-actions bot added documentation Improvements or additions to documentation size/m labels Jan 30, 2026
Comment on lines +2 to +18
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

Unused import SavedView.

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.

Suggested changeset 1
packages/client/src/index.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts
--- a/packages/client/src/index.ts
+++ b/packages/client/src/index.ts
@@ -12,7 +12,6 @@
   CreateViewRequest,
   UpdateViewRequest,
   ListViewsRequest,
-  SavedView,
   ListViewsResponse,
   ViewResponse
 } from '@objectstack/spec/api';
EOF
@@ -12,7 +12,6 @@
CreateViewRequest,
UpdateViewRequest,
ListViewsRequest,
SavedView,
ListViewsResponse,
ViewResponse
} from '@objectstack/spec/api';
Copilot is powered by AI and may make mistakes. Always verify output.
@hotlong hotlong marked this pull request as ready for review January 30, 2026 11:01
Copilot AI review requested due to automatic review settings January 30, 2026 11:01
@hotlong hotlong merged commit 336145d into main Jan 30, 2026
15 checks passed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, updated updateMany/deleteMany, separated createMany endpoint)
  • 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

Comment on lines 109 to +119
### `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).
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. updateMany: Changed from (object, data, filters) returning Promise<number> to (object, records[], options?) returning Promise<BatchUpdateResponse>
  2. 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).

Copilot uses AI. Check for mistakes.
Comment on lines +155 to +156
value: cacheOptions.ifNoneMatch.replace(/^W\/|"/g, ''),
weak: cacheOptions.ifNoneMatch.startsWith('W/')
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +150 to +170
// 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,
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// 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),

Copilot uses AI. Check for mistakes.
Comment on lines +304 to +317
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();
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Adding a migration guide to the README or CHANGELOG
  2. Providing a deprecation path if backward compatibility is needed
  3. Documenting this as a breaking change in release notes
Suggested change
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;

Copilot uses AI. Check for mistakes.
Comment on lines +331 to 342
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();
}
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Adding a migration guide explaining this breaking change
  2. Documenting this as a breaking change in release notes
  3. If filter-based deletion is still needed, consider providing an alternative method or documenting the pattern of using find() followed by deleteMany()

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants