@tier0/sdk-v2 0.0.0-stage → 0.2.1
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/LICENSE +21 -0
- package/NOTICE.md +7 -0
- package/README.md +201 -2
- package/dist/cjs/app-identity-contract.generated.d.ts +22 -0
- package/dist/cjs/app-identity-contract.generated.js +51 -0
- package/dist/cjs/app-identity-contract.generated.js.map +1 -0
- package/dist/cjs/capabilities.d.ts +54 -0
- package/dist/cjs/capabilities.js +134 -0
- package/dist/cjs/capabilities.js.map +1 -0
- package/dist/cjs/client.d.ts +604 -0
- package/dist/cjs/client.js +35 -0
- package/dist/cjs/client.js.map +1 -0
- package/dist/cjs/cloud-server.d.ts +31 -0
- package/dist/cjs/cloud-server.js +114 -0
- package/dist/cjs/cloud-server.js.map +1 -0
- package/dist/cjs/cloud-transport.d.ts +67 -0
- package/dist/cjs/cloud-transport.js +272 -0
- package/dist/cjs/cloud-transport.js.map +1 -0
- package/dist/cjs/controls.d.ts +93 -0
- package/dist/cjs/controls.js +169 -0
- package/dist/cjs/controls.js.map +1 -0
- package/dist/cjs/data/bootstrap.d.ts +70 -0
- package/dist/cjs/data/bootstrap.js +291 -0
- package/dist/cjs/data/bootstrap.js.map +1 -0
- package/dist/cjs/data/call.d.ts +33 -0
- package/dist/cjs/data/call.js +216 -0
- package/dist/cjs/data/call.js.map +1 -0
- package/dist/cjs/data/client.d.ts +134 -0
- package/dist/cjs/data/client.js +185 -0
- package/dist/cjs/data/client.js.map +1 -0
- package/dist/cjs/data/collection.d.ts +335 -0
- package/dist/cjs/data/collection.js +601 -0
- package/dist/cjs/data/collection.js.map +1 -0
- package/dist/cjs/data/context.d.ts +12 -0
- package/dist/cjs/data/context.js +50 -0
- package/dist/cjs/data/context.js.map +1 -0
- package/dist/cjs/data/declarations.d.ts +11 -0
- package/dist/cjs/data/declarations.js +18 -0
- package/dist/cjs/data/declarations.js.map +1 -0
- package/dist/cjs/data/define.d.ts +77 -0
- package/dist/cjs/data/define.js +31 -0
- package/dist/cjs/data/define.js.map +1 -0
- package/dist/cjs/data/error.d.ts +78 -0
- package/dist/cjs/data/error.js +182 -0
- package/dist/cjs/data/error.js.map +1 -0
- package/dist/cjs/data/index.d.ts +24 -0
- package/dist/cjs/data/index.js +49 -0
- package/dist/cjs/data/index.js.map +1 -0
- package/dist/cjs/data/schema.d.ts +88 -0
- package/dist/cjs/data/schema.js +657 -0
- package/dist/cjs/data/schema.js.map +1 -0
- package/dist/cjs/data/sort.d.ts +8 -0
- package/dist/cjs/data/sort.js +21 -0
- package/dist/cjs/data/sort.js.map +1 -0
- package/dist/cjs/data/subscribe.d.ts +108 -0
- package/dist/cjs/data/subscribe.js +270 -0
- package/dist/cjs/data/subscribe.js.map +1 -0
- package/dist/cjs/files.d.ts +67 -0
- package/dist/cjs/files.js +119 -0
- package/dist/cjs/files.js.map +1 -0
- package/dist/cjs/framework-scope.d.ts +1 -0
- package/dist/cjs/framework-scope.js +14 -0
- package/dist/cjs/framework-scope.js.map +1 -0
- package/dist/cjs/index.d.ts +14 -0
- package/dist/cjs/index.js +33 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/mq/broker.d.ts +65 -0
- package/dist/cjs/mq/broker.js +115 -0
- package/dist/cjs/mq/broker.js.map +1 -0
- package/dist/cjs/mq/client.d.ts +37 -0
- package/dist/cjs/mq/client.js +321 -0
- package/dist/cjs/mq/client.js.map +1 -0
- package/dist/cjs/mq/index.d.ts +5 -0
- package/dist/cjs/mq/index.js +11 -0
- package/dist/cjs/mq/index.js.map +1 -0
- package/dist/cjs/mq/types.d.ts +48 -0
- package/dist/cjs/mq/types.js +3 -0
- package/dist/cjs/mq/types.js.map +1 -0
- package/dist/cjs/openapi/api.generated.d.ts +820 -0
- package/dist/cjs/openapi/api.generated.js +607 -0
- package/dist/cjs/openapi/api.generated.js.map +1 -0
- package/dist/cjs/openapi/client.d.ts +56 -0
- package/dist/cjs/openapi/client.js +219 -0
- package/dist/cjs/openapi/client.js.map +1 -0
- package/dist/cjs/openapi/codecs.generated.d.ts +2234 -0
- package/dist/cjs/openapi/codecs.generated.js +6 -0
- package/dist/cjs/openapi/codecs.generated.js.map +1 -0
- package/dist/cjs/openapi/index.d.ts +5 -0
- package/dist/cjs/openapi/index.js +12 -0
- package/dist/cjs/openapi/index.js.map +1 -0
- package/dist/cjs/openapi/types.generated.d.ts +3227 -0
- package/dist/cjs/openapi/types.generated.js +3 -0
- package/dist/cjs/openapi/types.generated.js.map +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/react.d.ts +9 -0
- package/dist/cjs/react.js +16 -0
- package/dist/cjs/react.js.map +1 -0
- package/dist/cjs/runtime-context.d.ts +9 -0
- package/dist/cjs/runtime-context.js +25 -0
- package/dist/cjs/runtime-context.js.map +1 -0
- package/dist/cjs/runtime-env.d.ts +1 -0
- package/dist/cjs/runtime-env.js +18 -0
- package/dist/cjs/runtime-env.js.map +1 -0
- package/dist/cjs/source.d.ts +39 -0
- package/dist/cjs/source.js +29 -0
- package/dist/cjs/source.js.map +1 -0
- package/dist/cjs/stream.d.ts +40 -0
- package/dist/cjs/stream.js +33 -0
- package/dist/cjs/stream.js.map +1 -0
- package/dist/cjs/uns-v2/client.d.ts +132 -0
- package/dist/cjs/uns-v2/client.js +352 -0
- package/dist/cjs/uns-v2/client.js.map +1 -0
- package/dist/cjs/uns-v2/codec.d.ts +11 -0
- package/dist/cjs/uns-v2/codec.js +290 -0
- package/dist/cjs/uns-v2/codec.js.map +1 -0
- package/dist/cjs/uns-v2/data-client.d.ts +10 -0
- package/dist/cjs/uns-v2/data-client.js +18 -0
- package/dist/cjs/uns-v2/data-client.js.map +1 -0
- package/dist/cjs/uns-v2/generated.d.ts +18178 -0
- package/dist/cjs/uns-v2/generated.js +144 -0
- package/dist/cjs/uns-v2/generated.js.map +1 -0
- package/dist/cjs/uns-v2/index.d.ts +5 -0
- package/dist/cjs/uns-v2/index.js +24 -0
- package/dist/cjs/uns-v2/index.js.map +1 -0
- package/dist/cjs/uns-v2/sse.d.ts +20 -0
- package/dist/cjs/uns-v2/sse.js +65 -0
- package/dist/cjs/uns-v2/sse.js.map +1 -0
- package/dist/cjs/vue.d.ts +9 -0
- package/dist/cjs/vue.js +39 -0
- package/dist/cjs/vue.js.map +1 -0
- package/dist/esm/app-identity-contract.generated.d.ts +22 -0
- package/dist/esm/app-identity-contract.generated.js +48 -0
- package/dist/esm/app-identity-contract.generated.js.map +1 -0
- package/dist/esm/capabilities.d.ts +54 -0
- package/dist/esm/capabilities.js +129 -0
- package/dist/esm/capabilities.js.map +1 -0
- package/dist/esm/client.d.ts +604 -0
- package/dist/esm/client.js +32 -0
- package/dist/esm/client.js.map +1 -0
- package/dist/esm/cloud-server.d.ts +31 -0
- package/dist/esm/cloud-server.js +109 -0
- package/dist/esm/cloud-server.js.map +1 -0
- package/dist/esm/cloud-transport.d.ts +67 -0
- package/dist/esm/cloud-transport.js +267 -0
- package/dist/esm/cloud-transport.js.map +1 -0
- package/dist/esm/controls.d.ts +93 -0
- package/dist/esm/controls.js +163 -0
- package/dist/esm/controls.js.map +1 -0
- package/dist/esm/data/bootstrap.d.ts +70 -0
- package/dist/esm/data/bootstrap.js +286 -0
- package/dist/esm/data/bootstrap.js.map +1 -0
- package/dist/esm/data/call.d.ts +33 -0
- package/dist/esm/data/call.js +211 -0
- package/dist/esm/data/call.js.map +1 -0
- package/dist/esm/data/client.d.ts +134 -0
- package/dist/esm/data/client.js +181 -0
- package/dist/esm/data/client.js.map +1 -0
- package/dist/esm/data/collection.d.ts +335 -0
- package/dist/esm/data/collection.js +594 -0
- package/dist/esm/data/collection.js.map +1 -0
- package/dist/esm/data/context.d.ts +12 -0
- package/dist/esm/data/context.js +44 -0
- package/dist/esm/data/context.js.map +1 -0
- package/dist/esm/data/declarations.d.ts +11 -0
- package/dist/esm/data/declarations.js +10 -0
- package/dist/esm/data/declarations.js.map +1 -0
- package/dist/esm/data/define.d.ts +77 -0
- package/dist/esm/data/define.js +27 -0
- package/dist/esm/data/define.js.map +1 -0
- package/dist/esm/data/error.d.ts +78 -0
- package/dist/esm/data/error.js +176 -0
- package/dist/esm/data/error.js.map +1 -0
- package/dist/esm/data/index.d.ts +24 -0
- package/dist/esm/data/index.js +20 -0
- package/dist/esm/data/index.js.map +1 -0
- package/dist/esm/data/schema.d.ts +88 -0
- package/dist/esm/data/schema.js +646 -0
- package/dist/esm/data/schema.js.map +1 -0
- package/dist/esm/data/sort.d.ts +8 -0
- package/dist/esm/data/sort.js +18 -0
- package/dist/esm/data/sort.js.map +1 -0
- package/dist/esm/data/subscribe.d.ts +108 -0
- package/dist/esm/data/subscribe.js +265 -0
- package/dist/esm/data/subscribe.js.map +1 -0
- package/dist/esm/files.d.ts +67 -0
- package/dist/esm/files.js +113 -0
- package/dist/esm/files.js.map +1 -0
- package/dist/esm/framework-scope.d.ts +1 -0
- package/dist/esm/framework-scope.js +11 -0
- package/dist/esm/framework-scope.js.map +1 -0
- package/dist/esm/index.d.ts +14 -0
- package/dist/esm/index.js +10 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/mq/broker.d.ts +65 -0
- package/dist/esm/mq/broker.js +109 -0
- package/dist/esm/mq/broker.js.map +1 -0
- package/dist/esm/mq/client.d.ts +37 -0
- package/dist/esm/mq/client.js +314 -0
- package/dist/esm/mq/client.js.map +1 -0
- package/dist/esm/mq/index.d.ts +5 -0
- package/dist/esm/mq/index.js +3 -0
- package/dist/esm/mq/index.js.map +1 -0
- package/dist/esm/mq/types.d.ts +48 -0
- package/dist/esm/mq/types.js +2 -0
- package/dist/esm/mq/types.js.map +1 -0
- package/dist/esm/openapi/api.generated.d.ts +820 -0
- package/dist/esm/openapi/api.generated.js +603 -0
- package/dist/esm/openapi/api.generated.js.map +1 -0
- package/dist/esm/openapi/client.d.ts +56 -0
- package/dist/esm/openapi/client.js +210 -0
- package/dist/esm/openapi/client.js.map +1 -0
- package/dist/esm/openapi/codecs.generated.d.ts +2234 -0
- package/dist/esm/openapi/codecs.generated.js +3 -0
- package/dist/esm/openapi/codecs.generated.js.map +1 -0
- package/dist/esm/openapi/index.d.ts +5 -0
- package/dist/esm/openapi/index.js +3 -0
- package/dist/esm/openapi/index.js.map +1 -0
- package/dist/esm/openapi/types.generated.d.ts +3227 -0
- package/dist/esm/openapi/types.generated.js +2 -0
- package/dist/esm/openapi/types.generated.js.map +1 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/react.d.ts +9 -0
- package/dist/esm/react.js +12 -0
- package/dist/esm/react.js.map +1 -0
- package/dist/esm/runtime-context.d.ts +9 -0
- package/dist/esm/runtime-context.js +22 -0
- package/dist/esm/runtime-context.js.map +1 -0
- package/dist/esm/runtime-env.d.ts +1 -0
- package/dist/esm/runtime-env.js +15 -0
- package/dist/esm/runtime-env.js.map +1 -0
- package/dist/esm/source.d.ts +39 -0
- package/dist/esm/source.js +25 -0
- package/dist/esm/source.js.map +1 -0
- package/dist/esm/stream.d.ts +40 -0
- package/dist/esm/stream.js +29 -0
- package/dist/esm/stream.js.map +1 -0
- package/dist/esm/uns-v2/client.d.ts +132 -0
- package/dist/esm/uns-v2/client.js +343 -0
- package/dist/esm/uns-v2/client.js.map +1 -0
- package/dist/esm/uns-v2/codec.d.ts +11 -0
- package/dist/esm/uns-v2/codec.js +284 -0
- package/dist/esm/uns-v2/codec.js.map +1 -0
- package/dist/esm/uns-v2/data-client.d.ts +10 -0
- package/dist/esm/uns-v2/data-client.js +14 -0
- package/dist/esm/uns-v2/data-client.js.map +1 -0
- package/dist/esm/uns-v2/generated.d.ts +18178 -0
- package/dist/esm/uns-v2/generated.js +141 -0
- package/dist/esm/uns-v2/generated.js.map +1 -0
- package/dist/esm/uns-v2/index.d.ts +5 -0
- package/dist/esm/uns-v2/index.js +5 -0
- package/dist/esm/uns-v2/index.js.map +1 -0
- package/dist/esm/uns-v2/sse.d.ts +20 -0
- package/dist/esm/uns-v2/sse.js +60 -0
- package/dist/esm/uns-v2/sse.js.map +1 -0
- package/dist/esm/vue.d.ts +9 -0
- package/dist/esm/vue.js +36 -0
- package/dist/esm/vue.js.map +1 -0
- package/docs/acceptance-2026-10-22.md +88 -0
- package/package.json +243 -4
- package/skills/tier0-sdk-v2/SKILL.md +66 -0
- package/skills/tier0-sdk-v2/references/integration.md +72 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-agent/SKILL.md +73 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-agent/references/results.md +45 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-controls/SKILL.md +63 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-data/SKILL.md +58 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-data/references/consistency.md +70 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-data/references/errors.md +82 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-data/references/modeling.md +113 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-data/references/read-write.md +168 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-data/references/schema-evolution.md +36 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-files/SKILL.md +85 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-files/references/delete.md +22 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-files/references/download.md +49 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-files/references/multipart.md +42 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-files/references/upload.md +27 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-files/references/url.md +22 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-members/SKILL.md +56 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-members/references/app-members.md +41 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-members/references/platform-members.md +56 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-mq/SKILL.md +78 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-mq/references/quickstart.md +69 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-notifications/SKILL.md +42 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-notifications/references/get.md +34 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-notifications/references/send.md +72 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-source/SKILL.md +38 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-source/references/changes.md +40 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-source/references/create.md +63 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-source/references/delete.md +47 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-source/references/protocols.md +36 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-source/references/read.md +34 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-source/references/state.md +37 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-source/references/update.md +62 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-stream/SKILL.md +43 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-stream/references/create.md +56 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-stream/references/delete.md +35 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-stream/references/deploy.md +64 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-stream/references/lifecycle.md +26 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-stream/references/read.md +35 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-stream/references/update.md +36 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-system/SKILL.md +64 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-system/references/info.md +23 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-system/references/whoami.md +21 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-uns/SKILL.md +55 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-uns/references/create.md +56 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-uns/references/data-workflows.md +69 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-uns/references/delete.md +53 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-uns/references/namespace.md +38 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-uns/references/read.md +34 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-uns/references/restore.md +39 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-uns/references/update.md +62 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# 读写
|
|
2
|
+
|
|
3
|
+
`data.collection(declaration)` 返回按声明推断类型的句柄(也可用 alias 字符串,类型由调用方指定)。读到的记录是扁平文档 `{ _id, ...字段, _meta }`;过滤、排序、投影都用同样的路径(`status`、`customer.id`、`_id`、`_meta.updatedAt`),不加 `data.` 前缀。
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { createDataClient, defineCollection } from '@tier0/sdk-v2/data';
|
|
8
|
+
|
|
9
|
+
const workOrders = defineCollection({
|
|
10
|
+
alias: 'work_orders',
|
|
11
|
+
schema: z.object({
|
|
12
|
+
code: z.string(),
|
|
13
|
+
status: z.enum(['DRAFT', 'RELEASED', 'DONE']),
|
|
14
|
+
line: z.object({ id: z.string(), name: z.string() }),
|
|
15
|
+
targetQty: z.number().int().min(1),
|
|
16
|
+
notes: z.string().optional(),
|
|
17
|
+
}),
|
|
18
|
+
indexes: [{ path: 'code', unique: true }, { path: 'status' }],
|
|
19
|
+
});
|
|
20
|
+
const data = createDataClient({ collections: [workOrders] });
|
|
21
|
+
const orders = data.collection(workOrders);
|
|
22
|
+
|
|
23
|
+
export async function dashboard(page: number) {
|
|
24
|
+
const recent = await orders.find({ status: { $in: ['RELEASED', 'DONE'] } }, { sort: { '_meta.createdAt': -1 }, limit: 50 }).toArray();
|
|
25
|
+
// 一页 + 总数是一次请求;total.capped 为 true 时显示成 "10000+"。
|
|
26
|
+
const listPage = await orders.findPage({ status: 'RELEASED' }, { sort: { code: 1 }, page, pageSize: 20 });
|
|
27
|
+
// 每个状态的数量:一次 $group,不要按状态逐个 countDocuments。
|
|
28
|
+
const byStatus = await orders.countBy('status');
|
|
29
|
+
const totals = await orders
|
|
30
|
+
.aggregate<{ _id: string; total: number }>([{ $match: { status: 'DONE' } }, { $group: { _id: '$line.id', total: { $sum: '$targetQty' } } }])
|
|
31
|
+
.toArray();
|
|
32
|
+
return { recent, listPage, byStatus, totals };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 角标只需要知道“有没有超过 99 条”。 */
|
|
36
|
+
export async function releasedBadge() {
|
|
37
|
+
const { count, capped } = await orders.countDocuments({ status: 'RELEASED' }, { maxCount: 99 });
|
|
38
|
+
return capped ? '99+' : String(count);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function addNote(id: string, notes: string, revision: number) {
|
|
42
|
+
return orders.updateOne({ _id: id }, { $set: { notes } }, { expectedRevision: revision });
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## 一个页面一次请求:`data.queryBatch`
|
|
47
|
+
|
|
48
|
+
页面要同时显示列表、总数、各状态数量、某条记录时,用 `data.queryBatch` 把这些读取合成一次请求(最多 20 个),结果来自同一个快照,不要分别发多个请求:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { z } from 'zod';
|
|
52
|
+
import { createDataClient, defineCollection } from '@tier0/sdk-v2/data';
|
|
53
|
+
|
|
54
|
+
const workOrders = defineCollection({
|
|
55
|
+
alias: 'work_orders',
|
|
56
|
+
schema: z.object({ code: z.string(), status: z.enum(['DRAFT', 'RELEASED', 'DONE']) }),
|
|
57
|
+
indexes: [{ path: 'code', unique: true }, { path: 'status' }],
|
|
58
|
+
});
|
|
59
|
+
const data = createDataClient({ collections: [workOrders] });
|
|
60
|
+
const orders = data.collection(workOrders);
|
|
61
|
+
|
|
62
|
+
export async function ordersScreen(page: number, selectedId: string) {
|
|
63
|
+
const { list, byStatus, selected } = await data.queryBatch({
|
|
64
|
+
list: orders.query.findPage({ status: { $ne: 'DONE' } }, { sort: { code: 1 }, page, pageSize: 20 }),
|
|
65
|
+
byStatus: orders.query.countBy('status'),
|
|
66
|
+
selected: orders.query.get([selectedId]),
|
|
67
|
+
});
|
|
68
|
+
return { list, byStatus, selected: selected.items[0] ?? null };
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
- 读取用 `collection.query.*` 构造:`find`(首页,可带 `maxCount` 附带总数)、`findPage`、`count`、`countBy`、`distinct`、`aggregate`、`get`;每项只返回一页(最多 200 行),`get` 最多 100 个 ID。
|
|
73
|
+
- 结果按名字返回,类型与对应方法一致。名字只能用字母、数字、`_` `.` `-`。
|
|
74
|
+
- 某一项被拒(过滤条件不合法、无授权)时整个调用抛出该项的 `DataError`;传 `{ settle: true }` 则每项返回 `{ ok: true, value }` 或 `{ ok: false, error }`。语句超时或整批超过 2000 行 / 8 MiB 时整批失败。
|
|
75
|
+
|
|
76
|
+
## 方法
|
|
77
|
+
|
|
78
|
+
| 读取 | 说明 |
|
|
79
|
+
| --- | --- |
|
|
80
|
+
| `find(filter, { sort, limit, projection })` | 游标,每页最多 200 条;列表读取始终给 `limit` |
|
|
81
|
+
| `findOne(filter)` | 无匹配返回 `null` |
|
|
82
|
+
| `get(ids, { projection })` | 按 `_id` 读取,返回 `{ items, missing }`,`items` 按请求顺序(重复的 ID 只取第一次出现);不存在、已删除或员工无权查看的 ID 一律进 `missing`。超过 100 个自动分批 |
|
|
83
|
+
| `findPage(filter, { sort, page, pageSize, maxCount })` | 页码分页,一次请求返回 `{ items, total: { count, capped }, page, pageSize }`;pageSize 最多 200,总数默认数到 10000(`maxCount` 最多 100000),超过时 `capped: true` |
|
|
84
|
+
| `countDocuments(filter, { maxCount })` | 返回 `{ count, capped }`,默认数到 100000;只要阈值时给小的 `maxCount`(如角标 `99+`) |
|
|
85
|
+
| `countBy(field, filter)` | 每个取值的数量 `[{ value, count }]`,从多到少;一次 `$group` |
|
|
86
|
+
| `distinct(field, filter)` | 会取完所有页 |
|
|
87
|
+
| `aggregate(pipeline)` | `$match` `$sort` `$group` `$project` `$set` `$unwind` `$skip` `$limit` `$count` `$lookup`;`$lookup.from` 写声明或 alias,数据层换成集合 ID。`$exists` / `$distinct` / `$frequencies` 阶段不执行,改用 `countBy`、`distinct` 或 `$group` |
|
|
88
|
+
|
|
89
|
+
| 写入 | 说明 |
|
|
90
|
+
| --- | --- |
|
|
91
|
+
| `insertOne(doc)`、`insertMany(docs)` | 可带 `_id`;`insertMany` 最多 200 条、原子 |
|
|
92
|
+
| `updateOne` / `updateMany` / `replaceOne` | 更新用操作符(`$set` `$inc` `$push` `$unset`…);`replaceOne` 会丢掉其他代码版本写入的字段 |
|
|
93
|
+
| `deleteOne` / `deleteMany` | 软删除,App Key 需要 `uns.delete` |
|
|
94
|
+
| `findOneAndUpdate` / `findOneAndReplace` / `findOneAndDelete` | 原子读改写,默认返回更新后的文档 |
|
|
95
|
+
| `bulkWrite(ops)` | `insertOne` / `updateOne` / `replaceOne` / `deleteOne` 混合,最多 200 项、原子 |
|
|
96
|
+
| `data.transact(steps)` | 跨集合原子写,见 [一致性](consistency.md) |
|
|
97
|
+
|
|
98
|
+
## 批量与原子性
|
|
99
|
+
|
|
100
|
+
| 调用 | 上限 | 原子 |
|
|
101
|
+
| --- | --- | --- |
|
|
102
|
+
| `insertMany`、`bulkWrite`、`data.transact` | 每次 ≤200 项,超过在发送前抛 `BATCH_LIMIT_EXCEEDED` | 是,全部或全不 |
|
|
103
|
+
| `updateMany`、`deleteMany` | 无上限,翻页处理所有匹配 | 否,逐条提交;已有提交后失败时错误保留原 `code`,`execution: 'committed'`、`retry.safe: false`,`details` 是累计结果(`matchedCount` 等、`status: 'partiallyCommitted'`、续做位置 `resumeAfter`) |
|
|
104
|
+
|
|
105
|
+
超过 200 条按 200 分块写入(循环分块即可),每块是一次独立的原子调用:数据层自动给每次调用生成幂等键并在重试中复用,不需要手写;要让“某块写过就不再写”跨进程重启也成立,用固定 `_id`(或按业务意图显式传 `idempotencyKey`)。多条文档必须一起成功或失败时,用一次 `data.transact`(≤200 步),不要用 `updateMany`。不要在循环里逐条调用数据层:读用一次 `get` / `$in` 或 `queryBatch`,写用 `insertMany` / `updateMany` / `bulkWrite` 或 `data.transact`。
|
|
106
|
+
|
|
107
|
+
### 非原子批量中途失败
|
|
108
|
+
|
|
109
|
+
`updateMany` / `deleteMany` 已经提交了一部分后失败时(`execution === 'committed'`),不要整体重做:`$inc`、`$push` 这类更新会把已提交的记录再改一次。把 `details.resumeAfter` 传回去,从失败处继续:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import { DataError, type Collection } from '@tier0/sdk-v2/data';
|
|
113
|
+
|
|
114
|
+
export async function bumpAll(items: Collection<{ status: string; views: number }>) {
|
|
115
|
+
let resumeAfter: string | undefined;
|
|
116
|
+
for (let round = 0; round < 3; round += 1) {
|
|
117
|
+
try {
|
|
118
|
+
return await items.updateMany({ status: 'OPEN' }, { $inc: { views: 1 } }, { resumeAfter });
|
|
119
|
+
} catch (error) {
|
|
120
|
+
// 只续做已证明部分提交、且给了续做位置的失败;结局未知的那一页要先核对。
|
|
121
|
+
const next = error instanceof DataError && error.execution === 'committed' ? error.details.resumeAfter : undefined;
|
|
122
|
+
if (typeof next !== 'string') throw error;
|
|
123
|
+
resumeAfter = next;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
throw new Error('updateMany kept failing');
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`execution === 'unknown'` 时失败的那一页可能已经部分生效,续做前先核对。
|
|
131
|
+
|
|
132
|
+
## 过滤
|
|
133
|
+
|
|
134
|
+
支持 `$eq $ne $gt $gte $lt $lte $in $nin $exists $all $size $elemMatch $not $and $or $nor` 和 `$contains`。`$contains` 是子串匹配(1–256 字节,无通配符和正则),只对 ASCII 字母忽略大小写;缺失、null、非字符串值不匹配;只能用于声明为字符串的字段,用在数组或数字字段上返回 `INVALID_REQUEST`。它不能使用索引,大集合上可能触发 10 秒执行超时(`QUERY_TIMEOUT`),要和有索引的条件一起用。`$in` 最多 200 个值,排序最多 4 个字段(`toSortPairs` 也导出给自定义查询转换使用)。
|
|
135
|
+
|
|
136
|
+
## 校验
|
|
137
|
+
|
|
138
|
+
写入前按声明严格校验:未知字段、类型错误、未声明的更新路径抛 `ZodError`(HTTP 400 语义)。读取不严格解析:记录可能含其他代码版本写入的字段、新的枚举值,或缺少后来新增的字段。枚举渲染要有兜底标签,后加的字段按可能缺失处理。
|
|
139
|
+
|
|
140
|
+
## 订阅变更
|
|
141
|
+
|
|
142
|
+
`data.subscribe(...)` 返回已提交变更的异步迭代器(UNS `records:subscribe`,服务端推送):断线、超时、服务端结束都自动续传(`Last-Event-ID`),刷新页面不需要轮询。事件只有标识,不带业务数据,收到后按需重新读取。
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
import { z } from 'zod';
|
|
146
|
+
import { createDataClient, defineCollection, runAsApp, type SubscriptionEvent } from '@tier0/sdk-v2/data';
|
|
147
|
+
|
|
148
|
+
const tickets = defineCollection({ alias: 'tickets', schema: z.object({ title: z.string(), status: z.enum(['OPEN', 'DONE']) }) });
|
|
149
|
+
const data = createDataClient({ collections: [tickets] });
|
|
150
|
+
|
|
151
|
+
/** 一个进程一条 App 级订阅,在进程内扇出给浏览器。 */
|
|
152
|
+
export async function pumpChanges(send: (event: SubscriptionEvent) => void, signal: AbortSignal) {
|
|
153
|
+
await runAsApp(async () => {
|
|
154
|
+
for await (const event of data.subscribe({ collections: [tickets], signal })) {
|
|
155
|
+
// reset:续传位置失效或访问范围变化,浏览器应重新读取当前数据。
|
|
156
|
+
send(event);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
- 事件:`{ type: 'change', eventId, collection(alias), collectionId, change: 'record' | 'delete', recordId, revision, occurredAt, restricted }`、集合级通知 `{ type: 'change', …, change: 'collection', restricted: true }`(没有 `recordId` / `revision`),或 `{ type: 'reset', reason, message? }`。心跳与重连在 SDK 内部处理;迭代抛出 `DataError` 表示不能再重试(如 403),正常结束只有 `signal` 中止或跳出循环。
|
|
163
|
+
- 事件按写入事务的顺序到达,不是提交顺序,同一记录的 revision 可能乱序。SDK 已丢弃 revision 不大于该记录已投递 revision 的非 `delete` 变更;`delete` 一律投递(迟到的也投递),并清除该记录的 revision(同一 ID 再写入时可能从 1 开始)。收到事件后按 `recordId` 重新读取,不要用事件顺序推断记录的最终状态。
|
|
164
|
+
- `reset`:`CURSOR_EXPIRED`(续传位置已被清理、游标无法解开或租户库已迁移)或 `SCOPE_CHANGED`(访问范围变化,例如集合不再可读)。重新读取当前数据;SDK 随后不带续传位置重新订阅。
|
|
165
|
+
- `restricted: true`:该集合有员工数据规则。以 App 身份打开的订阅能看到所有记录的变更,所以默认把这类变更换成集合级通知,可以直接转发给任何员工的浏览器,浏览器再通过受规则约束的接口重新读取。只有不转发给浏览器的服务端逻辑才传 `restricted: 'record'` 拿完整事件(转发前仍须逐个经 `redactRestricted`)。
|
|
166
|
+
- 默认订阅全部已声明的集合(最多 64 个);`recordIds`(最多 100 个)只能配一个集合。
|
|
167
|
+
- 订阅不占数据请求的并发名额,但每个租户的订阅数有上限(超出时 SDK 按 429 自动退避重连):一个服务端进程只开一条订阅,在进程内扇出,不要每个浏览器连接开一条。App 刚导入还没就绪(`APP_NOT_READY`)时订阅会一直等待,不会结束。
|
|
168
|
+
- 变更保留 24 小时;`lastEventId` 太旧时收到 `reset`。`lastEventId` 必须是收到过的事件 ID(可见 ASCII),否则调用时直接抛 `TypeError`。
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Schema 演进
|
|
2
|
+
|
|
3
|
+
Preview 与部署后的 App 共用集合,较旧的部署版本随时可能用它的旧声明冷启动。因此 ensure 只会**放宽**:存储是所有运行过的声明的并集,每个版本的写入都保持有效。按变更类型选择做法:
|
|
4
|
+
|
|
5
|
+
| 变更 | 做法 |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| 新增可选字段(任意层级,含数组元素内)、新增枚举值、字段改为可选、`int` → `number`、放宽或去掉 `min`/`max`、新增索引 | 修改声明;下次访问时 ensure 生效 |
|
|
8
|
+
| 修改 `.describe()` 文本或集合 `description` | 修改声明,然后通过 tier0 MCP 执行 `schema:replace`,请求体用 `compileCollection(declaration)` 的结果(先 `dryRun`:期望 `widened`;`narrowed` 说明存储里还有该声明之外的内容,例如其他版本的字段——合并进请求体或停止)。ensure 保留已存储的描述并报告 `ANNOTATION_NOT_APPLIED`;新字段和新集合在创建时带上描述 |
|
|
9
|
+
| 新增每条记录都必须有的字段 | 先以 `.optional()` 加入,所有写入路径都写它,回填旧记录,等部署版本也写入后再用 `schema:replace` 改为必填 |
|
|
10
|
+
| 删除或重命名字段 | 代码不再使用旧字段(它可以留在存储里);新名字就是新的可选字段。通过 tier0 MCP 一次性回填(`records:updateMany` 配 `$rename` / `$set`) |
|
|
11
|
+
| 修改字段类型 | 换一个新字段名,回填,读取时以旧字段兜底。ensure 从不转换类型(`SCHEMA_CONFLICT`) |
|
|
12
|
+
| 删除枚举值、可选改必填、从存储删除字段、删除索引、收紧 `min`/`max` | 只能在部署版本不再写旧形态之后:通过 tier0 MCP `schema:replace`,先 `dryRun` |
|
|
13
|
+
|
|
14
|
+
不要为了“重置” Schema 而 `deleteMany` 或重建集合:部署版本的数据会一起丢失。修改 `seed` 不会重新写种子。
|
|
15
|
+
|
|
16
|
+
## 运行时代码与 worker 操作的边界
|
|
17
|
+
|
|
18
|
+
`schema:replace`、`archive`、`restore` 由 worker 通过 tier0 MCP 显式执行(`/uns/app/collections/schema:replace`、`archive`、`restore`),App 运行时代码不调用。数据层导出的 `compileCollection(declaration)` 给出与运行时 ensure 完全一致的条目 `{ alias, recordSchema, indexes, description? }`,可作为 ensure `dryRun` 或 `schema:replace` 请求体的基础。
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { z } from 'zod';
|
|
22
|
+
import { compileCollection, defineCollection } from '@tier0/sdk-v2/data';
|
|
23
|
+
|
|
24
|
+
const tickets = defineCollection({
|
|
25
|
+
alias: 'tickets',
|
|
26
|
+
schema: z.object({ title: z.string().describe('Short summary'), severity: z.enum(['LOW', 'HIGH']).optional() }),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/** schema:replace 请求体的基础;expectedRevision 取自 ensure 结果的集合 revision。 */
|
|
30
|
+
export function replaceBody(expectedRevision: number) {
|
|
31
|
+
const { alias, recordSchema, indexes } = compileCollection(tickets);
|
|
32
|
+
return { alias, recordSchema, indexes, expectedRevision, dryRun: true };
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Schema 中 `ignored`、`warnings`、`conflicts`、`violations` 的路径用 `.` 连接属性,用 `[]` 表示数组元素(`lines.[].qty`)。
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tier0-sdk-v2-files
|
|
3
|
+
description: 使用 @tier0/sdk-v2 实现 Tier0 应用附件、头像、导入导出、生成报告、上传下载和持久化文件引用,包括分片上传及中断处理。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 文件与附件
|
|
7
|
+
|
|
8
|
+
## 按任务读取操作文档
|
|
9
|
+
|
|
10
|
+
| 任务 | 参考 |
|
|
11
|
+
| --- | --- |
|
|
12
|
+
| 上传并保存 filePath | [上传](references/upload.md) |
|
|
13
|
+
| 大文件、进度、中断会话 | [分片上传](references/multipart.md) |
|
|
14
|
+
| 图片预览、临时访问地址 | [访问 URL](references/url.md) |
|
|
15
|
+
| 服务端转发、浏览器保存附件 | [下载](references/download.md) |
|
|
16
|
+
| 删除对象及核验回执 | [删除](references/delete.md) |
|
|
17
|
+
|
|
18
|
+
沿用 v1 的业务流程,调用已适配 v2;不提供旧版自动续传/并发助手。
|
|
19
|
+
|
|
20
|
+
业务文件使用平台对象存储,数据库保存返回的 filePath。App 本地目录可用于临时处理和构建,但不能替代持久化上传。使用服务端 client.files,API Key 不下发给浏览器或对象存储。
|
|
21
|
+
|
|
22
|
+
## 上传并保存引用
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { createClient } from '@tier0/sdk-v2';
|
|
26
|
+
|
|
27
|
+
export async function uploadReport(text: string) {
|
|
28
|
+
const client = createClient();
|
|
29
|
+
const file = new File([text], 'report.txt', { type: 'text/plain;charset=utf-8' });
|
|
30
|
+
const saved = await client.files.upload(file);
|
|
31
|
+
// 将 filePath 保存到业务记录;临时 URL 到期后重新获取。
|
|
32
|
+
return { filePath: saved.filePath };
|
|
33
|
+
}
|
|
34
|
+
export async function downloadFile(filePath: string) {
|
|
35
|
+
const response = await createClient().files.download({ filePath });
|
|
36
|
+
return response.arrayBuffer();
|
|
37
|
+
}
|
|
38
|
+
export async function getTemporaryLink(filePath: string) {
|
|
39
|
+
return createClient().files.url({ filePath, expiredSec: 300 });
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
upload 接收 Web File;Node 20.19+ 可直接构造。若文件来自磁盘,先读取内容构造 File,别把本地路径字符串当文件参数。大文件避免全部读入内存后重复复制;按业务运行环境决定流转方式。
|
|
44
|
+
|
|
45
|
+
## 上传协议与参数
|
|
46
|
+
|
|
47
|
+
- `upload(file, options)` 自动填写 fileName/contentType/size;options 支持平台定义的 business/useBy/visibility/sessionId/appInstanceId 与 signal,不猜测这些字段的合法业务枚举。
|
|
48
|
+
- 普通上传先向平台申请 postUrl/postFields,再向对象存储提交表单,file 必须排在签名字段之后。SDK 已处理,不能手工附带平台 Authorization。
|
|
49
|
+
- filePath 为持久化引用;私有文件的 fileUrl 为临时地址,公开文件可能返回公开 URL。发通知或生成页面时,按当前权限请求访问 URL。
|
|
50
|
+
- 下载通过平台申请签名地址后匿名读取,使用 `client.files.download`,不要手动跟随携带 API Key 的下载重定向。
|
|
51
|
+
|
|
52
|
+
## 分片上传
|
|
53
|
+
|
|
54
|
+
超过 100 MiB 自动分片,也可显式调用:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { createClient, MultipartUploadError } from '@tier0/sdk-v2';
|
|
58
|
+
|
|
59
|
+
export async function uploadLargeFile(file: File, signal?: AbortSignal) {
|
|
60
|
+
try {
|
|
61
|
+
return await createClient().files.uploadMultipart(file, {
|
|
62
|
+
partSize: 8 * 1024 * 1024, signal,
|
|
63
|
+
onProgress: ({ uploadedBytes, totalBytes }) => {
|
|
64
|
+
console.info({ uploadedBytes, totalBytes });
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (error instanceof MultipartUploadError) {
|
|
69
|
+
// 交给受控的作业存储保留 session;不要自动 complete/abort。
|
|
70
|
+
return { interrupted: true as const, session: error.session };
|
|
71
|
+
}
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
partSize 至少 5 MiB;SDK 使用服务端返回的实际分片大小,首版顺序上传,不自动重试/续传。中断 session 含 fileKey、uploadId、已成功 parts/ETag、completionAttempted;它是恢复依据,不是上传成功回执。
|
|
78
|
+
|
|
79
|
+
`files.multipart` 提供 init/partUrls/complete/abort 原子接口。继续已有分片时保留服务端 ETag;跨域直传还需要存储暴露 ETag 响应头。completionAttempted=true 时先核对最终文件是否存在,不能盲目重新完成或终止。当前 SDK 未提供自动列举远端分片的恢复助手。
|
|
80
|
+
|
|
81
|
+
## 删除与失败
|
|
82
|
+
|
|
83
|
+
`files.delete({ filePath })` 返回 `{ deleted }`,deleted=false 不能显示“已删除”。删除前按业务引用关系处理,读取/下载请求不附带删除。
|
|
84
|
+
|
|
85
|
+
平台阶段抛 ApiError/TransportError,存储阶段还可能抛 fetch/Abort 错误;不要吞掉上传失败后仍保存“成功”记录。上传/下载超时覆盖取得响应头阶段,返回 Response 的后续流由调用方消费和管理。
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# 删除文件
|
|
2
|
+
|
|
3
|
+
方法:`files.delete({filePath}, {signal}?)`;POST /openapi/v1/assets/files/delete。
|
|
4
|
+
|
|
5
|
+
| 字段 | 类型 | 必填 | 说明 |
|
|
6
|
+
| --- | --- | --- | --- |
|
|
7
|
+
| filePath | string | 是 | 业务记录中已确认目标的上传引用 |
|
|
8
|
+
| signal | AbortSignal | 否 | 第二参数;取消客户端等待 |
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { createClient, type Tier0Client } from '@tier0/sdk-v2';
|
|
12
|
+
|
|
13
|
+
export async function deleteAttachment(filePath: string, client: Tier0Client = createClient()) {
|
|
14
|
+
const result = await client.files.delete({ filePath });
|
|
15
|
+
if (!result.deleted) throw new Error('平台未确认文件已删除');
|
|
16
|
+
return { deleted: true };
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
删除前明确目标及业务引用,读取/下载不附带删除。返回已解包的 {deleted};不要复制 v1 把 204/空体也无条件视为成功的逻辑。v2 要按实际回执判断。
|
|
21
|
+
|
|
22
|
+
对象删除不提供 SDK 恢复功能。401/403 处理身份与授权;网络中断/结果未知时先对账,不把错误当“已经不存在”,不自动换身份重试。
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# 下载与应用附件保存
|
|
2
|
+
|
|
3
|
+
v2 方法:`files.download({filePath,…}, {signal}?) → Promise<Response>`。与 v1 不同,返回值本身就是 Response,不再取 result.response。
|
|
4
|
+
|
|
5
|
+
v2 先请求文件 URL,再匿名读取对象存储;不沿用带 Key 的重定向下载链。平台认证信息不能转发给存储。
|
|
6
|
+
|
|
7
|
+
## 同源服务端下载
|
|
8
|
+
|
|
9
|
+
应用路由先校验登录和业务权限,再按业务记录 ID 从数据库取可信 filePath。下例只负责已经授权后的转发,不可直接暴露为接受任意 filePath 的公共接口。
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { createClient, type Tier0Client } from '@tier0/sdk-v2';
|
|
13
|
+
|
|
14
|
+
export async function streamAuthorizedAttachment(
|
|
15
|
+
filePath: string, client: Tier0Client = createClient(), signal?: AbortSignal,
|
|
16
|
+
) {
|
|
17
|
+
const upstream = await client.files.download({ filePath }, { signal });
|
|
18
|
+
return new Response(upstream.body, {
|
|
19
|
+
headers: {
|
|
20
|
+
'Content-Type': upstream.headers.get('content-type') ?? 'application/octet-stream',
|
|
21
|
+
'Content-Disposition': 'attachment',
|
|
22
|
+
'Cache-Control': 'private, no-store',
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
大文件直接转发 response.body,避免服务端先完整 buffer。返回响应头后的流消费/取消由应用管理;SDK 请求超时不覆盖后续所有流读取。
|
|
29
|
+
|
|
30
|
+
## 浏览器保存
|
|
31
|
+
|
|
32
|
+
浏览器访问自己的业务下载路由,不持有平台 Key。下面的路由是应用需要实现的接口。
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
export async function saveAttachment(recordId: string, fileName: string) {
|
|
36
|
+
const response = await fetch('/api/attachments/' + encodeURIComponent(recordId) + '/download');
|
|
37
|
+
if (!response.ok) throw new Error('附件下载失败');
|
|
38
|
+
const url = URL.createObjectURL(await response.blob());
|
|
39
|
+
const anchor = document.createElement('a');
|
|
40
|
+
anchor.href = url;
|
|
41
|
+
anchor.download = fileName;
|
|
42
|
+
document.body.appendChild(anchor);
|
|
43
|
+
anchor.click();
|
|
44
|
+
anchor.remove();
|
|
45
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Blob 方案会占用浏览器内存;超大文件按应用支持的流式保存能力设计。不能将权限失败当空文件下载,也不能将文件不存在当成网络异常重试。
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# 分片上传、进度和中断
|
|
2
|
+
|
|
3
|
+
| 调用 | 行为 |
|
|
4
|
+
| --- | --- |
|
|
5
|
+
| files.upload(file) | 大于 100 MiB 自动分片 |
|
|
6
|
+
| files.uploadMultipart(file, options) | 非空文件显式分片,可报告进度 |
|
|
7
|
+
| files.multipart.init/partUrls/complete/abort | 原子接口,普通业务上传使用上面两个助手 |
|
|
8
|
+
|
|
9
|
+
| options 字段 | 类型 | 说明 |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| partSize | number | 默认 8 MiB,至少 5 MiB;实际采用服务器返回值 |
|
|
12
|
+
| onProgress | function | 收到 uploadedBytes、totalBytes |
|
|
13
|
+
| signal | AbortSignal | 取消等待/传输 |
|
|
14
|
+
| 其余上传元数据 | string | 与 upload 相同 |
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { createClient, MultipartUploadError, type Tier0Client } from '@tier0/sdk-v2';
|
|
18
|
+
|
|
19
|
+
export async function uploadWithProgress(
|
|
20
|
+
file: File, report: (uploadedBytes: number, totalBytes: number) => void,
|
|
21
|
+
client: Tier0Client = createClient(), signal?: AbortSignal,
|
|
22
|
+
) {
|
|
23
|
+
try {
|
|
24
|
+
const saved = await client.files.uploadMultipart(file, {
|
|
25
|
+
partSize: 8 * 1024 * 1024, signal,
|
|
26
|
+
onProgress: progress => report(progress.uploadedBytes, progress.totalBytes),
|
|
27
|
+
});
|
|
28
|
+
return { state: 'completed' as const, filePath: saved.filePath };
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (error instanceof MultipartUploadError) {
|
|
31
|
+
return { state: 'interrupted' as const, session: error.session };
|
|
32
|
+
}
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
成功后才持久化 filePath。中断 session 包含 fileKey、uploadId、parts(partNumber/etag)、completionAttempted;session 不是完成回执,不公开到日志或无关用户。
|
|
39
|
+
|
|
40
|
+
v2 首版顺序上传,无自动重试、并发参数或一键续传。不要复制 v1 的 resumeMultipartUpload、multipartSession 等不存在的助手。部分 PUT 可能成功但回执丢失,不能仅凭本地 parts 判断远端完整状态。跨域存储需要暴露 ETag 响应头。
|
|
41
|
+
|
|
42
|
+
completionAttempted=true 表示 complete 可能已提交,先对账最终文件,不能直接 abort 或重复 complete。明确放弃未完成会话时才调用 abort;初始化请求本身结果未知且无 session 时,保存错误信息交由平台对账,不猜 uploadId。
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# 上传并保存文件引用
|
|
2
|
+
|
|
3
|
+
从 v1 的 uploadFile 流程迁移,v2 使用 `client.files.upload(file, options)`;独立入口为 `createFilesClient`(@tier0/sdk-v2/files)。Node.js 20.19+。浏览器只调用应用自己的上传接口,平台 Key 留在服务端。
|
|
4
|
+
|
|
5
|
+
| 参数 | 类型 | 必填 | 说明 |
|
|
6
|
+
| --- | --- | --- | --- |
|
|
7
|
+
| file | File | 是 | 名称、MIME、大小从 File 读取;不是磁盘路径字符串 |
|
|
8
|
+
| options.business / useBy | string | 否 | 平台定义的业务归属,不猜枚举 |
|
|
9
|
+
| options.visibility | string | 否 | 根据业务授权选择公开或私有,不自动公开 |
|
|
10
|
+
| options.sessionId / appInstanceId | string | 否 | 真实运行上下文,不用 projectId 替代 |
|
|
11
|
+
| options.signal | AbortSignal | 否 | 取消等待/上传,不保证服务端回滚 |
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { createClient, type Tier0Client } from '@tier0/sdk-v2';
|
|
15
|
+
|
|
16
|
+
export async function uploadAttachment(file: File, client: Tier0Client = createClient()) {
|
|
17
|
+
const saved = await client.files.upload(file);
|
|
18
|
+
if (!saved.filePath) throw new Error('上传回执缺少 filePath');
|
|
19
|
+
return { filePath: saved.filePath };
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
上传流程:平台 prepareUpload 返回 postUrl/postFields/filePath → SDK 按签名字段在前、file 在后构建表单 → 匿名上传存储。不要给对象存储附带平台 Authorization。上传失败时不能将准备阶段返回的 filePath 当成功附件保存。
|
|
24
|
+
|
|
25
|
+
业务表持久化 filePath;临时签名 URL 不作为长期引用。文件写入和业务记录写入不是原子事务,业务保存失败时记录待清理对象,只在任务授权范围内清理。
|
|
26
|
+
|
|
27
|
+
文件大于 100 MiB 时自动分片;阈值不是单文件产品上限,套餐配额/文件类型限制由后端决定。需要进度时显式用 [uploadMultipart](multipart.md)。v2 没有 v1 的 options.multipart/concurrency/retry/resume 控件,不照抄旧参数。
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# 获取文件访问地址
|
|
2
|
+
|
|
3
|
+
方法:`files.url({filePath,expiredSec?,responseContentDisposition?}, {signal}?)`;GET /openapi/v1/assets/files/url。
|
|
4
|
+
|
|
5
|
+
| 字段 | 类型 | 必填 | 说明 |
|
|
6
|
+
| --- | --- | --- | --- |
|
|
7
|
+
| filePath | string | 是 | 上传返回的持久化引用 |
|
|
8
|
+
| expiredSec | number / bigint | 否 | 请求有效期(秒),由后端决定实际期限 |
|
|
9
|
+
| responseContentDisposition | string | 否 | 底层响应头提示,不代替浏览器保存动作 |
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { createClient, type Tier0Client } from '@tier0/sdk-v2';
|
|
13
|
+
|
|
14
|
+
export async function getPreview(filePath: string, client: Tier0Client = createClient()) {
|
|
15
|
+
const result = await client.files.url({ filePath, expiredSec: 300 });
|
|
16
|
+
return { url: result.fileUrl, expiresAt: result.expiresAt };
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
返回已解包的 fileUrl、可选 expiresAt。私有文件是短期签名 URL;公开文件可能是公开地址且无过期字段。不能对缺失 expiresAt 猜时间,也不能用普通 Number 强转大整数。
|
|
21
|
+
|
|
22
|
+
URL 适合图片预览、媒体引用或明确要求的临时分享。业务表仍保存 filePath。附件“下载”用[下载流程](download.md),避免直接导航签名 URL 进入 PDF 预览器。分享链接不改变文件权限,也不允许把 API Key 拼进 URL。
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tier0-sdk-v2-members
|
|
3
|
+
description: 使用 @tier0/sdk-v2 查询 Tier0 Workspace 成员、搜索用户和解析通知收件人,并按 appId 查询能使用某个 App 的成员;区分 Workspace 身份与 App 访问授权。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Workspace 成员查询
|
|
7
|
+
|
|
8
|
+
参数表、成员选择和有界增量分页见 [Workspace 成员](references/platform-members.md)。v1 的项目成员接口未迁入 v2,不能用 Workspace 列表代替 App 授权名单。
|
|
9
|
+
|
|
10
|
+
公开入口有两个:`client.members.listWorkspace` 只用于 Workspace 级的成员选择;需要能使用某个 App 的成员时用 `client.members.listApp`。没有 project 成员迁移别名,也不在客户端推断 App 成员。使用服务端 API Key;平台授权决定可查询范围。
|
|
11
|
+
|
|
12
|
+
## 搜索与展示候选人
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { createClient } from '@tier0/sdk-v2';
|
|
16
|
+
|
|
17
|
+
export async function searchMembers(keyword: string, page = 1) {
|
|
18
|
+
const result = await createClient().members.listWorkspace({
|
|
19
|
+
keyword, statuses: ['active'], page, size: 20,
|
|
20
|
+
});
|
|
21
|
+
return {
|
|
22
|
+
list: result.list.map(member => ({
|
|
23
|
+
userId: member.userId,
|
|
24
|
+
label: member.nickName || member.userName || member.email || '未命名成员',
|
|
25
|
+
email: member.email,
|
|
26
|
+
status: member.status,
|
|
27
|
+
roles: member.roles,
|
|
28
|
+
})),
|
|
29
|
+
total: result.total, page: result.page, size: result.size,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
返回值已解包,不读 result.data。userId 始终作为字符串传给通知或业务表;不要转换成 Number。名称不唯一,用户意图不能唯一定位时返回候选人,不能静默选第一个。
|
|
35
|
+
|
|
36
|
+
## 筛选与分页
|
|
37
|
+
|
|
38
|
+
请求可带 keyword、page、size、roleKey、roles、statuses、updatedAtStart、updatedAtEnd。角色与状态取自实际平台定义,不造枚举或假定固定管理员角色。时间字段为字符串,按服务端认可格式使用;不要将 number 毫秒直接填进去。
|
|
39
|
+
|
|
40
|
+
返回 list/total/page/size;total/page/size 可能为 number 或 bigint。批量同步按服务端返回分页继续,并限定本次任务的范围,不把一页或受限视图宣称为完整目录。按更新时间增量同步时保存已确认的水位并处理边界重复。
|
|
41
|
+
|
|
42
|
+
## 用于通知
|
|
43
|
+
|
|
44
|
+
1. 确定查询的是当前 Workspace,按姓名/邮箱等输入取得候选人。
|
|
45
|
+
2. 使用选定的 userId 调用通知,展示名只用于 UI。
|
|
46
|
+
3. 需要“这个 App 的所有用户”时用 `listApp` 取访问名单,不能直接遍历 Workspace 成员群发。
|
|
47
|
+
|
|
48
|
+
[通知 Skill](../tier0-sdk-v2-notifications/SKILL.md)说明渠道与幂等。成员查询不会替你建立 App 访问权限,也不能证明收件人可以打开通知里的业务页面。
|
|
49
|
+
|
|
50
|
+
## App 可用成员
|
|
51
|
+
|
|
52
|
+
能使用某个 App 的成员用 `members.listApp`,参数、示例与错误处理见 [App 可用成员](references/app-members.md)。`appId` 必须显式传入,SDK 不隐式读取 `APP_ID`;查询当前 App 时传 `getCurrentAppId()`。接口只接受 API Key,登录会话不能调用;绑定 App 的凭证只能查自己的 appId,查其他 App 返回 403。3003016 / 3003017 都是 HTTP 503,表示访问名单暂时无法给出,不是空名单:按 `ApiError.code` 分支,不匹配 message 文本;3003016 需要重新部署 App,3003017 可稍后重试。
|
|
53
|
+
|
|
54
|
+
## 当前边界
|
|
55
|
+
|
|
56
|
+
whoami 是 Key 身份查询,不是成员列表;它也不是浏览器当前员工身份。401/403 应核对 Workspace 和权限,不自动切换到更高权限凭证。只回传界面实际需要的成员字段,避免把整个目录和角色数据泄露给无关调用方。
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# App 可用成员
|
|
2
|
+
|
|
3
|
+
方法:`members.listApp(body)`;POST /openapi/v1/apps/access-members/list。返回能使用该 App 线上版本的有效成员。`appId` 必填且由调用方显式传入,SDK 不隐式读取 `APP_ID`;查询当前 App 时传 `getCurrentAppId()`。接口只接受 API Key,登录会话不能调用;绑定 App 的凭证只能查自己的 appId,查其他 App 返回 403。
|
|
4
|
+
|
|
5
|
+
| 字段 | 类型 | 说明 |
|
|
6
|
+
| --- | --- | --- |
|
|
7
|
+
| appId | string | 必填,agent-platform App UUID |
|
|
8
|
+
| roleKeys | string[] | 任一命中即可,大小写不敏感 |
|
|
9
|
+
| grantedAtStart / grantedAtEnd | number / bigint | 授权时间,Unix 毫秒,闭区间,精度为秒 |
|
|
10
|
+
| page / size | number / bigint | 分页 |
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { ApiError, createClient, getCurrentAppId, type Tier0Client } from '@tier0/sdk-v2';
|
|
14
|
+
|
|
15
|
+
/** 线上版本的角色事实未落库:重试无效,需要重新部署 App。 */
|
|
16
|
+
const APP_ACCESS_LIST_UNDETERMINED = 3003016;
|
|
17
|
+
/** 成员有效状态暂时取不到:稍后重试。 */
|
|
18
|
+
const APP_ACCESS_LIST_UNAVAILABLE = 3003017;
|
|
19
|
+
|
|
20
|
+
export async function appMembers(page = 1, client: Tier0Client = createClient(), appId = getCurrentAppId()) {
|
|
21
|
+
try {
|
|
22
|
+
const result = await client.members.listApp({ appId, page, size: 20 });
|
|
23
|
+
return {
|
|
24
|
+
list: result.list.map(member => ({
|
|
25
|
+
userId: member.userId,
|
|
26
|
+
label: member.userName || member.email || '未命名成员',
|
|
27
|
+
roles: member.roles.map(role => role.roleName),
|
|
28
|
+
})),
|
|
29
|
+
total: result.total, page: result.page, size: result.size,
|
|
30
|
+
};
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error instanceof ApiError && error.code === APP_ACCESS_LIST_UNDETERMINED) throw new Error('App 访问名单无法确定,请重新部署 App');
|
|
33
|
+
if (error instanceof ApiError && error.code === APP_ACCESS_LIST_UNAVAILABLE) throw new Error('App 访问名单暂不可用,请稍后重试');
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
返回已解包的 appId、published(1 已发布 / 2 未发布)、allActiveMembers(1 所有有效成员都可使用 / 2 按角色授权)、roles、list、total、page、size。成员的 roles[].sources 区分直接授予(direct)和经成员组授予(group)。userId 保持字符串。
|
|
40
|
+
|
|
41
|
+
3003016 / 3003017 都是 HTTP 503:访问名单暂时无法给出,不是空名单,也不代表所有人可用。按 `ApiError.code` 分支,不匹配 message 文本。3003016 是线上版本角色事实未落库,重试无效,需要重新部署 App;3003017 是成员有效状态暂时取不到,可稍后重试。无权限(403)时报告配置问题,不改用 Workspace 成员列表代替。
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Workspace 成员查询与选择
|
|
2
|
+
|
|
3
|
+
方法:`members.listWorkspace(body)`;POST /openapi/v1/platform/getMembers。查询当前 Key 所属 Workspace;云端需要 uns:read。通知读写权限不自动包含成员查询。
|
|
4
|
+
|
|
5
|
+
| 字段 | 类型 | 说明 |
|
|
6
|
+
| --- | --- | --- |
|
|
7
|
+
| keyword | string | 姓名、昵称、邮箱关键词 |
|
|
8
|
+
| roleKey / roles | string / string[] | 单个或多个角色筛选,组合为 OR |
|
|
9
|
+
| statuses | string[] | active / disabled;选择通知收件人通常 active |
|
|
10
|
+
| updatedAtStart / updatedAtEnd | string | RFC3339,包含边界 |
|
|
11
|
+
| page | number / bigint | 从 1 开始,默认 1 |
|
|
12
|
+
| size | number / bigint | 默认 20,最大 100 |
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { createClient, type Tier0Client } from '@tier0/sdk-v2';
|
|
16
|
+
|
|
17
|
+
export async function memberOptions(keyword: string, page = 1, client: Tier0Client = createClient()) {
|
|
18
|
+
const result = await client.members.listWorkspace({ keyword, statuses: ['active'], page, size: 20 });
|
|
19
|
+
return {
|
|
20
|
+
options: result.list.map(member => ({
|
|
21
|
+
value: member.userId,
|
|
22
|
+
label: member.nickName || member.userName || member.email || '未命名成员',
|
|
23
|
+
email: member.email, roles: member.roles.map(role => role.roleName || role.roleKey),
|
|
24
|
+
})),
|
|
25
|
+
total: result.total, page: result.page, size: result.size,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
v2 返回已解包的 list/total/page/size,不读 result.data。userId、roleId 保持字符串;value 留作内部选项值,用户看到姓名/邮箱/角色,不让用户填写 ID。不确定匹配时展示候选人,不默认选第一项。
|
|
31
|
+
|
|
32
|
+
内置公开角色 owner/builder/operator;筛选兼容性以服务端为准,匹配后仍返回成员全部角色。不要根据某个角色名自行推断 App 访问授权。
|
|
33
|
+
|
|
34
|
+
## 有界分页同步
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { createClient, type Tier0Client } from '@tier0/sdk-v2';
|
|
38
|
+
|
|
39
|
+
export async function* scanMembers(
|
|
40
|
+
updatedAtStart: string, updatedAtEnd: string, maxPages = 20,
|
|
41
|
+
client: Tier0Client = createClient(),
|
|
42
|
+
) {
|
|
43
|
+
if (!Number.isSafeInteger(maxPages) || maxPages < 1) throw new Error('无效页数上限');
|
|
44
|
+
for (let page = 1; page <= maxPages; page++) {
|
|
45
|
+
const result = await client.members.listWorkspace({ page, size: 100, updatedAtStart, updatedAtEnd });
|
|
46
|
+
yield result.list;
|
|
47
|
+
if (BigInt(result.page) * BigInt(result.size) >= BigInt(result.total)) return;
|
|
48
|
+
if (!result.list.length) throw new Error('分页结果与总数不一致');
|
|
49
|
+
}
|
|
50
|
+
throw new Error('达到同步页数上限,结果未完整');
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
增量时间包含边界,按 userId/updatedAt 去重,成功完成后再保存水位;跨页没有固定快照保证。只向界面返回实际需要字段。
|
|
55
|
+
|
|
56
|
+
v1 project-members 不迁移:v2 没有 getCurrentProjectId 或项目成员接口。Workspace 列表只用于 Workspace 级选择,不能冒充某 App 的可访问用户;需要 App 可用成员时用 `members.listApp`(见 [App 可用成员](app-members.md))。无权限时报告配置问题,不以用户手填 ID 绕过。
|