结构化图片画布持久化并补充历史资源修复
将旧布局上限统一提升至2MiB并接入revision CAS保存 新增画布图层、生成对话框和迁移状态结构化存储 新增migration operator保护的回填、激活和回滚脚本 新增历史图片映射及音频资源恢复的定向dry-run/apply流程 补齐前端保存队列、OpenAPI、Bindings、发布归档与回归测试
This commit is contained in:
@@ -503,6 +503,9 @@
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/components/responses/NotFound"
|
||||
},
|
||||
"409": {
|
||||
"$ref": "#/components/responses/Conflict"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1327,6 +1330,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Conflict": {
|
||||
"description": "画布 revision 冲突,需重新读取最新快照后合并或重试",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"UpstreamError": {
|
||||
"description": "上游生成服务失败",
|
||||
"content": {
|
||||
@@ -1755,8 +1768,17 @@
|
||||
"$ref": "#/components/schemas/EditorCanvasViewport"
|
||||
},
|
||||
"layers": {
|
||||
"type": "object",
|
||||
"description": "画布图层 JSON,最大约 256KB。"
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"description": "兼容画布布局 JSON,最大 2 MiB;服务端会拆分为结构化图层与生成对话框行。"
|
||||
},
|
||||
"expectedRevision": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "可选的画布 revision CAS;不匹配时返回 409。"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -2110,7 +2132,11 @@
|
||||
"$ref": "#/components/schemas/EditorCanvasViewport"
|
||||
},
|
||||
"layers": {
|
||||
"type": "object"
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
"type": "array",
|
||||
@@ -2132,6 +2158,8 @@
|
||||
"title",
|
||||
"viewport",
|
||||
"layers",
|
||||
"revision",
|
||||
"layoutStorageVersion",
|
||||
"createdAt",
|
||||
"updatedAt"
|
||||
],
|
||||
@@ -2149,7 +2177,25 @@
|
||||
"$ref": "#/components/schemas/EditorCanvasViewport"
|
||||
},
|
||||
"layers": {
|
||||
"type": "object"
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"revision": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"layoutStorageVersion": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"backgroundColor": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string",
|
||||
|
||||
@@ -86,7 +86,7 @@ npm run check:server-rs-ddd
|
||||
- 画布 Agent 工具复用既有编辑器图片生成 / 修改 / 图标 spritesheet BFF,并继续使用后端模型定价和 `execute_billable_asset_operation_with_cost`;前端不提交 `priceMudPoints`。
|
||||
- `/messages/{messageId}/confirm` 与 `/messages/{messageId}/cancel` 只返回成功确认;前端成功后立即重新读取整个会话,以会话详情中的权威消息状态和 `externalJobId` 驱动气泡展示与任务轮询。
|
||||
- 会话详情的终态懒回填必须在单次 GET 和同一 conversation lock 内完成有界重试:任务结果读取、completed payload 解析或工具 formatter 首次失败后最多重试 3 次,每次等待 100ms 并重新读取主任务。任务读取失败或 completed 任务暂缺 `result_payload_json` 时,本次重试耗尽后仍保留 OSS 工具消息的 `not_completed + externalJobId`,由下次会话读取继续 reconcile;JSON 损坏、结果结构不兼容或 formatter 失败等确定性致命错误在重试耗尽后原子写为 `failed`,保存“重试 3 次后仍失败”的最后错误,避免永久循环。
|
||||
- 画布 Agent 是“正式任务 payload 不进入通用用户 read model”规则的窄例外消费者:`GET /api/editor/agent-conversations/{conversationId}` 只按会话中已有的 `externalJobId` 定向读取主任务,完成后由对应工具 formatter 从 `result_payload_json` 提取并归一有界的图片 / 视频 / 音频引用,写入 OSS 工具消息后返回。前端仍不得通过通用任务列表 / 状态接口读取或解析 `request_payload_json` / `result_payload_json`;OSS 轻量媒体引用只是会话展示与后续 Agent 上下文,不替代 `editor_project_resource`、`editor_asset`、`editor_canvas.layers_json` 或 `external_generation_job` 的业务真相。
|
||||
- 画布 Agent 是“正式任务 payload 不进入通用用户 read model”规则的窄例外消费者:`GET /api/editor/agent-conversations/{conversationId}` 只按会话中已有的 `externalJobId` 定向读取主任务,完成后由对应工具 formatter 从 `result_payload_json` 提取并归一有界的图片 / 视频 / 音频引用,写入 OSS 工具消息后返回。前端仍不得通过通用任务列表 / 状态接口读取或解析 `request_payload_json` / `result_payload_json`;OSS 轻量媒体引用只是会话展示与后续 Agent 上下文,不替代 `editor_project_resource`、`editor_asset`、结构化画布表或 `external_generation_job` 的业务真相。未激活结构化存储的 canvas 才继续以 `editor_canvas.layers_json` 作为 legacy 布局真相。
|
||||
|
||||
### 创作 / 游玩统一流程主干
|
||||
|
||||
@@ -286,7 +286,7 @@ npm run check:server-rs-ddd
|
||||
|
||||
- Rust 结构体:`ExternalGenerationJob`
|
||||
- 源码:`server-rs/crates/spacetime-module/src/external_generation.rs`
|
||||
- 用途:外部生成 worker 的内部持久任务队列;`GENARRATIVE_EXTERNAL_GENERATION_MODE=queue` 时,`api-server` HTTP 角色只入队,`external-generation-worker` 角色通过 claim lease 领取、续租、执行,并用 `lease_token` 栅栏回写阶段、完成 / 失败。队列行继续保存 worker 执行、计费与滚动发布兼容所需字段,末尾可选 `phase` 只取 `generating / processing`;claim 写 `generating`,真实进入抠图处理时由受 `job_id + worker_id + lease_token` 保护的 procedure 写 `processing`。phase procedure 以结构化结果区分 `LeaseFencingRejected` 与 `OtherRejected`;`LeaseFencingRejected` 立即终止,`OtherRejected` 以及 SDK 的 `Procedure` / `Runtime` 错误不重试,只有 `Build` / `ConnectDropped` / `Timeout` 在同一个 job attempt 内重试一次。该重试只重新上报 phase,不把任务写回 `pending`,也不重新调用 provider;编辑器 job 入队固定 `max_attempts=1`,第二次传输失败后任务进入 `failed`,不会回到 `pending` 或从 provider 生成起点重跑。用户可见任务列表、价格、状态、阶段、未确认终态数量和通知确认时间的正式读取事实源已经迁到 `external_generation_job_summary`;BFF 不得再为列表 / 详情 / acknowledge 读取该大表。拼图 `compile_puzzle_draft` 的前置 `compile_puzzle_agent_draft`、`generate_puzzle_images` 与 `generate_puzzle_ui_background` 的业务写回也在对应 SpacetimeDB transaction 内校验 `job_id + worker_id + lease_token`、job kind、owner 和 source entity,避免过期 worker 写 session / work profile;图片画布编辑器的 `editor_image_generation`、`editor_image_edit`、`editor_background_removal`、`editor_icon_spritesheet_generation`、`editor_ui_design_asset_extraction`、`editor_character_animation_generation`、`editor_video_generation`、`editor_sound_effect_generation` 和 `editor_background_music_generation` 复用同一队列表,worker 成功后经 `api-server` facade 写入 `editor_project_resource` / `editor_asset` / `editor_canvas.layers_json`,前端只通过 BFF job 状态轮询和项目快照读取恢复完成态。`GENARRATIVE_EXTERNAL_GENERATION_MODE=inline` 时不创建该队列行,三个 external generation guard 字段必须同时为空才允许 api-server 受控同步写回,半空 guard 仍会拒绝。worker 成功写回业务事实后才能 complete job;业务失败态写回成功后才能 fail job,失败态未写回时保留租约等待后续重领。
|
||||
- 用途:外部生成 worker 的内部持久任务队列;`GENARRATIVE_EXTERNAL_GENERATION_MODE=queue` 时,`api-server` HTTP 角色只入队,`external-generation-worker` 角色通过 claim lease 领取、续租、执行,并用 `lease_token` 栅栏回写阶段、完成 / 失败。队列行继续保存 worker 执行、计费与滚动发布兼容所需字段,末尾可选 `phase` 只取 `generating / processing`;claim 写 `generating`,真实进入抠图处理时由受 `job_id + worker_id + lease_token` 保护的 procedure 写 `processing`。phase procedure 以结构化结果区分 `LeaseFencingRejected` 与 `OtherRejected`;`LeaseFencingRejected` 立即终止,`OtherRejected` 以及 SDK 的 `Procedure` / `Runtime` 错误不重试,只有 `Build` / `ConnectDropped` / `Timeout` 在同一个 job attempt 内重试一次。该重试只重新上报 phase,不把任务写回 `pending`,也不重新调用 provider;编辑器 job 入队固定 `max_attempts=1`,第二次传输失败后任务进入 `failed`,不会回到 `pending` 或从 provider 生成起点重跑。用户可见任务列表、价格、状态、阶段、未确认终态数量和通知确认时间的正式读取事实源已经迁到 `external_generation_job_summary`;BFF 不得再为列表 / 详情 / acknowledge 读取该大表。拼图 `compile_puzzle_draft` 的前置 `compile_puzzle_agent_draft`、`generate_puzzle_images` 与 `generate_puzzle_ui_background` 的业务写回也在对应 SpacetimeDB transaction 内校验 `job_id + worker_id + lease_token`、job kind、owner 和 source entity,避免过期 worker 写 session / work profile;图片画布编辑器的 `editor_image_generation`、`editor_image_edit`、`editor_background_removal`、`editor_icon_spritesheet_generation`、`editor_ui_design_asset_extraction`、`editor_character_animation_generation`、`editor_video_generation`、`editor_sound_effect_generation` 和 `editor_background_music_generation` 复用同一队列表。结构化 canvas 激活后,当前 worker completion 先以读取时 canvas revision 执行 CAS,并发冲突时拒绝覆盖并保留可诊断失败;目标是进一步收口为受 lease 栅栏保护的单事务幂等写入 `editor_project_resource`、结果 `editor_canvas_layer`、`editor_canvas_generation_dialog` 终态和 canvas revision。未激活 canvas 在 2 MiB 上限内继续走 legacy `editor_canvas.layers_json` 兼容写回。前端只通过 BFF job 状态轮询和项目快照读取恢复完成态。`GENARRATIVE_EXTERNAL_GENERATION_MODE=inline` 时不创建该队列行,三个 external generation guard 字段必须同时为空才允许 api-server 受控同步写回,半空 guard 仍会拒绝。worker 成功写回业务事实后才能 complete job;业务失败态写回成功后才能 fail job,失败态未写回时保留租约等待后续重领。
|
||||
- 载荷约束:本次先对 `source_module = editor-canvas` 的 `request_payload_json` / `result_payload_json` 实施有限大小合法 JSON、任意层级禁止 `data:` / `blob:` 的双层门禁,只保存 worker 执行必需的普通参数和已登记媒体引用。画布 Agent 来源的任务可在 `result_payload_json.editor-agent-tool-call-result` 中保存有界的轻量结果和已登记媒体引用,供后端按已有 `externalJobId + owner_user_id` 定向懒回填;其它编辑器任务保持元数据结果,并可保存有界的 `warning.code/reason`。其它玩法在完成各自参考图资源化之前不由本次门禁静默改变既有请求契约。该主表只供 worker claim / 执行、受控维护以及画布 Agent 的定向结果回填读取;正式用户任务列表、单任务状态、队列概览与 acknowledge 不得返回或解析这两个 payload。画布 Agent 懒回填必须经对应工具 formatter 归一为有界轻量媒体引用后写入 OSS 会话,不能把原始 payload 直接透传前端。
|
||||
- 非阻断告警:角色形象、图标图集和 UI 素材提取已保存 provider 原图、但透明背景处理最终失败时,以原图唯一主图完成任务;透明图和切片不写入画布。这个 source-only 降级只包住透明背景处理的最终失败,phase 上报、provider 原图持久化、透明处理图持久化或画布写回失败仍按任务错误传播。图标 / UI 透明图集成功但自动拆分降级时仍保留透明图集;通用 `warning` 与 `sliceWarning` 互斥。两类成功降级都以既有 `completed` 状态收口,不新增状态值:source-only 的 inline / external v1 响应使用结构化 `warning.code/reason`,仅拆分失败的 inline / external v1 响应继续使用既有 `sliceWarning.code/reason`,其 `reason` 保留原始诊断;queue worker 才把两者归一为有界的 `result_payload_json.warning`,且通用 `warning` 优先并原样保留完整 `reason`,只有 `sliceWarning.reason` 由 worker 添加“图集已生成,但自动拆分未完成:”前缀。除上述画布 Agent 定向回填的轻量结果外,队列结果不保存图片、切片列表或媒体 URL。
|
||||
|
||||
@@ -531,21 +531,42 @@ npm run check:server-rs-ddd
|
||||
|
||||
- Rust 结构体:`EditorProject`
|
||||
- 源码:`server-rs/crates/spacetime-module/src/editor_project_storage.rs`
|
||||
- 说明:图片画布工程真相表,保存 owner、标题和工程时间戳;viewport 与图层布局已拆到 `editor_canvas`,旧 layout columns 暂作为兼容列保留,不再作为权威数据源。只通过 `/api/editor/projects*`、`/api/editor/projects/{projectId}/agent-conversations`、`/api/editor/agent-conversations/{conversationId}*` BFF 和 `spacetime-client` facade 读写;项目页列表、重命名和删除也使用该能力,删除工程时级联清理默认画布和资源元数据。
|
||||
- 说明:图片画布工程真相表,保存 owner、标题和工程时间戳;viewport 与画布数据已拆到 `editor_canvas` 及其结构化子表,旧 project layout columns 暂作为兼容列保留,不再作为权威数据源。只通过 `/api/editor/projects*`、`/api/editor/projects/{projectId}/agent-conversations`、`/api/editor/agent-conversations/{conversationId}*` BFF 和 `spacetime-client` facade 读写;项目页列表、重命名和删除也使用该能力,删除工程时级联清理默认画布、结构化画布行、迁移状态和资源元数据。
|
||||
- 索引:`by_editor_project_owner_user_id` 用于读取当前用户最近编辑工程和项目页工程列表。
|
||||
|
||||
### `editor_canvas`
|
||||
|
||||
- Rust 结构体:`EditorCanvas`
|
||||
- 源码:`server-rs/crates/spacetime-module/src/editor_project_storage.rs`
|
||||
- 说明:图片画布数据表,归属于 `editor_project`,保存默认画布的 viewport typed columns、图层布局 JSON、owner 和时间戳;当前编辑器读取 / 保存 project 的默认 canvas,后续支持一个工程多个 canvas。
|
||||
- 说明:图片画布根表,归属于 `editor_project`,保存默认画布的 viewport typed columns、owner、时间戳与当前 revision;当前编辑器读取 / 保存 project 的默认 canvas,后续支持一个工程多个 canvas。`layers_json` 继续保留,legacy canvas 在统一 **2 MiB** 上限内以它作为布局真相;`editor_canvas_layout_migration` 激活 structured 后,图层和生成对话框分别以 `editor_canvas_layer`、`editor_canvas_generation_dialog` 为权威,`layers_json` 只作为存量迁移输入和受限回滚载体。所有结构化 mutation 必须以 `expected_revision` 做 CAS,成功事务只递增一次 revision。
|
||||
- 索引:`by_editor_canvas_project_id`、`by_editor_canvas_owner_user_id`。
|
||||
|
||||
### `editor_canvas_layer`
|
||||
|
||||
- Rust 结构体:`EditorCanvasLayer`
|
||||
- 源码:`server-rs/crates/spacetime-module/src/editor_project_storage.rs`
|
||||
- 说明:图片画布结构化图层表,一行保存一个 layer 的 canvas / project / owner 归属、几何、层级顺序、分组、hidden / locked / flip 状态与 `resource_id`。V1 的 `item_json` 只保留未结构化扩展字段,单行最大 512 KiB;快照以 typed 列重组,不得把完整图层 JSON 当作平行真相。媒体业务真相仍由 `editor_project_resource` / `editor_asset` 持有。历史 layout 的 `sourceResourceId == resourceId` 属于无意义自引用,迁移时按资源表真相剥离;其他资源字段冲突继续 fail-closed。唯一存量缺资源例外是已缺资源行、但帧与预览均为稳定站内对象路径的 `local-* + generated + image-sequence` 历史角色动作图层:迁移保留其有界媒体扩展并纳入 canonical hash;active 后只能续存同一行且扩展不可变,不能新增或篡改。其他缺资源图层继续 fail-closed。当前站内写入通过 revision CAS 把兼容布局事务性拆成行;后续再将新增、移动、缩放、删除、重排和分组收窄为有界 batch mutation。
|
||||
- 索引:按 canvas 读取完整结构化快照,按 project 做级联清理;owner 保留在行内用于归属校验。同一 canvas 的 layer id 必须稳定且唯一。
|
||||
|
||||
### `editor_canvas_generation_dialog`
|
||||
|
||||
- Rust 结构体:`EditorCanvasGenerationDialog`
|
||||
- 源码:`server-rs/crates/spacetime-module/src/editor_project_storage.rs`
|
||||
- 说明:图片画布生成对话框表,保存 canvas / project / owner、生成模式、状态、可选 source / generated layer、占位几何和有界扩展字段;typed 列为快照真相,`dialog_json` 只保留未结构化参数。当前 worker completion 以读取时 revision 调用 CAS 保存,冲突时拒绝覆盖;`job_id + worker_id + lease_token` 栅栏下的资源 / layer / dialog / job 单事务完成仍是后续收口。
|
||||
- 索引:按 canvas 和 project 读取结构化行;当前未建立 external job 二级索引。
|
||||
|
||||
### `editor_canvas_layout_migration`
|
||||
|
||||
- Rust 结构体:`EditorCanvasLayoutMigration`
|
||||
- 源码:`server-rs/crates/spacetime-module/src/editor_project_storage.rs`
|
||||
- 说明:每个 canvas 一行的结构化持久化控制与迁移审计表,保存 schema version、`backfilled / active / rolled_back` 状态、最后校验 revision、legacy / structured canonical hash、layer / dialog 数量、资源引用集合 hash 和激活 / 回滚时间。存量迁移固定执行幂等 `backfill → hash / 数量 / 资源引用核对 → revision CAS activate`;active 重入和 rollback 同样重新核对,不只按状态早返。回滚结果还必须不超过 2 MiB。三个 procedure 只允许 database migration operator 调用;`npm run spacetime:editor-canvas-layout:migrate` 默认 dry-run,显式 `--apply` 才写入。
|
||||
- 索引:`canvas_id` 唯一,一切模式切换仅允许受限迁移 procedure 执行;前端不得提交或推断存储模式。
|
||||
|
||||
### `editor_project_resource`
|
||||
|
||||
- Rust 结构体:`EditorProjectResource`
|
||||
- 源码:`server-rs/crates/spacetime-module/src/editor_project_storage.rs`
|
||||
- 说明:图片画布工程资源元数据表,保存已经放入某个 project 画布的上传 / 生成图片资源快照、OSS 引用、尺寸、来源类型、prompt、provider、task、源资源关系、`asset_kind`、`generation_inputs_json` 和历史 `public_showcase_enabled`。`asset_kind` 标记角色、图标、UI 设计图、视频、音频等素材类别;`generation_inputs_json` 保存用户可见生成输入快照,供图片信息页刷新后恢复。`public_showcase_enabled` 只保留旧接口兼容,不再作为 `/creation` 的 `陶泥儿精选` 事实源;精选公开改由账号级生成素材提交 `editor_showcase_asset` 审核决定。图片 / 图标 / UI 提取等生成 BFF 在请求携带 `project_id` 时负责创建该表记录并把 resource 快照返回前端;前端只保存布局引用,不能把同一生成结果再次作为正式业务真相写入。项目封面快照也落在该表,使用 `asset_kind = project-cover-snapshot`、`source_type = uploaded` 和私有 OSS / asset object 引用,代表画布当前视口栅格化后的静态封面;项目列表和创作主页最近项目只读取最新封面快照资源,不在列表页根据 `editor_canvas.layers_json` 临时拼画布。从账号级素材库把同一生成素材拖回同一项目画布时,后端优先复用同项目内同源同媒体资源,避免每个图层实例都插入新的资源行。账号级素材删除不级联删除该表,避免历史画布丢图。`editor_canvas.layers_json` 只保存图层几何、层级、分组、资源引用和生成器对象;新写入不再把素材生成输入快照作为图层布局真相保存,旧布局字段只作为兼容兜底读取。
|
||||
- 说明:图片画布工程资源元数据表,保存已经放入某个 project 画布的上传 / 生成图片资源快照、OSS 引用、尺寸、来源类型、prompt、provider、task、源资源关系、`asset_kind`、`generation_inputs_json` 和历史 `public_showcase_enabled`。`asset_kind` 标记角色、图标、UI 设计图、视频、音频等素材类别;`generation_inputs_json` 保存用户可见生成输入快照,供图片信息页刷新后恢复。`public_showcase_enabled` 只保留旧接口兼容,不再作为 `/creation` 的 `陶泥儿精选` 事实源;精选公开改由账号级生成素材提交 `editor_showcase_asset` 审核决定。图片 / 图标 / UI 提取等生成 BFF 在请求携带 `project_id` 时负责创建该表记录并把 resource 快照返回前端;前端只保存稳定 `resource_id` 布局引用,不能把同一生成结果再次作为正式业务真相写入。项目封面快照也落在该表,使用 `asset_kind = project-cover-snapshot`、`source_type = uploaded` 和私有 OSS / asset object 引用,代表画布当前视口栅格化后的静态封面;项目列表和创作主页最近项目只读取最新封面快照资源,不在列表页根据 layout 临时拼画布。从账号级素材库把同一生成素材拖回同一项目画布时,后端优先复用同项目内同源同媒体资源,避免每个图层实例都插入新的资源行。账号级素材删除不级联删除该表,避免历史画布丢图。结构化 canvas 的几何、层级、分组和资源引用以 `editor_canvas_layer` 为权威,生成器对象以 `editor_canvas_generation_dialog` 为权威;legacy canvas 才在 2 MiB 上限内从 `editor_canvas.layers_json` 兼容读取。新写入不再把素材生成输入快照作为图层布局真相保存。历史普通图层缺资源只能由 migration operator 调用 `repair_editor_canvas_resources_and_return` 定向修复:procedure 每次只处理一个尚无迁移记录的 legacy canvas,校验 owner/project、revision、canvas/project 两份 raw layout SHA-256、精确 layer/resource/sourceResourceId、同工程替换资源与 private asset_object 谱系;图片只替换引用,音频只恢复经核验的 `420x120` 项目资源行。运维入口 `npm run spacetime:editor-canvas-resources:repair` 默认 dry-run,apply 必须绑定 plan SHA-256 并在成功后自动复核 already-repaired,禁止手工 SQL 绕过事务 guard。
|
||||
- 索引:`by_editor_project_resource_project_id`、`by_editor_project_resource_owner_user_id`。
|
||||
|
||||
### `editor_asset_folder`
|
||||
|
||||
@@ -518,7 +518,7 @@ Pingora current release 自审脚本 `scripts/ops/pingora-current-release-audit.
|
||||
|
||||
同一 API release 随包依赖还必须包含 `scripts/check-pingora-release-readiness.mjs` 与 `scripts/check-pingora-canary-live.mjs`。前者在 current release 上以 `--release-runtime-only` 汇总运行时复核,后者支撑目标 Nginx canary live smoke;缺少任一脚本时不能进入直连切换窗口。
|
||||
|
||||
`Genarrative-Stdb-Module-Build` 的 Jenkins 归档产物必须包含 `build/<version>/spacetime_module.wasm`、`spacetime_module.wasm.sha256`、`release-manifest.json`、`scripts/deploy/production-stdb-publish.sh`、`scripts/deploy/production-runtime-writer-identity-rotate.mjs`、`scripts/deploy/maintenance-on.sh`、`scripts/deploy/maintenance-off.sh`、`scripts/spacetime-migration-common.mjs` 和 `scripts/database-backup-to-oss.mjs`,不得包含 `migration-bootstrap-secret.txt` 或任何原始 bootstrap secret。`Genarrative-Stdb-Module-Build` 只接受 `MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID` 指向的受保护 Jenkins Secret File:构建 shell 从临时文件读取原始值,强制校验为 64 位十六进制,计算 SHA-256,随后只通过 `GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET_SHA256` 注入 Rust 编译;WASM 因而只包含摘要,不包含可下载的原文,Stdb `release-manifest.json` 以 `migration_bootstrap_secret_sha256` 记录该非敏感摘要。`Genarrative-Stdb-Module-Publish` 只通过 `copyArtifacts` 复制上述非敏感产物,不在目标机器 checkout Git,并在发布阶段用同一个凭据 ID 再次挂载 Secret File;publish 必须再次校验 64 位十六进制、重算 SHA-256,并与 manifest 的 `migration_bootstrap_secret_sha256` 强制匹配后才可发布。Full Build 必须保证 Stdb Build / Publish 的 `MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID` 完全相同并把同一个 ID 同时透传,不能从构建 artifact 传 secret;ID 不同、manifest 缺摘要或摘要不匹配都必须在发布前失败。
|
||||
`Genarrative-Stdb-Module-Build` 的 Jenkins 归档产物必须包含 `build/<version>/spacetime_module.wasm`、`spacetime_module.wasm.sha256`、`release-manifest.json`、`scripts/deploy/production-stdb-publish.sh`、`scripts/deploy/production-runtime-writer-identity-rotate.mjs`、`scripts/deploy/maintenance-on.sh`、`scripts/deploy/maintenance-off.sh`、`scripts/spacetime-migration-common.mjs`、`scripts/spacetime-maintain-external-generation-jobs.mjs`、`scripts/spacetime-migrate-editor-canvas-layout.mjs` 和 `scripts/database-backup-to-oss.mjs`,不得包含 `migration-bootstrap-secret.txt` 或任何原始 bootstrap secret。`Genarrative-Stdb-Module-Build` 只接受 `MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID` 指向的受保护 Jenkins Secret File:构建 shell 从临时文件读取原始值,强制校验为 64 位十六进制,计算 SHA-256,随后只通过 `GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET_SHA256` 注入 Rust 编译;WASM 因而只包含摘要,不包含可下载的原文,Stdb `release-manifest.json` 以 `migration_bootstrap_secret_sha256` 记录该非敏感摘要。`Genarrative-Stdb-Module-Publish` 只通过 `copyArtifacts` 复制上述非敏感产物,不在目标机器 checkout Git,并在发布阶段用同一个凭据 ID 再次挂载 Secret File;publish 必须再次校验 64 位十六进制、重算 SHA-256,并与 manifest 的 `migration_bootstrap_secret_sha256` 强制匹配后才可发布。Full Build 必须保证 Stdb Build / Publish 的 `MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID` 完全相同并把同一个 ID 同时透传,不能从构建 artifact 传 secret;ID 不同、manifest 缺摘要或摘要不匹配都必须在发布前失败。
|
||||
|
||||
三个 SCM Jenkinsfile 将 `MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID` 默认固定为 `genarrative-spacetime-bootstrap-secret-dev-file`。Secret File 的原文只存在于 Jenkins Credentials;credential ID、参数默认值和定时 / 发布行为以仓库 Jenkinsfile 为事实源,不能只改 Job UI,因为 Declarative Pipeline 下一次载入会重写参数定义。旧 Secret Text `genarrative-spacetime-bootstrap-secret-dev` 继续保留给 Database Import / Export,不得原地改类型或删除。
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
# 图片画布结构化持久化与迁移回滚方案
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
图片画布当前把全部图层和生成对话框整体序列化到 `editor_canvas.layers_json`。该字段接近原 256 KiB 上限时,资源登记仍可成功,而布局保存会返回 `413`;任务完成后重新读取后端旧快照,会把前端尚未持久化的参考图和生成结果从画布移除。
|
||||
|
||||
本方案分两步处理:
|
||||
|
||||
1. 先把 `api-server` 与 SpacetimeDB 的 legacy layout 校验统一提高到 **2 MiB**,为存量画布止血。HTTP 路由的 body envelope 必须覆盖 2 MiB payload 及 JSON 包装开销,不能只修改领域常量。
|
||||
2. 再把画布拆为结构化的 layer、generation dialog、layout migration/state 三张表;`editor_canvas.layers_json` 保留为旧数据、迁移输入和受限回滚载体,不再作为激活结构化存储后的权威读取来源。
|
||||
|
||||
2 MiB 是迁移窗口内的临时兼容上限,不是继续扩大整体 JSON 的长期容量方案。媒体正文、Data URL、生成输入快照仍不得进入画布布局;媒体和生成元数据继续以 `editor_project_resource` / `editor_asset` 为真相源。
|
||||
|
||||
## 2. 权威边界
|
||||
|
||||
结构化模式激活后,画布事实分工如下:
|
||||
|
||||
| 数据 | 权威位置 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 工程归属、默认画布、viewport、当前 revision | `editor_project` / `editor_canvas` | `editor_canvas` 保留 `layers_json` legacy 列,但其内容不参与结构化模式下的正常写入仲裁 |
|
||||
| 图层实例、几何、层级、分组、显示与锁定状态、资源引用 | `editor_canvas_layer` | 一行一个图层;模式专属的有界扩展字段可放 JSON,不能复制媒体正文或完整资源元数据 |
|
||||
| 生成对话框、占位层、来源层、结果层、任务与状态 | `editor_canvas_generation_dialog` | 生成任务和 UI 对话框的持久关联,不再嵌入全量 layout JSON |
|
||||
| 存储模式、迁移阶段、校验 hash、激活与回滚审计 | `editor_canvas_layout_migration` | 每个 canvas 一行,控制 legacy / structured 读取,不允许前端自行切换 |
|
||||
| 图片、视频、音频等媒体及生成元数据 | `editor_project_resource` / `editor_asset` | layer/dialog 只保存稳定 ID 引用;资源创建成功不能被当作布局保存成功 |
|
||||
|
||||
结构化快照由后端在同一 revision 下读取 `editor_canvas`、layer、dialog 和 layout migration state 后组装。前端只消费 BFF 快照,不直接订阅表、做 join 或推断迁移状态。
|
||||
|
||||
## 3. 表与写入契约
|
||||
|
||||
### 3.1 `editor_canvas_layer`
|
||||
|
||||
保存 `layer_id`、`canvas_id`、`project_id`、`owner_user_id`、坐标、宽高、原始尺寸、层级顺序、可选 `group_id`、hidden / locked / flip 状态、`resource_id`、有界的未结构化扩展 JSON、创建与更新时间。查询有 canvas / project 索引;同一 canvas 的层级顺序由 `sort_order` 决定。
|
||||
|
||||
layer 只表达“某个资源怎样放在画布上”。`src / prompt / actualPrompt / model / provider / taskId / objectKey / assetObjectId / sourceResourceId / assetKind / generationInputs / sourceType` 不得为方便展示而重复进入 `item_json` 或成为 layer 真相;迁移或结构化保存时先逐字段核对 `editor_project_resource`,缺少资源、字段冲突或无法无损重组时必须 fail-closed。历史 layout 若把 `sourceResourceId` 错写成当前图层自己的 `resourceId`,这是无意义的自引用,不作为 A/B 来源冲突:迁移时删除该重复字段并以项目资源表为真相;其他非空且不一致的来源 ID 继续拒绝。唯一存量缺资源例外是历史角色动作产生的自包含本地图层:`resourceId` 必须以 `local-` 开头、`sourceType=generated`、`mediaType=image-sequence`,至少包含一帧;帧序号必须从 1 连续递增,宽高必须是有限正数;所有 `imageSequenceFrames[].imageSrc` 及可选 `thumbnailSrc / previewVideoPath` 都必须是无 query / fragment、无路径回退段的站内根路径,且不能含 `data:`、`blob:`、HTTP 或签名 URL;可选帧 `objectKey` 必须与 `imageSrc` 去掉首斜杠后完全一致,图层级 `imageSrc / objectKey / assetObjectId` 必须为空,图层级 `src` 只允许为空或与首帧 `imageSrc` 完全一致。满足这些条件但资源行已不存在时,保留其有界媒体扩展和生成元数据以便前端从首帧恢复,不把这些字段从 canonical hash 中剥离;`sourceResourceId` 目标仍存在时必须属于同工程和 owner,目标已删除时保留原引用参与 hash,不据此伪造资源行。该例外不适用于普通图片、视频、音频、非本地 ID、空帧序列或已有资源字段冲突。存量 `assetKind / generationInputs` 仅允许在资源行尚未记录时由 apply 事务补入资源表,dry-run 只计算预览而不写库。前端读取项目快照时继续使用随项目返回的 resources 按 `resource_id` hydrate,兼容现有画布快照语义。
|
||||
|
||||
### 3.2 `editor_canvas_generation_dialog`
|
||||
|
||||
保存 `dialog_id`、`canvas_id`、`project_id`、`owner_user_id`、生成模式、状态、可选 `source_layer_id`、`generated_layer_id`、占位几何、有界扩展 JSON、创建与更新时间。扩展 JSON 只保留未结构化的生成参数,`mode / status / source / generated / placeholder.x/y/width/height` 以 typed 列为真相;`placeholder.originalWidth / originalHeight`、其他未知 placeholder 字段、dialog 内未知字段及 generation-dialog 顶层未知字段必须原样保存在扩展 JSON。`canvas-settings` 暂无扩展列,出现未识别字段时直接拒绝迁移,不能静默丢弃。当前 external job 关联仍以 worker 任务为准,未在本表重复建立任务真相。
|
||||
|
||||
### 3.3 `editor_canvas_layout_migration`
|
||||
|
||||
保存 `canvas_id`、schema version、`backfilled / active / rolled_back` 状态、最后校验 revision、legacy / structured canonical hash、layer / dialog 数量、资源引用集合 hash、迁移 / 激活 / 回滚时间。状态变更只允许已授权 database migration operator 调用 procedure 执行。procedure 错误作为调用结果和运维日志保留,不另外写入可能与失败事务脱节的 `last_error` 列。
|
||||
|
||||
### 3.4 revision CAS
|
||||
|
||||
所有用户布局写入必须携带读取快照时获得的 `expectedRevision`。procedure 在事务中校验 canvas 当前 revision;不一致返回 `409`,前端应重载后端最新快照,不能只换上新 revision 就原样重放冲突前的整包布局。
|
||||
|
||||
前端保存队列只对无 HTTP 响应的传输失败以及 `408 / 425 / 429 / 502 / 503 / 504` 做有界退避重试,`400 / 403 / 404 / 413` 等确定性错误不重复提交。若旧请求执行期间已有更新布局排队,旧请求失败后必须继续发送最新布局;`409` 后权威快照暂时加载失败时保留 pending save 并定时重新进入冲突恢复,不能等待用户再次拖动画布才恢复保存。
|
||||
|
||||
本次结构化 V1 先保留旧 `{ viewport, layers }` PATCH 作为兼容输入。legacy canvas 即使携带 `expectedRevision` 也只做 CAS legacy 保存,不允许用户写入绕过 migration operator 直接激活 structured;只有已完成 backfill / activate、且 active 迁移记录的 revision / hash / 数量 / 资源引用校验均通过时,后端才在单个事务内把兼容输入拆成 layer / dialog 行并递增一次 revision。旧无 CAS procedure 不得写 structured canvas。V1 快照从 typed 列重组,`item_json / dialog_json` 只保留最大 512 KiB 的未结构化扩展字段。自包含本地图片序列在 active canvas 中只能继续保存已回填且 `layerId / resourceId / sourceType / item_json` 语义完全一致的原行;允许修改几何、层级、分组、显隐等 typed 布局字段。前端序列化按正常资源真相边界省略 `assetKind / generationInputs` 时,后端只从既有结构化行恢复这两个冻结字段再校验;显式修改仍拒绝。active 路径不再经过 legacy 元数据清洗,拒绝新增缺资源序列或改写既有帧、预览、prompt 和生成扩展。后续将新增、移动、缩放、删除、重排和分组收窄为有界 batch mutation;在此之前 2 MiB 仍是兼容整包入口的上限。
|
||||
|
||||
### 3.5 worker 原子完成
|
||||
|
||||
worker 完成生成任务时,本次先用读取时 revision 调用 CAS 保存;发生并发变更时拒绝覆盖并让任务保留可诊断失败,不再静默覆盖用户布局。最终收口仍是受 `job_id + worker_id + lease_token` 栅栏保护的后端 procedure 在同一事务内:
|
||||
|
||||
1. 校验 job、owner、project、canvas、dialog 和租约;
|
||||
2. 幂等创建或确认 `editor_project_resource`;
|
||||
3. 创建 / 替换结果 layer,并删除或更新占位 layer;
|
||||
4. 把 dialog 更新为终态并关联 `generated_layer_id`;
|
||||
5. 递增 canvas revision,最后才允许完成 external job。
|
||||
|
||||
重复 completion 必须返回同一资源、layer 和 dialog 终态,不得重复插入,也不能因 dialog 暂时缺失而返回 `changed=false` 后仍把任务标记完成。任一步失败时整笔业务写回回滚,任务保留可诊断的失败或可重试状态。
|
||||
|
||||
## 4. 存量迁移
|
||||
|
||||
迁移按 canvas 执行 `backfill → hash 核对 → activate`,并保持幂等:
|
||||
|
||||
1. **Backfill**:在短事务中读取 `editor_canvas.layers_json`、canvas revision 和更新时间;解析 legacy items,把普通图层与 generation dialog 分别归一到两张结构化表。按稳定 `canvas_id + item id` upsert,重复运行不能产生新 ID 或重复行。记录本次基准 revision 和 legacy canonical hash。
|
||||
2. **Hash 核对**:按固定字段顺序、数值归一规则和稳定 item 排序,把结构化行重组成 canonical legacy 语义;分别计算 layer 数、dialog 数、资源引用集合以及 SHA-256 canonical hash。raw JSON 的空白、对象 key 顺序和无意义默认值差异不作为不一致;任何不可识别字段必须保存在有界扩展字段中或使迁移失败,不能静默丢弃。项目资源存在时继续逐字段核对;除 `sourceResourceId == resourceId` 的历史自引用按资源表真相剥离外,其他 `sourceResourceId` 等冲突都拒绝迁移。只有上一节定义的缺资源自包含本地图片序列保留 layout 内元数据并参与 hash。
|
||||
3. **Activate**:只有 structured hash、数量和资源引用集合均匹配,且 `editor_canvas.revision` 仍等于 backfill 基准 revision 时,才以 CAS 把 layout migration state 切为 `active` 并把 canvas storage version 切到 structured。revision 已变化时丢弃本轮验证结果并重新 backfill;不得覆盖迁移期间的用户更新。
|
||||
|
||||
当前提供单项目、可重跑的运维入口:`npm run spacetime:editor-canvas-layout:migrate -- --database <db> --server <server> --project-id <id> --owner-user-id <id> --action <backfill|activate|rollback>`。默认 dry-run,只有显式追加 `--apply` 才写入;backfill dry-run 即使遇到尚无 `editor_canvas` 的旧工程,也只用 `editor_project` 构造内存预览,不创建 canvas、不补资源元数据。每个项目依次执行 backfill dry-run / apply 和 activate dry-run / apply,不在一个长事务中扫全表。脚本只调用受 migration operator 保护的 procedure,不直接修改生产表。
|
||||
|
||||
若全量 backfill 审计发现普通图层缺少 `editor_project_resource`,先使用定向资源修复入口:`npm run spacetime:editor-canvas-resources:repair -- --database <db> --server <server> --plan-file </absolute/outside-repo/repair-plan.json>`。plan 必须是仓库外、当前用户持有、权限严格为 `0600` 的普通文件;每个 canvas 绑定 owner / project、expected revision、`editor_canvas.layers_json` 与 legacy `editor_project.layers_json` 两份原始字节 SHA-256,以及精确 layer/resource/sourceResourceId 或 asset_object 修复动作。默认逐 canvas dry-run;apply 必须追加 `--apply --confirm-plan-sha256 <dry-run 输出>`,成功后脚本自动以同一 plan 再 dry-run,并要求全部返回 `already_repaired`。
|
||||
|
||||
`repair_editor_canvas_resources_and_return` 只允许 migration operator 调用,并且只修尚无迁移记录的 legacy canvas。图片动作仅在缺失旧 resourceId、精确旧 sourceResourceId、同工程唯一现存资源、layout 资源元数据与 private asset_object owner/objectKey/task 谱系全部一致时,替换 `resourceId` 并删除重复 `sourceResourceId`。音频动作仅在原 resourceId 全局不存在、图层与 private asset_object 的 owner/objectKey/source job/entity/content type/长度全部匹配时,恢复 `420x120` 的 `sound-effect` / `background-music` 项目资源行;资源 `asset_kind` 分别映射 asset_object 的 `editor_sound_effect` / `editor_background_music`。apply 在一个事务内插入资源、同步两份 legacy layout、递增一次 canvas revision;任一 guard 失败整画布回滚。plan、脚本输出、测试和文档均不得包含生产真实 ID。
|
||||
|
||||
## 5. 兼容与回滚
|
||||
|
||||
- 结构化代码上线后先保持 `legacy` 主读写,允许 schema 与 procedure 先行发布;确认新 API 可用后才开始 backfill 和逐 canvas 激活。
|
||||
- `layers_json` 在迁移和观察期内保留原值,不删除、不改名、不重排现有字段。激活结构化模式不立即清空 legacy JSON。
|
||||
- 回滚前由后端在一个一致 revision 下读取 layer 与 dialog,按 legacy schema 重组并计算 canonical hash。重组结果必须是合法 JSON 且 UTF-8 大小 **不超过 2 MiB**,随后在事务内写回 `editor_canvas.layers_json`、递增 revision、记录回滚审计并把 state 切回 `legacy`。对已处于 `rolled_back` 的重复回滚请求,也必须重新复核 revision、structured hash、layer/dialog 数量、资源引用集合以及 legacy/structured 一致性,任一漂移都 fail-closed。
|
||||
- 超过 2 MiB、包含无法降级字段、hash / 资源引用核对失败或 revision CAS 冲突时,回滚必须拒绝并保持 structured,禁止截断图层、丢弃 dialog 或只切换读取标志。生产二进制回滚应保留能读取 structured 的前向兼容版本,不能把超限 canvas 强行交给旧二进制。
|
||||
- 观察期结束前不删除三张结构化表或 legacy 列。后续是否停止生成 legacy 回滚快照、是否清理旧 JSON,需另行评审和迁移窗口,不随本次结构化上线自动执行。
|
||||
|
||||
## 6. 发布次序
|
||||
|
||||
1. 发布 2 MiB hotfix:先发布不变更 schema 的 SpacetimeDB 模块校验,再部署同一上限和足够 body envelope 的 `api-server`;确认 release manifest、服务状态和健康检查。
|
||||
2. 发布新增三表、revision / CAS、typed 快照重组与迁移 procedure 的 SpacetimeDB 模块;同步 migration table 白名单和生成绑定。此时保持 legacy 主读写。
|
||||
3. 部署可同时读取 legacy / structured 的 `spacetime-client` 与 `api-server`,再部署携带 revision 的前端。混合版本期间未激活 canvas 必须仍可正常编辑,旧 API 写 structured canvas 必须失败而不是无 CAS 覆盖。
|
||||
4. 先对测试账号和单个真实 canvas dry-run、backfill、核对、activate,再按批次扩大;持续观测 CAS conflict、迁移失败、快照缺资源和 worker completion 指标。
|
||||
5. 验收完成并经过观察期后,才把新 canvas 默认设为 structured。legacy 回滚能力继续保留。
|
||||
|
||||
SpacetimeDB 必须先于依赖新 procedure / bindings 的 API 发布;前端必须最后发布。任何阶段失败都停留在当前可读模式,不做跨版本破坏性清理。
|
||||
|
||||
## 7. 验收清单
|
||||
|
||||
- 2 MiB hotfix 后,原先约 254 KiB 的受影响画布和大于 256 KiB 的测试布局可保存并刷新恢复;超过 2 MiB 的 legacy layout 在 API 与 SpacetimeDB 两层都稳定拒绝,错误可观测。
|
||||
- backfill 重跑不会增加 layer / dialog 行,canonical hash、数量和资源引用集合一致;迁移中发生用户写入时 activate CAS 失败并安全重跑。
|
||||
- release 存量抽样中的缺资源 `local-*` 角色动作序列可无损 round-trip;同形状但空帧、相对路径、HTTP / 签名 URL、`data:` / `blob:` 引用必须拒绝。已有资源的 `sourceResourceId == resourceId` 历史自引用应按资源表真相安全剥离,其他来源 ID 或资源字段冲突仍必须拒绝。
|
||||
- release 全量审计暴露的普通缺资源行必须先通过定向 repair dry-run;图片只能复用同工程唯一资源,音频只能从已登记 private asset_object 恢复。修复后同一 plan 全部命中 `already_repaired`,再重跑全量 backfill dry-run,要求所有 canvas 均通过。
|
||||
- structured 模式下 typed 列而非扩展 JSON 决定几何、层级、分组、显示 / 锁定、资源引用和 dialog 状态;两个客户端基于同一 revision 写入时只允许一个成功,冲突方重载后端最新快照,不换上新 revision 原样重放旧整包。细粒度 batch mutation 是取消 2 MiB 兼容入口的后续项,不冒充为本次已完成。
|
||||
- worker completion 当前以读取时 revision 做 CAS,冲突时拒绝覆盖;V2 保存和保存后快照在同一 procedure 结果内返回,避免“已提交但后续 GET 失败”的不确定结果。lease-fenced 资源 / layer / dialog / job 单事务 completion 仍是后续收口项。
|
||||
- structured 快照刷新后,上传参考图、生成结果、占位与 dialog 状态均可恢复;资源存在但布局写入失败时不会伪装为保存成功。
|
||||
- 回滚重组结果经 schema 校验、canonical hash / 资源引用核对且不超过 2 MiB;超限或不一致时明确拒绝且 structured 快照仍可读取。
|
||||
- 完成 `npm run spacetime:generate`、`npm run check:spacetime-runtime-access`、`npm run check:spacetime-schema`、相关 Rust / API / 前端定向测试、`npm run check:encoding` 和 `git diff --check`。
|
||||
@@ -153,7 +153,7 @@ pipeline {
|
||||
|
||||
stage('Archive') {
|
||||
steps {
|
||||
archiveArtifacts artifacts: "build/${env.EFFECTIVE_BUILD_VERSION}/spacetime_module.wasm,build/${env.EFFECTIVE_BUILD_VERSION}/spacetime_module.wasm.sha256,build/${env.EFFECTIVE_BUILD_VERSION}/release-manifest.json,scripts/deploy/production-stdb-publish.sh,scripts/deploy/production-runtime-writer-identity-rotate.mjs,scripts/deploy/maintenance-on.sh,scripts/deploy/maintenance-off.sh,scripts/spacetime-migration-common.mjs,scripts/spacetime-maintain-external-generation-jobs.mjs,scripts/database-backup-to-oss.mjs", fingerprint: true
|
||||
archiveArtifacts artifacts: "build/${env.EFFECTIVE_BUILD_VERSION}/spacetime_module.wasm,build/${env.EFFECTIVE_BUILD_VERSION}/spacetime_module.wasm.sha256,build/${env.EFFECTIVE_BUILD_VERSION}/release-manifest.json,scripts/deploy/production-stdb-publish.sh,scripts/deploy/production-runtime-writer-identity-rotate.mjs,scripts/deploy/maintenance-on.sh,scripts/deploy/maintenance-off.sh,scripts/spacetime-migration-common.mjs,scripts/spacetime-maintain-external-generation-jobs.mjs,scripts/spacetime-migrate-editor-canvas-layout.mjs,scripts/spacetime-repair-editor-canvas-resources.mjs,scripts/database-backup-to-oss.mjs", fingerprint: true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ pipeline {
|
||||
copyArtifacts(
|
||||
projectName: params.BUILD_JOB_NAME,
|
||||
selector: specific(params.BUILD_NUMBER_TO_DEPLOY),
|
||||
filter: "build/${params.BUILD_VERSION}/spacetime_module.wasm,build/${params.BUILD_VERSION}/spacetime_module.wasm.sha256,build/${params.BUILD_VERSION}/release-manifest.json,scripts/deploy/production-stdb-publish.sh,scripts/deploy/production-runtime-writer-identity-rotate.mjs,scripts/deploy/maintenance-on.sh,scripts/deploy/maintenance-off.sh,scripts/spacetime-migration-common.mjs,scripts/spacetime-maintain-external-generation-jobs.mjs,scripts/database-backup-to-oss.mjs",
|
||||
filter: "build/${params.BUILD_VERSION}/spacetime_module.wasm,build/${params.BUILD_VERSION}/spacetime_module.wasm.sha256,build/${params.BUILD_VERSION}/release-manifest.json,scripts/deploy/production-stdb-publish.sh,scripts/deploy/production-runtime-writer-identity-rotate.mjs,scripts/deploy/maintenance-on.sh,scripts/deploy/maintenance-off.sh,scripts/spacetime-migration-common.mjs,scripts/spacetime-maintain-external-generation-jobs.mjs,scripts/spacetime-migrate-editor-canvas-layout.mjs,scripts/spacetime-repair-editor-canvas-resources.mjs,scripts/database-backup-to-oss.mjs",
|
||||
target: '.',
|
||||
fingerprintArtifacts: true
|
||||
)
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
"admin-web:preview": "npm --prefix apps/admin-web run preview --",
|
||||
"spacetime:generate": "node scripts/generate-spacetime-bindings.mjs",
|
||||
"spacetime:external-generation:maintain": "node scripts/spacetime-maintain-external-generation-jobs.mjs",
|
||||
"spacetime:editor-canvas-layout:migrate": "node scripts/spacetime-migrate-editor-canvas-layout.mjs",
|
||||
"spacetime:editor-canvas-resources:repair": "node scripts/spacetime-repair-editor-canvas-resources.mjs",
|
||||
"spacetime:wechat-virtual-payment:reconcile": "node scripts/reconcile-wechat-virtual-payment-order.mjs",
|
||||
"check:wechat-virtual-payment-reconcile": "node scripts/check-wechat-virtual-payment-reconcile.mjs",
|
||||
"check:api-server-env": "node scripts/check-api-server-env.mjs",
|
||||
|
||||
@@ -521,6 +521,46 @@ const checks = [
|
||||
includes: 'completed_before_micros: encodeSpacetimeCliOption(',
|
||||
reason: '历史 payload 截止时间必须编码为 CLI SATS Option,确保事故时间过滤可调用。',
|
||||
},
|
||||
{
|
||||
file: 'scripts/spacetime-migrate-editor-canvas-layout.mjs',
|
||||
includes: 'dry_run: !options.apply',
|
||||
reason: '图片画布存量迁移必须默认 dry-run,只有显式 --apply 才写入。',
|
||||
},
|
||||
{
|
||||
file: 'scripts/spacetime-repair-editor-canvas-resources.mjs',
|
||||
includes: 'buildProcedureInput(canvas, updatedAtMicros, !options.apply)',
|
||||
reason: '图片画布资源修复必须默认 dry-run,只有显式 --apply 才写入。',
|
||||
},
|
||||
{
|
||||
file: 'scripts/spacetime-repair-editor-canvas-resources.mjs',
|
||||
includes: '--confirm-plan-sha256',
|
||||
reason: '图片画布资源修复 apply 必须绑定同一份 dry-run plan 摘要。',
|
||||
},
|
||||
{
|
||||
file: 'scripts/spacetime-repair-editor-canvas-resources.mjs',
|
||||
includes: '(metadata.mode & 0o777) !== 0o600',
|
||||
reason: '包含真实资源 ID 的修复 plan 必须使用仓库外 0600 文件。',
|
||||
},
|
||||
{
|
||||
file: 'jenkins/Jenkinsfile.production-stdb-module-build',
|
||||
includes: 'scripts/spacetime-migrate-editor-canvas-layout.mjs',
|
||||
reason: 'Stdb Build 必须归档图片画布存量迁移脚本。',
|
||||
},
|
||||
{
|
||||
file: 'jenkins/Jenkinsfile.production-stdb-module-publish',
|
||||
includes: 'scripts/spacetime-migrate-editor-canvas-layout.mjs',
|
||||
reason: 'Stdb Publish 必须从同一上游制品复制图片画布存量迁移脚本。',
|
||||
},
|
||||
{
|
||||
file: 'jenkins/Jenkinsfile.production-stdb-module-build',
|
||||
includes: 'scripts/spacetime-repair-editor-canvas-resources.mjs',
|
||||
reason: 'Stdb Build 必须归档图片画布资源修复脚本。',
|
||||
},
|
||||
{
|
||||
file: 'jenkins/Jenkinsfile.production-stdb-module-publish',
|
||||
includes: 'scripts/spacetime-repair-editor-canvas-resources.mjs',
|
||||
reason: 'Stdb Publish 必须从同一上游制品复制图片画布资源修复脚本。',
|
||||
},
|
||||
{
|
||||
file: 'scripts/deploy-rust-remote.sh',
|
||||
excludes:
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import {
|
||||
callSpacetimeProcedureViaCli,
|
||||
ensureProcedureOk,
|
||||
} from './spacetime-migration-common.mjs';
|
||||
|
||||
const ACTIONS = new Map([
|
||||
['backfill', 'backfill_editor_canvas_layout_and_return'],
|
||||
['activate', 'activate_editor_canvas_layout_and_return'],
|
||||
['rollback', 'rollback_editor_canvas_layout_and_return'],
|
||||
]);
|
||||
|
||||
function usage() {
|
||||
return `用法:
|
||||
node scripts/spacetime-migrate-editor-canvas-layout.mjs \\
|
||||
--database <name> --project-id <projectId> --owner-user-id <userId> \\
|
||||
--action <backfill|activate|rollback> [--server <name-or-url>] [--apply]
|
||||
|
||||
默认只执行 dry-run 校验,不修改数据;追加 --apply 才写入。
|
||||
必须使用已授权 database migration operator 的 spacetime CLI 登录态,
|
||||
并显式传入 --server 或设置 GENARRATIVE_SPACETIME_SERVER。
|
||||
|
||||
存量项目的安全顺序:
|
||||
1. backfill dry-run
|
||||
2. backfill --apply
|
||||
3. activate dry-run
|
||||
4. activate --apply
|
||||
需要回滚时先执行 rollback dry-run,确认 hash / revision / 2 MiB 门禁通过后再 --apply。`;
|
||||
}
|
||||
|
||||
function parseOptions(argv) {
|
||||
const options = {
|
||||
action: '',
|
||||
apply: false,
|
||||
database: process.env.GENARRATIVE_SPACETIME_DATABASE || '',
|
||||
ownerUserId: '',
|
||||
passthrough: [],
|
||||
projectId: '',
|
||||
server: process.env.GENARRATIVE_SPACETIME_SERVER || '',
|
||||
serverUrl: process.env.GENARRATIVE_SPACETIME_SERVER_URL || '',
|
||||
};
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
const readValue = () => {
|
||||
const value = argv[index + 1];
|
||||
if (!value || value.startsWith('--')) {
|
||||
throw new Error(`${arg} 缺少参数值。`);
|
||||
}
|
||||
index += 1;
|
||||
return value.trim();
|
||||
};
|
||||
if (arg === '--action') {
|
||||
options.action = readValue();
|
||||
} else if (arg === '--database') {
|
||||
options.database = readValue();
|
||||
} else if (arg === '--project-id') {
|
||||
options.projectId = readValue();
|
||||
} else if (arg === '--owner-user-id') {
|
||||
options.ownerUserId = readValue();
|
||||
} else if (arg === '--server') {
|
||||
options.server = readValue();
|
||||
} else if (arg === '--server-url') {
|
||||
options.serverUrl = readValue();
|
||||
} else if (arg === '--apply') {
|
||||
options.apply = true;
|
||||
} else if (arg === '--help' || arg === '-h') {
|
||||
options.help = true;
|
||||
} else if (arg === '--no-config' || arg === '--anonymous') {
|
||||
options.passthrough.push(arg);
|
||||
} else {
|
||||
throw new Error(`未知参数: ${arg}`);
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
try {
|
||||
const options = parseOptions(process.argv.slice(2));
|
||||
if (options.help) {
|
||||
console.log(usage());
|
||||
process.exit(0);
|
||||
}
|
||||
if (!options.database || !options.projectId || !options.ownerUserId) {
|
||||
throw new Error('--database、--project-id 和 --owner-user-id 均为必填。');
|
||||
}
|
||||
if (!options.server && !options.serverUrl) {
|
||||
throw new Error('必须显式传入 --server / --server-url,不使用默认 cloud target。');
|
||||
}
|
||||
const procedureName = ACTIONS.get(options.action);
|
||||
if (!procedureName) {
|
||||
throw new Error('--action 只支持 backfill、activate 或 rollback。');
|
||||
}
|
||||
const result = await callSpacetimeProcedureViaCli(options, procedureName, {
|
||||
project_id: options.projectId,
|
||||
owner_user_id: options.ownerUserId,
|
||||
updated_at_micros: Date.now() * 1_000,
|
||||
dry_run: !options.apply,
|
||||
});
|
||||
ensureProcedureOk(result);
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
action: options.action,
|
||||
applied: options.apply,
|
||||
procedure: procedureName,
|
||||
...result,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
if (!options.apply) {
|
||||
console.log('dry-run 已通过;确认输出后追加 --apply 重跑同一动作。');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[spacetime:editor-canvas-layout:migrate] ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -292,6 +292,36 @@ function normalizeProcedureResult(value, procedureName) {
|
||||
}
|
||||
|
||||
function normalizeSatsProduct(value, procedureName) {
|
||||
if (
|
||||
procedureName === 'repair_editor_canvas_resources_and_return' &&
|
||||
value.length === 3
|
||||
) {
|
||||
return {
|
||||
ok: normalizeSatsValue(value[0]),
|
||||
repair: normalizeEditorCanvasResourceRepairSnapshot(
|
||||
normalizeSatsOption(value[1]),
|
||||
),
|
||||
error_message: normalizeSatsOption(value[2]),
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
[
|
||||
'backfill_editor_canvas_layout_and_return',
|
||||
'activate_editor_canvas_layout_and_return',
|
||||
'rollback_editor_canvas_layout_and_return',
|
||||
].includes(procedureName) &&
|
||||
value.length === 3
|
||||
) {
|
||||
return {
|
||||
ok: normalizeSatsValue(value[0]),
|
||||
migration: normalizeEditorCanvasLayoutMigrationSnapshot(
|
||||
normalizeSatsOption(value[1]),
|
||||
),
|
||||
error_message: normalizeSatsOption(value[2]),
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
procedureName === 'backfill_external_generation_job_summaries_and_return' &&
|
||||
value.length === 8
|
||||
@@ -357,6 +387,40 @@ function normalizeSatsProduct(value, procedureName) {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeEditorCanvasLayoutMigrationSnapshot(value) {
|
||||
if (!Array.isArray(value) || value.length !== 11) {
|
||||
return value;
|
||||
}
|
||||
return {
|
||||
canvas_id: normalizeSatsValue(value[0]),
|
||||
project_id: normalizeSatsValue(value[1]),
|
||||
source_layout_sha256: normalizeSatsValue(value[2]),
|
||||
structured_layout_sha256: normalizeSatsValue(value[3]),
|
||||
verified_revision: normalizeSatsValue(value[4]),
|
||||
layer_count: normalizeSatsValue(value[5]),
|
||||
dialog_count: normalizeSatsValue(value[6]),
|
||||
resource_refs_sha256: normalizeSatsValue(value[7]),
|
||||
migration_version: normalizeSatsValue(value[8]),
|
||||
status: normalizeSatsValue(value[9]),
|
||||
revision: normalizeSatsValue(value[10]),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeEditorCanvasResourceRepairSnapshot(value) {
|
||||
if (!Array.isArray(value) || value.length !== 7) {
|
||||
return value;
|
||||
}
|
||||
return {
|
||||
dry_run: normalizeSatsValue(value[0]),
|
||||
already_repaired: normalizeSatsValue(value[1]),
|
||||
matched_layer_count: normalizeSatsValue(value[2]),
|
||||
remapped_layer_count: normalizeSatsValue(value[3]),
|
||||
restored_resource_count: normalizeSatsValue(value[4]),
|
||||
removed_source_resource_id_count: normalizeSatsValue(value[5]),
|
||||
revision: normalizeSatsValue(value[6]),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeSatsValue(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => normalizeSatsValue(item));
|
||||
|
||||
@@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
buildSpacetimeCallArgs,
|
||||
encodeSpacetimeCliOption,
|
||||
parseProcedureResult,
|
||||
} from './spacetime-migration-common.mjs';
|
||||
|
||||
describe('SpacetimeDB CLI SATS option encoding', () => {
|
||||
@@ -33,4 +34,69 @@ describe('SpacetimeDB CLI SATS option encoding', () => {
|
||||
expect(input.cursor_job_id).toEqual([0, 'job-2']);
|
||||
expect(input.completed_before_micros).toEqual([0, 123]);
|
||||
});
|
||||
|
||||
it('normalizes editor canvas migration procedure results', () => {
|
||||
const result = parseProcedureResult(
|
||||
JSON.stringify([
|
||||
true,
|
||||
[
|
||||
0,
|
||||
[
|
||||
'canvas-1',
|
||||
'project-1',
|
||||
'source-hash',
|
||||
'structured-hash',
|
||||
7,
|
||||
2,
|
||||
1,
|
||||
'resource-hash',
|
||||
1,
|
||||
'backfilled',
|
||||
7,
|
||||
],
|
||||
],
|
||||
[1],
|
||||
]),
|
||||
'backfill_editor_canvas_layout_and_return',
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: true,
|
||||
migration: {
|
||||
canvas_id: 'canvas-1',
|
||||
project_id: 'project-1',
|
||||
source_layout_sha256: 'source-hash',
|
||||
structured_layout_sha256: 'structured-hash',
|
||||
verified_revision: 7,
|
||||
layer_count: 2,
|
||||
dialog_count: 1,
|
||||
resource_refs_sha256: 'resource-hash',
|
||||
migration_version: 1,
|
||||
status: 'backfilled',
|
||||
revision: 7,
|
||||
},
|
||||
error_message: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('normalizes editor canvas resource repair procedure results', () => {
|
||||
const result = parseProcedureResult(
|
||||
JSON.stringify([true, [0, [true, false, 2, 0, 0, 0, 7]], [1]]),
|
||||
'repair_editor_canvas_resources_and_return',
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: true,
|
||||
repair: {
|
||||
dry_run: true,
|
||||
already_repaired: false,
|
||||
matched_layer_count: 2,
|
||||
remapped_layer_count: 0,
|
||||
restored_resource_count: 0,
|
||||
removed_source_resource_id_count: 0,
|
||||
revision: 7,
|
||||
},
|
||||
error_message: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,477 @@
|
||||
import {
|
||||
chmod,
|
||||
mkdir,
|
||||
mkdtemp,
|
||||
rm,
|
||||
symlink,
|
||||
writeFile,
|
||||
} from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
buildProcedureInput,
|
||||
executeRepairPlan,
|
||||
parseOptions,
|
||||
readRepairPlan,
|
||||
runCli,
|
||||
validateRepairPlan,
|
||||
} from './spacetime-repair-editor-canvas-resources.mjs';
|
||||
|
||||
const SHA_A = 'a'.repeat(64);
|
||||
const SHA_B = 'b'.repeat(64);
|
||||
const PLAN_SHA = 'c'.repeat(64);
|
||||
const temporaryRoots: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
process.exitCode = undefined;
|
||||
await Promise.all(
|
||||
temporaryRoots
|
||||
.splice(0)
|
||||
.map((root) => rm(root, { recursive: true, force: true })),
|
||||
);
|
||||
});
|
||||
|
||||
function action(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
action_kind: 'reuse_project_resource',
|
||||
layer_id: 'private-layer-id',
|
||||
expected_resource_id: 'missing-private-resource-id',
|
||||
replacement_resource_id: 'replacement-private-resource-id',
|
||||
expected_layout_source_resource_id: 'source-private-resource-id',
|
||||
asset_object_id: null,
|
||||
asset_kind: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function canvas(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
canvas_id: 'private-canvas-id',
|
||||
project_id: 'private-project-id',
|
||||
owner_user_id: 'private-owner-id',
|
||||
expected_revision: 7,
|
||||
expected_canvas_layout_sha256: SHA_A,
|
||||
expected_project_layout_sha256: SHA_B,
|
||||
actions: [action()],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function planValue(canvases = [canvas()]) {
|
||||
return {
|
||||
schema_version: 1,
|
||||
expected_canvas_count: canvases.length,
|
||||
expected_action_count: canvases.reduce(
|
||||
(sum, item) =>
|
||||
sum + (Array.isArray(item.actions) ? item.actions.length : 0),
|
||||
0,
|
||||
),
|
||||
canvases,
|
||||
};
|
||||
}
|
||||
|
||||
function executionOptions(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
apply: false,
|
||||
confirmPlanSha256: '',
|
||||
database: 'genarrative-prod',
|
||||
passthrough: [],
|
||||
planFile: '/outside/repair-plan.json',
|
||||
server: 'http://127.0.0.1:3101',
|
||||
serverUrl: '',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function procedureResult(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
ok: true,
|
||||
repair: {
|
||||
dry_run: true,
|
||||
already_repaired: false,
|
||||
matched_layer_count: 1,
|
||||
remapped_layer_count: 1,
|
||||
restored_resource_count: 0,
|
||||
removed_source_resource_id_count: 1,
|
||||
revision: 7,
|
||||
},
|
||||
error_message: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('spacetime editor canvas resource repair options', () => {
|
||||
it('defaults to dry-run and requires an explicit apply flag', () => {
|
||||
const options = parseOptions([
|
||||
'--database',
|
||||
'genarrative-prod',
|
||||
'--server-url',
|
||||
'http://127.0.0.1:3101',
|
||||
'--plan-file',
|
||||
'/tmp/repair-plan.json',
|
||||
]);
|
||||
|
||||
expect(options.apply).toBe(false);
|
||||
expect(options.confirmPlanSha256).toBe('');
|
||||
expect(options.planFile).toBe('/tmp/repair-plan.json');
|
||||
});
|
||||
|
||||
it('records explicit apply and lowercases the confirmed plan hash', () => {
|
||||
const options = parseOptions([
|
||||
'--apply',
|
||||
'--confirm-plan-sha256',
|
||||
SHA_A.toUpperCase(),
|
||||
]);
|
||||
|
||||
expect(options.apply).toBe(true);
|
||||
expect(options.confirmPlanSha256).toBe(SHA_A);
|
||||
});
|
||||
|
||||
it('rejects anonymous procedure calls', () => {
|
||||
expect(() => parseOptions(['--anonymous'])).toThrow('未知参数');
|
||||
});
|
||||
});
|
||||
|
||||
describe('spacetime editor canvas resource repair plan', () => {
|
||||
it('validates and normalizes the two supported action kinds', () => {
|
||||
const plan = validateRepairPlan(
|
||||
planValue([
|
||||
canvas({
|
||||
actions: [
|
||||
action(),
|
||||
action({
|
||||
action_kind: 'restore_project_resource',
|
||||
layer_id: 'private-audio-layer-id',
|
||||
expected_resource_id: 'missing-private-audio-resource-id',
|
||||
replacement_resource_id: null,
|
||||
expected_layout_source_resource_id: null,
|
||||
asset_object_id: 'private-asset-object-id',
|
||||
asset_kind: 'background-music',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
expect(plan.expected_action_count).toBe(2);
|
||||
expect(plan.canvases[0].actions).toMatchObject([
|
||||
{ action_kind: 'reuse_project_resource' },
|
||||
{
|
||||
action_kind: 'restore_project_resource',
|
||||
asset_kind: 'background-music',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('rejects count mismatches, duplicate layers, and invalid kind-specific fields', () => {
|
||||
expect(() =>
|
||||
validateRepairPlan({ ...planValue(), expected_action_count: 2 }),
|
||||
).toThrow('expected_action_count');
|
||||
expect(() =>
|
||||
validateRepairPlan(
|
||||
planValue([
|
||||
canvas({
|
||||
actions: [action(), action({ replacement_resource_id: 'other' })],
|
||||
}),
|
||||
]),
|
||||
),
|
||||
).toThrow('layer_id 在同一画布内重复');
|
||||
expect(() =>
|
||||
validateRepairPlan(
|
||||
planValue([
|
||||
canvas({
|
||||
actions: [
|
||||
action({
|
||||
action_kind: 'restore_project_resource',
|
||||
replacement_resource_id: null,
|
||||
expected_layout_source_resource_id: null,
|
||||
asset_object_id: 'private-asset-object-id',
|
||||
asset_kind: 'image',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
]),
|
||||
),
|
||||
).toThrow('asset_kind 只支持');
|
||||
});
|
||||
|
||||
it('reads only an external current-user 0600 regular plan and returns its hash', async () => {
|
||||
const root = await makeTemporaryRoot();
|
||||
const planPath = path.join(root, 'repair-plan.json');
|
||||
await writeFile(planPath, `${JSON.stringify(planValue())}\n`, 'utf8');
|
||||
await chmod(planPath, 0o600);
|
||||
|
||||
const result = await readRepairPlan(planPath);
|
||||
|
||||
expect(result.plan.expected_canvas_count).toBe(1);
|
||||
expect(result.planSha256).toMatch(/^[0-9a-f]{64}$/u);
|
||||
});
|
||||
|
||||
it('rejects permissive modes, repository paths, and symlink path components', async () => {
|
||||
const root = await makeTemporaryRoot();
|
||||
const planPath = path.join(root, 'repair-plan.json');
|
||||
await writeFile(planPath, JSON.stringify(planValue()), 'utf8');
|
||||
await chmod(planPath, 0o644);
|
||||
await expect(readRepairPlan(planPath)).rejects.toThrow('0600');
|
||||
|
||||
await chmod(planPath, 0o600);
|
||||
await expect(readRepairPlan(planPath, { repoRoot: root })).rejects.toThrow(
|
||||
'必须位于仓库外',
|
||||
);
|
||||
|
||||
const realDirectory = path.join(root, 'real');
|
||||
const linkedDirectory = path.join(root, 'linked');
|
||||
await mkdir(realDirectory);
|
||||
const linkedPlanPath = path.join(realDirectory, 'linked-plan.json');
|
||||
await writeFile(linkedPlanPath, JSON.stringify(planValue()), 'utf8');
|
||||
await chmod(linkedPlanPath, 0o600);
|
||||
await symlink(realDirectory, linkedDirectory);
|
||||
await expect(
|
||||
readRepairPlan(path.join(linkedDirectory, 'linked-plan.json')),
|
||||
).rejects.toThrow('路径链不能包含符号链接');
|
||||
});
|
||||
});
|
||||
|
||||
describe('spacetime editor canvas resource repair execution', () => {
|
||||
it('splits the two action kinds into typed procedure inputs', () => {
|
||||
const plan = validateRepairPlan(
|
||||
planValue([
|
||||
canvas({
|
||||
actions: [
|
||||
action(),
|
||||
action({
|
||||
action_kind: 'restore_project_resource',
|
||||
layer_id: 'private-audio-layer-id',
|
||||
expected_resource_id: 'missing-private-audio-resource-id',
|
||||
replacement_resource_id: null,
|
||||
expected_layout_source_resource_id: null,
|
||||
asset_object_id: 'private-asset-object-id',
|
||||
asset_kind: 'sound-effect',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
const input = buildProcedureInput(plan.canvases[0], 123, true);
|
||||
|
||||
expect(input.remaps).toEqual([
|
||||
{
|
||||
layer_id: 'private-layer-id',
|
||||
expected_missing_resource_id: 'missing-private-resource-id',
|
||||
replacement_resource_id: 'replacement-private-resource-id',
|
||||
expected_layout_source_resource_id: 'source-private-resource-id',
|
||||
},
|
||||
]);
|
||||
expect(input.restores).toEqual([
|
||||
{
|
||||
layer_id: 'private-audio-layer-id',
|
||||
resource_id: 'missing-private-audio-resource-id',
|
||||
asset_object_id: 'private-asset-object-id',
|
||||
asset_kind: 'sound-effect',
|
||||
},
|
||||
]);
|
||||
expect(input.canvas_id).toBe('private-canvas-id');
|
||||
expect(input.updated_at_micros).toBe(123);
|
||||
expect(input.dry_run).toBe(true);
|
||||
});
|
||||
|
||||
it('calls one procedure per canvas and emits only anonymous aggregate output', async () => {
|
||||
const plan = validateRepairPlan(
|
||||
planValue([
|
||||
canvas(),
|
||||
canvas({
|
||||
canvas_id: 'second-private-canvas-id',
|
||||
project_id: 'second-private-project-id',
|
||||
actions: [
|
||||
action({
|
||||
layer_id: 'second-private-layer-id',
|
||||
expected_resource_id: 'second-private-resource-id',
|
||||
replacement_resource_id: 'second-private-replacement-id',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
]),
|
||||
);
|
||||
const calls: unknown[] = [];
|
||||
|
||||
const summary = await executeRepairPlan({
|
||||
options: executionOptions(),
|
||||
plan,
|
||||
planSha256: PLAN_SHA,
|
||||
updatedAtMicros: 123,
|
||||
callProcedure: async (options, procedureName, input) => {
|
||||
calls.push({ options, procedureName, input });
|
||||
return procedureResult();
|
||||
},
|
||||
});
|
||||
|
||||
expect(calls).toHaveLength(2);
|
||||
expect(calls).toMatchObject([
|
||||
{ procedureName: 'repair_editor_canvas_resources_and_return' },
|
||||
{ procedureName: 'repair_editor_canvas_resources_and_return' },
|
||||
]);
|
||||
expect(summary).toMatchObject({
|
||||
ok: true,
|
||||
dry_run: true,
|
||||
canvas_count: 2,
|
||||
action_count: 2,
|
||||
processed_canvas_count: 2,
|
||||
matched_layer_count: 2,
|
||||
updated_layer_count: 2,
|
||||
inserted_resource_count: 0,
|
||||
});
|
||||
const publicOutput = JSON.stringify(summary);
|
||||
for (const privateValue of [
|
||||
'private-project-id',
|
||||
'private-layer-id',
|
||||
'missing-private-resource-id',
|
||||
'replacement-private-resource-id',
|
||||
]) {
|
||||
expect(publicOutput).not.toContain(privateValue);
|
||||
}
|
||||
});
|
||||
|
||||
it('requires apply to match the dry-run plan hash', async () => {
|
||||
const plan = validateRepairPlan(planValue());
|
||||
const callProcedure = async () => procedureResult();
|
||||
|
||||
await expect(
|
||||
executeRepairPlan({
|
||||
options: executionOptions({ apply: true, confirmPlanSha256: SHA_A }),
|
||||
plan,
|
||||
planSha256: PLAN_SHA,
|
||||
callProcedure,
|
||||
}),
|
||||
).rejects.toThrow('与当前 plan 不一致');
|
||||
});
|
||||
|
||||
it('stops apply at the first failed canvas without exposing procedure messages', async () => {
|
||||
const plan = validateRepairPlan(
|
||||
planValue([
|
||||
canvas(),
|
||||
canvas({
|
||||
canvas_id: 'second-private-canvas-id',
|
||||
project_id: 'second-private-project-id',
|
||||
actions: [action({ layer_id: 'second-private-layer-id' })],
|
||||
}),
|
||||
]),
|
||||
);
|
||||
let callCount = 0;
|
||||
|
||||
await expect(
|
||||
executeRepairPlan({
|
||||
options: executionOptions({
|
||||
apply: true,
|
||||
confirmPlanSha256: PLAN_SHA,
|
||||
}),
|
||||
plan,
|
||||
planSha256: PLAN_SHA,
|
||||
callProcedure: async () => {
|
||||
callCount += 1;
|
||||
return procedureResult({
|
||||
ok: false,
|
||||
repair: null,
|
||||
error_message:
|
||||
'contains private-project-id and private-resource-id',
|
||||
});
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
'第 1 个画布修复失败,error_code=repair_rejected;此前成功处理 0 个画布',
|
||||
);
|
||||
expect(callCount).toBe(1);
|
||||
});
|
||||
|
||||
it('continues dry-run validation and reports only ordinal-safe failure codes', async () => {
|
||||
const plan = validateRepairPlan(
|
||||
planValue([
|
||||
canvas(),
|
||||
canvas({
|
||||
canvas_id: 'second-private-canvas-id',
|
||||
project_id: 'second-private-project-id',
|
||||
actions: [action({ layer_id: 'second-private-layer-id' })],
|
||||
}),
|
||||
]),
|
||||
);
|
||||
let callCount = 0;
|
||||
|
||||
const summary = await executeRepairPlan({
|
||||
options: executionOptions(),
|
||||
plan,
|
||||
planSha256: PLAN_SHA,
|
||||
callProcedure: async () => {
|
||||
callCount += 1;
|
||||
return callCount === 1
|
||||
? procedureResult({
|
||||
ok: false,
|
||||
repair: null,
|
||||
error_message: 'contains private-project-id',
|
||||
})
|
||||
: procedureResult();
|
||||
},
|
||||
});
|
||||
|
||||
expect(callCount).toBe(2);
|
||||
expect(summary).toMatchObject({
|
||||
ok: false,
|
||||
processed_canvas_count: 1,
|
||||
failures: [{ canvas_ordinal: 1, error_code: 'repair_rejected' }],
|
||||
});
|
||||
expect(JSON.stringify(summary)).not.toContain('private-project-id');
|
||||
});
|
||||
|
||||
it('automatically dry-runs the same plan after apply and requires already repaired', async () => {
|
||||
const plan = validateRepairPlan(planValue());
|
||||
const originalLog = console.log;
|
||||
console.log = () => {};
|
||||
let callCount = 0;
|
||||
try {
|
||||
const summary = await runCli(
|
||||
[
|
||||
'--database',
|
||||
'genarrative-prod',
|
||||
'--server-url',
|
||||
'http://127.0.0.1:3101',
|
||||
'--plan-file',
|
||||
'/outside/repair-plan.json',
|
||||
'--apply',
|
||||
'--confirm-plan-sha256',
|
||||
PLAN_SHA,
|
||||
],
|
||||
{
|
||||
readPlan: async () => ({ plan, planSha256: PLAN_SHA }),
|
||||
callProcedure: async (_options, _procedureName, input) => {
|
||||
callCount += 1;
|
||||
const dryRun = input.dry_run === true;
|
||||
return procedureResult({
|
||||
repair: {
|
||||
...procedureResult().repair,
|
||||
dry_run: dryRun,
|
||||
already_repaired: dryRun,
|
||||
remapped_layer_count: dryRun ? 0 : 1,
|
||||
removed_source_resource_id_count: dryRun ? 0 : 1,
|
||||
revision: 8,
|
||||
},
|
||||
});
|
||||
},
|
||||
updatedAtMicros: 123,
|
||||
},
|
||||
);
|
||||
|
||||
expect(callCount).toBe(2);
|
||||
expect(summary).toMatchObject({ ok: true, dry_run: false });
|
||||
} finally {
|
||||
console.log = originalLog;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
async function makeTemporaryRoot() {
|
||||
const root = await mkdtemp(path.join(tmpdir(), 'editor-canvas-repair-'));
|
||||
temporaryRoots.push(root);
|
||||
return root;
|
||||
}
|
||||
@@ -42,14 +42,14 @@ use spacetime_client::{
|
||||
EditorAssetMediaRepairRecordInput, EditorAssetRecord, EditorAssetUpdateRecordInput,
|
||||
EditorCanvasRecord, EditorCanvasViewportRecord, EditorProjectCreateRecordInput,
|
||||
EditorProjectDeleteRecordInput, EditorProjectGetRecordInput,
|
||||
EditorProjectLayoutSaveRecordInput, EditorProjectRecord, EditorProjectRenameRecordInput,
|
||||
EditorProjectResourceCreateRecordInput, EditorProjectResourceMediaRepairRecordInput,
|
||||
EditorProjectResourceRecord, EditorProjectResourceShowcaseUpdateRecordInput,
|
||||
EditorShowcaseAssetLikeToggleRecordInput, EditorShowcaseAssetPublicListRecordInput,
|
||||
EditorShowcaseAssetRecord, EditorShowcaseAssetSubmitRecordInput,
|
||||
EditorShowcaseCampaignConfigGetRecordInput, EditorShowcaseCampaignConfigRecord,
|
||||
ExternalGenerationJobPhaseUpdateError, ExternalGenerationJobPhaseUpdateRecordInput,
|
||||
SpacetimeClientError,
|
||||
EditorProjectLayoutSaveRecordInput, EditorProjectLayoutSaveV2RecordInput, EditorProjectRecord,
|
||||
EditorProjectRenameRecordInput, EditorProjectResourceCreateRecordInput,
|
||||
EditorProjectResourceMediaRepairRecordInput, EditorProjectResourceRecord,
|
||||
EditorProjectResourceShowcaseUpdateRecordInput, EditorShowcaseAssetLikeToggleRecordInput,
|
||||
EditorShowcaseAssetPublicListRecordInput, EditorShowcaseAssetRecord,
|
||||
EditorShowcaseAssetSubmitRecordInput, EditorShowcaseCampaignConfigGetRecordInput,
|
||||
EditorShowcaseCampaignConfigRecord, ExternalGenerationJobPhaseUpdateError,
|
||||
ExternalGenerationJobPhaseUpdateRecordInput, SpacetimeClientError,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -166,6 +166,7 @@ pub struct EditorCanvasViewportPayload {
|
||||
pub struct EditorProjectLayoutSaveRequest {
|
||||
pub(crate) viewport: EditorCanvasViewportPayload,
|
||||
pub(crate) layers: Value,
|
||||
pub(crate) expected_revision: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -454,6 +455,8 @@ pub struct EditorProjectResponse {
|
||||
pub struct EditorProjectLayoutSaveResponse {
|
||||
project_id: String,
|
||||
canvas_id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
revision: Option<u64>,
|
||||
updated_at: String,
|
||||
}
|
||||
|
||||
@@ -743,6 +746,10 @@ pub struct EditorCanvasPayload {
|
||||
title: String,
|
||||
viewport: EditorCanvasViewportPayload,
|
||||
layers: Value,
|
||||
revision: u64,
|
||||
layout_storage_version: u32,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
background_color: Option<String>,
|
||||
created_at: String,
|
||||
updated_at: String,
|
||||
}
|
||||
@@ -1048,25 +1055,53 @@ pub async fn save_editor_project_layout(
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Json(payload): Json<EditorProjectLayoutSaveRequest>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
let expected_revision = payload.expected_revision;
|
||||
let viewport = payload.viewport.into_record();
|
||||
let layers_json = serialize_editor_layers(payload.layers)?;
|
||||
let ack = state
|
||||
.spacetime_client()
|
||||
.save_editor_project_layout_ack(EditorProjectLayoutSaveRecordInput {
|
||||
project_id,
|
||||
owner_user_id: authenticated.claims().user_id().to_string(),
|
||||
viewport: payload.viewport.into_record(),
|
||||
layers_json,
|
||||
updated_at_micros: current_utc_micros(),
|
||||
})
|
||||
.await
|
||||
.map_err(map_editor_project_error)?;
|
||||
let owner_user_id = authenticated.claims().user_id().to_string();
|
||||
let updated_at_micros = current_utc_micros();
|
||||
let (project_id, canvas_id, revision, updated_at) =
|
||||
if let Some(expected_revision) = expected_revision {
|
||||
let ack = state
|
||||
.spacetime_client()
|
||||
.save_editor_project_layout_v2_ack(EditorProjectLayoutSaveV2RecordInput {
|
||||
project_id,
|
||||
owner_user_id,
|
||||
viewport,
|
||||
layers_json,
|
||||
expected_revision,
|
||||
updated_at_micros,
|
||||
})
|
||||
.await
|
||||
.map_err(map_editor_project_error)?;
|
||||
(
|
||||
ack.project_id,
|
||||
ack.canvas_id,
|
||||
Some(ack.revision),
|
||||
ack.updated_at,
|
||||
)
|
||||
} else {
|
||||
let ack = state
|
||||
.spacetime_client()
|
||||
.save_editor_project_layout_ack(EditorProjectLayoutSaveRecordInput {
|
||||
project_id,
|
||||
owner_user_id,
|
||||
viewport,
|
||||
layers_json,
|
||||
updated_at_micros,
|
||||
})
|
||||
.await
|
||||
.map_err(map_editor_project_error)?;
|
||||
(ack.project_id, ack.canvas_id, None, ack.updated_at)
|
||||
};
|
||||
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
EditorProjectLayoutSaveResponse {
|
||||
project_id: ack.project_id,
|
||||
canvas_id: ack.canvas_id,
|
||||
updated_at: ack.updated_at,
|
||||
project_id,
|
||||
canvas_id,
|
||||
revision,
|
||||
updated_at,
|
||||
},
|
||||
))
|
||||
}
|
||||
@@ -5730,6 +5765,9 @@ fn editor_canvas_payload_from_record(
|
||||
scale: record.viewport.scale,
|
||||
},
|
||||
layers: sanitize_editor_payload_media(record.layers, resources),
|
||||
revision: record.revision,
|
||||
layout_storage_version: record.layout_storage_version,
|
||||
background_color: record.background_color,
|
||||
created_at: record.created_at,
|
||||
updated_at: record.updated_at,
|
||||
}
|
||||
@@ -6206,6 +6244,7 @@ pub(crate) async fn complete_editor_canvas_generation(
|
||||
.await
|
||||
.map_err(map_editor_project_error)?;
|
||||
let project = repair_editor_project_record_inline_media(state, project).await;
|
||||
let expected_revision = project.canvas.revision;
|
||||
let viewport = project.viewport.clone();
|
||||
let project_payload = editor_project_payload_from_record(project);
|
||||
let Some(placeholder) =
|
||||
@@ -6226,17 +6265,15 @@ pub(crate) async fn complete_editor_canvas_generation(
|
||||
if !completion_result.changed {
|
||||
return Ok(None);
|
||||
}
|
||||
let saved = state
|
||||
.spacetime_client()
|
||||
.save_editor_project_layout(EditorProjectLayoutSaveRecordInput {
|
||||
project_id: project_id.to_string(),
|
||||
owner_user_id: owner_user_id.to_string(),
|
||||
viewport,
|
||||
layers_json: serialize_editor_layers(completion_result.layers)?,
|
||||
updated_at_micros: current_utc_micros(),
|
||||
})
|
||||
.await
|
||||
.map_err(map_editor_project_error)?;
|
||||
let saved = save_editor_project_layout_with_revision_and_get(
|
||||
state,
|
||||
project_id,
|
||||
owner_user_id,
|
||||
viewport,
|
||||
completion_result.layers,
|
||||
expected_revision,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Some(editor_project_payload_from_record(saved)))
|
||||
}
|
||||
@@ -6266,6 +6303,7 @@ pub(crate) async fn complete_editor_canvas_generation_with_items(
|
||||
.await
|
||||
.map_err(map_editor_project_error)?;
|
||||
let project = repair_editor_project_record_inline_media(state, project).await;
|
||||
let expected_revision = project.canvas.revision;
|
||||
let viewport = project.viewport.clone();
|
||||
let project_payload = editor_project_payload_from_record(project);
|
||||
let Some(placeholder) =
|
||||
@@ -6285,17 +6323,15 @@ pub(crate) async fn complete_editor_canvas_generation_with_items(
|
||||
if !completion_result.changed {
|
||||
return Ok(None);
|
||||
}
|
||||
let saved = state
|
||||
.spacetime_client()
|
||||
.save_editor_project_layout(EditorProjectLayoutSaveRecordInput {
|
||||
project_id: project_id.to_string(),
|
||||
owner_user_id: owner_user_id.to_string(),
|
||||
viewport,
|
||||
layers_json: serialize_editor_layers(completion_result.layers)?,
|
||||
updated_at_micros: current_utc_micros(),
|
||||
})
|
||||
.await
|
||||
.map_err(map_editor_project_error)?;
|
||||
let saved = save_editor_project_layout_with_revision_and_get(
|
||||
state,
|
||||
project_id,
|
||||
owner_user_id,
|
||||
viewport,
|
||||
completion_result.layers,
|
||||
expected_revision,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Some(editor_project_payload_from_record(saved)))
|
||||
}
|
||||
@@ -6329,6 +6365,7 @@ pub(crate) async fn complete_editor_canvas_background_removal(
|
||||
.await
|
||||
.map_err(map_editor_project_error)?;
|
||||
let project = repair_editor_project_record_inline_media(state, project).await;
|
||||
let expected_revision = project.canvas.revision;
|
||||
let viewport = project.viewport.clone();
|
||||
let project_payload = editor_project_payload_from_record(project);
|
||||
let Some(layers) = replace_editor_canvas_layer_with_resource(
|
||||
@@ -6341,19 +6378,39 @@ pub(crate) async fn complete_editor_canvas_background_removal(
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let saved = state
|
||||
let saved = save_editor_project_layout_with_revision_and_get(
|
||||
state,
|
||||
project_id,
|
||||
owner_user_id,
|
||||
viewport,
|
||||
layers,
|
||||
expected_revision,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Some(editor_project_payload_from_record(saved)))
|
||||
}
|
||||
|
||||
pub(crate) async fn save_editor_project_layout_with_revision_and_get(
|
||||
state: &AppState,
|
||||
project_id: &str,
|
||||
owner_user_id: &str,
|
||||
viewport: EditorCanvasViewportRecord,
|
||||
layers: Value,
|
||||
expected_revision: u64,
|
||||
) -> Result<EditorProjectRecord, AppError> {
|
||||
state
|
||||
.spacetime_client()
|
||||
.save_editor_project_layout(EditorProjectLayoutSaveRecordInput {
|
||||
.save_editor_project_layout_v2(EditorProjectLayoutSaveV2RecordInput {
|
||||
project_id: project_id.to_string(),
|
||||
owner_user_id: owner_user_id.to_string(),
|
||||
viewport,
|
||||
layers_json: serialize_editor_layers(layers)?,
|
||||
expected_revision,
|
||||
updated_at_micros: current_utc_micros(),
|
||||
})
|
||||
.await
|
||||
.map_err(map_editor_project_error)?;
|
||||
|
||||
Ok(Some(editor_project_payload_from_record(saved)))
|
||||
.map_err(map_editor_project_error)
|
||||
}
|
||||
|
||||
fn replace_editor_canvas_layer_with_resource(
|
||||
@@ -7954,6 +8011,12 @@ pub(crate) fn map_editor_project_error(error: SpacetimeClientError) -> AppError
|
||||
"message": message,
|
||||
}))
|
||||
}
|
||||
SpacetimeClientError::Procedure(message) if message.contains("版本冲突") => {
|
||||
AppError::from_status(StatusCode::CONFLICT).with_details(json!({
|
||||
"provider": "editor-project",
|
||||
"message": message,
|
||||
}))
|
||||
}
|
||||
SpacetimeClientError::Procedure(message) => AppError::from_status(StatusCode::BAD_REQUEST)
|
||||
.with_details(json!({
|
||||
"provider": "editor-project",
|
||||
@@ -8149,6 +8212,15 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_project_revision_conflicts_map_to_http_conflict() {
|
||||
let error = map_editor_project_error(SpacetimeClientError::Procedure(
|
||||
"图片画布版本冲突:期望 revision 4,当前为 5".to_string(),
|
||||
));
|
||||
|
||||
assert_eq!(error.status_code(), StatusCode::CONFLICT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bgfilter_body_read_failure_audits_as_transport_not_5xx() {
|
||||
// HTTP 状态已成功、但读 body 时链路断裂:外部 API 失败审计必须归类为 transport,
|
||||
|
||||
@@ -32,7 +32,8 @@ use crate::{
|
||||
editor_project_resource_payload_from_record, extract_editor_ui_design_assets_for_owner,
|
||||
generate_editor_icon_spritesheet_for_owner, generate_editor_image_for_owner,
|
||||
map_editor_project_error, normalize_editor_persisted_media_src, normalize_optional_string,
|
||||
serialize_editor_asset_metadata, serialize_editor_layers,
|
||||
save_editor_project_layout_with_revision_and_get, serialize_editor_asset_metadata,
|
||||
serialize_editor_layers,
|
||||
},
|
||||
external_api_auth::ExternalApiPrincipal,
|
||||
http_error::AppError,
|
||||
@@ -62,6 +63,7 @@ pub struct ExternalEditorProjectCreateRequest {
|
||||
pub struct ExternalEditorCanvasSaveRequest {
|
||||
viewport: EditorCanvasViewportPayload,
|
||||
layers: Value,
|
||||
expected_revision: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -339,17 +341,29 @@ pub async fn save_external_editor_canvas(
|
||||
Json(payload): Json<ExternalEditorCanvasSaveRequest>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
require_scope(&principal, SCOPE_EDITOR_CANVAS)?;
|
||||
let project = state
|
||||
.spacetime_client()
|
||||
.save_editor_project_layout(EditorProjectLayoutSaveRecordInput {
|
||||
project_id,
|
||||
owner_user_id: principal.owner_user_id().to_string(),
|
||||
viewport: payload.viewport.into_record(),
|
||||
layers_json: serialize_editor_layers(payload.layers)?,
|
||||
updated_at_micros: current_utc_micros(),
|
||||
})
|
||||
.await
|
||||
.map_err(map_editor_project_error)?;
|
||||
let project = if let Some(expected_revision) = payload.expected_revision {
|
||||
save_editor_project_layout_with_revision_and_get(
|
||||
&state,
|
||||
project_id.as_str(),
|
||||
principal.owner_user_id(),
|
||||
payload.viewport.into_record(),
|
||||
payload.layers,
|
||||
expected_revision,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
state
|
||||
.spacetime_client()
|
||||
.save_editor_project_layout(EditorProjectLayoutSaveRecordInput {
|
||||
project_id,
|
||||
owner_user_id: principal.owner_user_id().to_string(),
|
||||
viewport: payload.viewport.into_record(),
|
||||
layers_json: serialize_editor_layers(payload.layers)?,
|
||||
updated_at_micros: current_utc_micros(),
|
||||
})
|
||||
.await
|
||||
.map_err(map_editor_project_error)?
|
||||
};
|
||||
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
@@ -854,6 +868,14 @@ mod tests {
|
||||
.get("priceMudPoints")
|
||||
.is_none()
|
||||
);
|
||||
assert_eq!(
|
||||
parsed["components"]["schemas"]["EditorProject"]["properties"]["layers"]["type"],
|
||||
"array"
|
||||
);
|
||||
assert_eq!(
|
||||
parsed["components"]["schemas"]["EditorCanvas"]["properties"]["layers"]["type"],
|
||||
"array"
|
||||
);
|
||||
assert!(
|
||||
parsed["paths"]
|
||||
.get("/api/external/v1/editor/images/edits")
|
||||
|
||||
@@ -185,6 +185,53 @@ impl SpacetimeClient {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn save_editor_project_layout_v2_ack(
|
||||
&self,
|
||||
input: EditorProjectLayoutSaveV2RecordInput,
|
||||
) -> Result<EditorProjectLayoutSaveV2AckRecord, SpacetimeClientError> {
|
||||
let procedure_input = input.into();
|
||||
|
||||
self.call_after_connect(
|
||||
"save_editor_project_layout_v_2_ack",
|
||||
move |connection, sender| {
|
||||
connection
|
||||
.procedures()
|
||||
.save_editor_project_layout_v_2_ack_then(procedure_input, move |_, result| {
|
||||
let mapped = result
|
||||
.map_err(SpacetimeClientError::from_sdk_error)
|
||||
.and_then(map_editor_project_layout_save_v2_procedure_result);
|
||||
send_once(&sender, mapped);
|
||||
});
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn save_editor_project_layout_v2(
|
||||
&self,
|
||||
input: EditorProjectLayoutSaveV2RecordInput,
|
||||
) -> Result<EditorProjectRecord, SpacetimeClientError> {
|
||||
let procedure_input = input.into();
|
||||
|
||||
self.call_after_connect(
|
||||
"save_editor_project_layout_v_2_and_return",
|
||||
move |connection, sender| {
|
||||
connection
|
||||
.procedures()
|
||||
.save_editor_project_layout_v_2_and_return_then(
|
||||
procedure_input,
|
||||
move |_, result| {
|
||||
let mapped = result
|
||||
.map_err(SpacetimeClientError::from_sdk_error)
|
||||
.and_then(map_editor_project_required_procedure_result);
|
||||
send_once(&sender, mapped);
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_editor_project_resource(
|
||||
&self,
|
||||
input: EditorProjectResourceCreateRecordInput,
|
||||
|
||||
@@ -46,7 +46,8 @@ pub use mapper::{
|
||||
EditorGenerationPricingConfigRecord, EditorGenerationPricingConfigUpsertRecordInput,
|
||||
EditorGenerationPricingTierRecord, EditorProjectCreateRecordInput,
|
||||
EditorProjectDeleteRecordInput, EditorProjectGetRecordInput, EditorProjectLayoutSaveAckRecord,
|
||||
EditorProjectLayoutSaveRecordInput, EditorProjectRecord, EditorProjectRenameRecordInput,
|
||||
EditorProjectLayoutSaveRecordInput, EditorProjectLayoutSaveV2AckRecord,
|
||||
EditorProjectLayoutSaveV2RecordInput, EditorProjectRecord, EditorProjectRenameRecordInput,
|
||||
EditorProjectResourceCreateRecordInput, EditorProjectResourceMediaRepairRecordInput,
|
||||
EditorProjectResourcePublicShowcaseListRecordInput, EditorProjectResourceRecord,
|
||||
EditorProjectResourceShowcaseUpdateRecordInput, EditorShowcaseAssetAdminListRecordInput,
|
||||
|
||||
@@ -99,7 +99,8 @@ pub use self::editor_project::{
|
||||
EditorGenerationPricingConfigRecord, EditorGenerationPricingConfigUpsertRecordInput,
|
||||
EditorGenerationPricingTierRecord, EditorProjectCreateRecordInput,
|
||||
EditorProjectDeleteRecordInput, EditorProjectGetRecordInput, EditorProjectLayoutSaveAckRecord,
|
||||
EditorProjectLayoutSaveRecordInput, EditorProjectRecord, EditorProjectRenameRecordInput,
|
||||
EditorProjectLayoutSaveRecordInput, EditorProjectLayoutSaveV2AckRecord,
|
||||
EditorProjectLayoutSaveV2RecordInput, EditorProjectRecord, EditorProjectRenameRecordInput,
|
||||
EditorProjectResourceCreateRecordInput, EditorProjectResourceMediaRepairRecordInput,
|
||||
EditorProjectResourcePublicShowcaseListRecordInput, EditorProjectResourceRecord,
|
||||
EditorProjectResourceShowcaseUpdateRecordInput, EditorShowcaseAssetAdminListRecordInput,
|
||||
@@ -256,8 +257,8 @@ pub(crate) use self::editor_project::{
|
||||
map_editor_generation_pricing_config_optional_procedure_result,
|
||||
map_editor_generation_pricing_config_required_procedure_result,
|
||||
map_editor_project_delete_procedure_result, map_editor_project_layout_save_procedure_result,
|
||||
map_editor_project_list_procedure_result, map_editor_project_optional_procedure_result,
|
||||
map_editor_project_required_procedure_result,
|
||||
map_editor_project_layout_save_v2_procedure_result, map_editor_project_list_procedure_result,
|
||||
map_editor_project_optional_procedure_result, map_editor_project_required_procedure_result,
|
||||
map_editor_project_resource_list_procedure_result,
|
||||
map_editor_project_resource_procedure_result, map_editor_showcase_asset_list_procedure_result,
|
||||
map_editor_showcase_asset_procedure_result,
|
||||
|
||||
@@ -27,6 +27,9 @@ pub struct EditorCanvasRecord {
|
||||
pub title: String,
|
||||
pub viewport: EditorCanvasViewportRecord,
|
||||
pub layers: serde_json::Value,
|
||||
pub revision: u64,
|
||||
pub layout_storage_version: u32,
|
||||
pub background_color: Option<String>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
@@ -39,6 +42,15 @@ pub struct EditorProjectLayoutSaveAckRecord {
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct EditorProjectLayoutSaveV2AckRecord {
|
||||
pub project_id: String,
|
||||
pub canvas_id: String,
|
||||
pub revision: u64,
|
||||
pub updated_at: String,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct EditorProjectResourceRecord {
|
||||
pub resource_id: String,
|
||||
@@ -251,6 +263,16 @@ pub struct EditorProjectLayoutSaveRecordInput {
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct EditorProjectLayoutSaveV2RecordInput {
|
||||
pub project_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub viewport: EditorCanvasViewportRecord,
|
||||
pub layers_json: String,
|
||||
pub expected_revision: u64,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct EditorProjectResourceCreateRecordInput {
|
||||
pub resource_id: String,
|
||||
@@ -544,6 +566,25 @@ impl From<EditorProjectLayoutSaveRecordInput>
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EditorProjectLayoutSaveV2RecordInput>
|
||||
for crate::module_bindings::EditorProjectLayoutSaveV2Input
|
||||
{
|
||||
fn from(input: EditorProjectLayoutSaveV2RecordInput) -> Self {
|
||||
Self {
|
||||
project_id: input.project_id,
|
||||
owner_user_id: input.owner_user_id,
|
||||
viewport: crate::module_bindings::EditorProjectViewportSnapshot {
|
||||
x: input.viewport.x,
|
||||
y: input.viewport.y,
|
||||
scale: input.viewport.scale,
|
||||
},
|
||||
layers_json: input.layers_json,
|
||||
expected_revision: input.expected_revision,
|
||||
updated_at_micros: input.updated_at_micros,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EditorProjectResourceCreateRecordInput>
|
||||
for crate::module_bindings::EditorProjectResourceCreateInput
|
||||
{
|
||||
@@ -944,6 +985,25 @@ pub(crate) fn map_editor_project_layout_save_procedure_result(
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn map_editor_project_layout_save_v2_procedure_result(
|
||||
result: EditorProjectLayoutSaveV2ProcedureResult,
|
||||
) -> Result<EditorProjectLayoutSaveV2AckRecord, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
|
||||
let ack = result
|
||||
.ack
|
||||
.ok_or_else(|| SpacetimeClientError::missing_snapshot("图片画布保存 V2 结果"))?;
|
||||
Ok(EditorProjectLayoutSaveV2AckRecord {
|
||||
project_id: ack.project_id,
|
||||
canvas_id: ack.canvas_id,
|
||||
revision: ack.revision,
|
||||
updated_at: format_timestamp_micros(ack.updated_at_micros),
|
||||
updated_at_micros: ack.updated_at_micros,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn map_editor_project_list_procedure_result(
|
||||
result: EditorProjectListProcedureResult,
|
||||
) -> Result<Vec<EditorProjectRecord>, SpacetimeClientError> {
|
||||
@@ -1157,6 +1217,9 @@ fn map_editor_project_snapshot(
|
||||
title: snapshot.canvas.title,
|
||||
viewport: viewport.clone(),
|
||||
layers: layers.clone(),
|
||||
revision: snapshot.canvas.revision,
|
||||
layout_storage_version: snapshot.canvas.layout_storage_version,
|
||||
background_color: snapshot.canvas.background_color,
|
||||
created_at: format_timestamp_micros(snapshot.canvas.created_at_micros),
|
||||
updated_at: format_timestamp_micros(snapshot.canvas.updated_at_micros),
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ pub mod accept_quest_reducer;
|
||||
pub mod acknowledge_external_generation_job_summaries_and_return_procedure;
|
||||
pub mod acknowledge_external_generation_jobs_and_return_procedure;
|
||||
pub mod acknowledge_quest_completion_reducer;
|
||||
pub mod activate_editor_canvas_layout_and_return_procedure;
|
||||
pub mod admin_account_create_input_type;
|
||||
pub mod admin_account_credential_procedure_result_type;
|
||||
pub mod admin_account_credential_snapshot_type;
|
||||
@@ -138,6 +139,7 @@ pub mod auth_store_projection_user_type;
|
||||
pub mod auth_store_projection_view_type;
|
||||
pub mod authenticate_external_api_key_and_return_procedure;
|
||||
pub mod authorize_database_migration_operator_procedure;
|
||||
pub mod backfill_editor_canvas_layout_and_return_procedure;
|
||||
pub mod backfill_external_generation_job_summaries_and_return_procedure;
|
||||
pub mod bark_battle_draft_config_row_type;
|
||||
pub mod bark_battle_draft_config_snapshot_type;
|
||||
@@ -432,6 +434,20 @@ pub mod editor_asset_snapshot_type;
|
||||
pub mod editor_asset_table;
|
||||
pub mod editor_asset_type;
|
||||
pub mod editor_asset_update_input_type;
|
||||
pub mod editor_canvas_generation_dialog_table;
|
||||
pub mod editor_canvas_generation_dialog_type;
|
||||
pub mod editor_canvas_layer_table;
|
||||
pub mod editor_canvas_layer_type;
|
||||
pub mod editor_canvas_layout_migration_input_type;
|
||||
pub mod editor_canvas_layout_migration_procedure_result_type;
|
||||
pub mod editor_canvas_layout_migration_snapshot_type;
|
||||
pub mod editor_canvas_layout_migration_table;
|
||||
pub mod editor_canvas_layout_migration_type;
|
||||
pub mod editor_canvas_missing_resource_restore_input_type;
|
||||
pub mod editor_canvas_resource_remap_input_type;
|
||||
pub mod editor_canvas_resource_repair_input_type;
|
||||
pub mod editor_canvas_resource_repair_procedure_result_type;
|
||||
pub mod editor_canvas_resource_repair_snapshot_type;
|
||||
pub mod editor_canvas_snapshot_type;
|
||||
pub mod editor_canvas_table;
|
||||
pub mod editor_canvas_type;
|
||||
@@ -453,6 +469,9 @@ pub mod editor_project_get_recent_input_type;
|
||||
pub mod editor_project_layout_save_ack_type;
|
||||
pub mod editor_project_layout_save_input_type;
|
||||
pub mod editor_project_layout_save_procedure_result_type;
|
||||
pub mod editor_project_layout_save_v_2_ack_type;
|
||||
pub mod editor_project_layout_save_v_2_input_type;
|
||||
pub mod editor_project_layout_save_v_2_procedure_result_type;
|
||||
pub mod editor_project_list_input_type;
|
||||
pub mod editor_project_list_procedure_result_type;
|
||||
pub mod editor_project_procedure_result_type;
|
||||
@@ -520,10 +539,10 @@ pub mod external_generation_job_phase_update_failure_kind_type;
|
||||
pub mod external_generation_job_phase_update_input_type;
|
||||
pub mod external_generation_job_phase_update_procedure_result_type;
|
||||
pub mod external_generation_job_procedure_result_type;
|
||||
pub mod external_generation_job_result_procedure_result_type;
|
||||
pub mod external_generation_job_renew_lease_input_type;
|
||||
pub mod external_generation_job_snapshot_type;
|
||||
pub mod external_generation_job_result_procedure_result_type;
|
||||
pub mod external_generation_job_result_snapshot_type;
|
||||
pub mod external_generation_job_snapshot_type;
|
||||
pub mod external_generation_job_summary_backfill_input_type;
|
||||
pub mod external_generation_job_summary_backfill_procedure_result_type;
|
||||
pub mod external_generation_job_summary_procedure_result_type;
|
||||
@@ -1034,6 +1053,7 @@ pub mod remix_puzzle_work_procedure;
|
||||
pub mod rename_editor_project_and_return_procedure;
|
||||
pub mod renew_external_generation_job_lease_and_return_procedure;
|
||||
pub mod repair_editor_asset_media_and_return_procedure;
|
||||
pub mod repair_editor_canvas_resources_and_return_procedure;
|
||||
pub mod repair_editor_project_resource_media_and_return_procedure;
|
||||
pub mod resolve_combat_action_and_return_procedure;
|
||||
pub mod resolve_combat_action_input_type;
|
||||
@@ -1058,6 +1078,7 @@ pub mod resume_profile_save_archive_and_return_procedure;
|
||||
pub mod retry_puzzle_clear_level_run_procedure;
|
||||
pub mod revoke_database_migration_operator_procedure;
|
||||
pub mod revoke_external_api_key_and_return_procedure;
|
||||
pub mod rollback_editor_canvas_layout_and_return_procedure;
|
||||
pub mod rotate_editor_generation_runtime_service_identity_and_return_procedure;
|
||||
pub mod rpg_agent_draft_card_kind_type;
|
||||
pub mod rpg_agent_draft_card_status_type;
|
||||
@@ -1224,6 +1245,8 @@ pub mod runtime_tracking_event_procedure_result_type;
|
||||
pub mod runtime_tracking_scope_kind_type;
|
||||
pub mod save_editor_project_layout_ack_procedure;
|
||||
pub mod save_editor_project_layout_and_return_procedure;
|
||||
pub mod save_editor_project_layout_v_2_ack_procedure;
|
||||
pub mod save_editor_project_layout_v_2_and_return_procedure;
|
||||
pub mod save_puzzle_form_draft_procedure;
|
||||
pub mod save_puzzle_generated_images_procedure;
|
||||
pub mod save_puzzle_ui_background_procedure;
|
||||
@@ -1449,6 +1472,7 @@ pub use accept_quest_reducer::accept_quest;
|
||||
pub use acknowledge_external_generation_job_summaries_and_return_procedure::acknowledge_external_generation_job_summaries_and_return;
|
||||
pub use acknowledge_external_generation_jobs_and_return_procedure::acknowledge_external_generation_jobs_and_return;
|
||||
pub use acknowledge_quest_completion_reducer::acknowledge_quest_completion;
|
||||
pub use activate_editor_canvas_layout_and_return_procedure::activate_editor_canvas_layout_and_return;
|
||||
pub use admin_account_create_input_type::AdminAccountCreateInput;
|
||||
pub use admin_account_credential_procedure_result_type::AdminAccountCredentialProcedureResult;
|
||||
pub use admin_account_credential_snapshot_type::AdminAccountCredentialSnapshot;
|
||||
@@ -1577,6 +1601,7 @@ pub use auth_store_projection_user_type::AuthStoreProjectionUser;
|
||||
pub use auth_store_projection_view_type::AuthStoreProjectionView;
|
||||
pub use authenticate_external_api_key_and_return_procedure::authenticate_external_api_key_and_return;
|
||||
pub use authorize_database_migration_operator_procedure::authorize_database_migration_operator;
|
||||
pub use backfill_editor_canvas_layout_and_return_procedure::backfill_editor_canvas_layout_and_return;
|
||||
pub use backfill_external_generation_job_summaries_and_return_procedure::backfill_external_generation_job_summaries_and_return;
|
||||
pub use bark_battle_draft_config_row_type::BarkBattleDraftConfigRow;
|
||||
pub use bark_battle_draft_config_snapshot_type::BarkBattleDraftConfigSnapshot;
|
||||
@@ -1871,6 +1896,20 @@ pub use editor_asset_snapshot_type::EditorAssetSnapshot;
|
||||
pub use editor_asset_table::*;
|
||||
pub use editor_asset_type::EditorAsset;
|
||||
pub use editor_asset_update_input_type::EditorAssetUpdateInput;
|
||||
pub use editor_canvas_generation_dialog_table::*;
|
||||
pub use editor_canvas_generation_dialog_type::EditorCanvasGenerationDialog;
|
||||
pub use editor_canvas_layer_table::*;
|
||||
pub use editor_canvas_layer_type::EditorCanvasLayer;
|
||||
pub use editor_canvas_layout_migration_input_type::EditorCanvasLayoutMigrationInput;
|
||||
pub use editor_canvas_layout_migration_procedure_result_type::EditorCanvasLayoutMigrationProcedureResult;
|
||||
pub use editor_canvas_layout_migration_snapshot_type::EditorCanvasLayoutMigrationSnapshot;
|
||||
pub use editor_canvas_layout_migration_table::*;
|
||||
pub use editor_canvas_layout_migration_type::EditorCanvasLayoutMigration;
|
||||
pub use editor_canvas_missing_resource_restore_input_type::EditorCanvasMissingResourceRestoreInput;
|
||||
pub use editor_canvas_resource_remap_input_type::EditorCanvasResourceRemapInput;
|
||||
pub use editor_canvas_resource_repair_input_type::EditorCanvasResourceRepairInput;
|
||||
pub use editor_canvas_resource_repair_procedure_result_type::EditorCanvasResourceRepairProcedureResult;
|
||||
pub use editor_canvas_resource_repair_snapshot_type::EditorCanvasResourceRepairSnapshot;
|
||||
pub use editor_canvas_snapshot_type::EditorCanvasSnapshot;
|
||||
pub use editor_canvas_table::*;
|
||||
pub use editor_canvas_type::EditorCanvas;
|
||||
@@ -1892,6 +1931,9 @@ pub use editor_project_get_recent_input_type::EditorProjectGetRecentInput;
|
||||
pub use editor_project_layout_save_ack_type::EditorProjectLayoutSaveAck;
|
||||
pub use editor_project_layout_save_input_type::EditorProjectLayoutSaveInput;
|
||||
pub use editor_project_layout_save_procedure_result_type::EditorProjectLayoutSaveProcedureResult;
|
||||
pub use editor_project_layout_save_v_2_ack_type::EditorProjectLayoutSaveV2Ack;
|
||||
pub use editor_project_layout_save_v_2_input_type::EditorProjectLayoutSaveV2Input;
|
||||
pub use editor_project_layout_save_v_2_procedure_result_type::EditorProjectLayoutSaveV2ProcedureResult;
|
||||
pub use editor_project_list_input_type::EditorProjectListInput;
|
||||
pub use editor_project_list_procedure_result_type::EditorProjectListProcedureResult;
|
||||
pub use editor_project_procedure_result_type::EditorProjectProcedureResult;
|
||||
@@ -1959,10 +2001,10 @@ pub use external_generation_job_phase_update_failure_kind_type::ExternalGenerati
|
||||
pub use external_generation_job_phase_update_input_type::ExternalGenerationJobPhaseUpdateInput;
|
||||
pub use external_generation_job_phase_update_procedure_result_type::ExternalGenerationJobPhaseUpdateProcedureResult;
|
||||
pub use external_generation_job_procedure_result_type::ExternalGenerationJobProcedureResult;
|
||||
pub use external_generation_job_result_procedure_result_type::ExternalGenerationJobResultProcedureResult;
|
||||
pub use external_generation_job_renew_lease_input_type::ExternalGenerationJobRenewLeaseInput;
|
||||
pub use external_generation_job_snapshot_type::ExternalGenerationJobSnapshot;
|
||||
pub use external_generation_job_result_procedure_result_type::ExternalGenerationJobResultProcedureResult;
|
||||
pub use external_generation_job_result_snapshot_type::ExternalGenerationJobResultSnapshot;
|
||||
pub use external_generation_job_snapshot_type::ExternalGenerationJobSnapshot;
|
||||
pub use external_generation_job_summary_backfill_input_type::ExternalGenerationJobSummaryBackfillInput;
|
||||
pub use external_generation_job_summary_backfill_procedure_result_type::ExternalGenerationJobSummaryBackfillProcedureResult;
|
||||
pub use external_generation_job_summary_procedure_result_type::ExternalGenerationJobSummaryProcedureResult;
|
||||
@@ -2473,6 +2515,7 @@ pub use remix_puzzle_work_procedure::remix_puzzle_work;
|
||||
pub use rename_editor_project_and_return_procedure::rename_editor_project_and_return;
|
||||
pub use renew_external_generation_job_lease_and_return_procedure::renew_external_generation_job_lease_and_return;
|
||||
pub use repair_editor_asset_media_and_return_procedure::repair_editor_asset_media_and_return;
|
||||
pub use repair_editor_canvas_resources_and_return_procedure::repair_editor_canvas_resources_and_return;
|
||||
pub use repair_editor_project_resource_media_and_return_procedure::repair_editor_project_resource_media_and_return;
|
||||
pub use resolve_combat_action_and_return_procedure::resolve_combat_action_and_return;
|
||||
pub use resolve_combat_action_input_type::ResolveCombatActionInput;
|
||||
@@ -2497,6 +2540,7 @@ pub use resume_profile_save_archive_and_return_procedure::resume_profile_save_ar
|
||||
pub use retry_puzzle_clear_level_run_procedure::retry_puzzle_clear_level_run;
|
||||
pub use revoke_database_migration_operator_procedure::revoke_database_migration_operator;
|
||||
pub use revoke_external_api_key_and_return_procedure::revoke_external_api_key_and_return;
|
||||
pub use rollback_editor_canvas_layout_and_return_procedure::rollback_editor_canvas_layout_and_return;
|
||||
pub use rotate_editor_generation_runtime_service_identity_and_return_procedure::rotate_editor_generation_runtime_service_identity_and_return;
|
||||
pub use rpg_agent_draft_card_kind_type::RpgAgentDraftCardKind;
|
||||
pub use rpg_agent_draft_card_status_type::RpgAgentDraftCardStatus;
|
||||
@@ -2663,6 +2707,8 @@ pub use runtime_tracking_event_procedure_result_type::RuntimeTrackingEventProced
|
||||
pub use runtime_tracking_scope_kind_type::RuntimeTrackingScopeKind;
|
||||
pub use save_editor_project_layout_ack_procedure::save_editor_project_layout_ack;
|
||||
pub use save_editor_project_layout_and_return_procedure::save_editor_project_layout_and_return;
|
||||
pub use save_editor_project_layout_v_2_ack_procedure::save_editor_project_layout_v_2_ack;
|
||||
pub use save_editor_project_layout_v_2_and_return_procedure::save_editor_project_layout_v_2_and_return;
|
||||
pub use save_puzzle_form_draft_procedure::save_puzzle_form_draft;
|
||||
pub use save_puzzle_generated_images_procedure::save_puzzle_generated_images;
|
||||
pub use save_puzzle_ui_background_procedure::save_puzzle_ui_background;
|
||||
@@ -3213,6 +3259,9 @@ pub struct DbUpdate {
|
||||
editor_asset_group_cohort: __sdk::TableUpdate<EditorAssetGroupCohort>,
|
||||
editor_asset_group_source_provenance: __sdk::TableUpdate<EditorAssetGroupSourceProvenance>,
|
||||
editor_canvas: __sdk::TableUpdate<EditorCanvas>,
|
||||
editor_canvas_generation_dialog: __sdk::TableUpdate<EditorCanvasGenerationDialog>,
|
||||
editor_canvas_layer: __sdk::TableUpdate<EditorCanvasLayer>,
|
||||
editor_canvas_layout_migration: __sdk::TableUpdate<EditorCanvasLayoutMigration>,
|
||||
editor_generation_pricing_config: __sdk::TableUpdate<EditorGenerationPricingConfig>,
|
||||
editor_generation_runtime_identity_rotation:
|
||||
__sdk::TableUpdate<EditorGenerationRuntimeIdentityRotation>,
|
||||
@@ -3481,6 +3530,19 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate {
|
||||
"editor_canvas" => db_update
|
||||
.editor_canvas
|
||||
.append(editor_canvas_table::parse_table_update(table_update)?),
|
||||
"editor_canvas_generation_dialog" => {
|
||||
db_update.editor_canvas_generation_dialog.append(
|
||||
editor_canvas_generation_dialog_table::parse_table_update(table_update)?,
|
||||
)
|
||||
}
|
||||
"editor_canvas_layer" => db_update
|
||||
.editor_canvas_layer
|
||||
.append(editor_canvas_layer_table::parse_table_update(table_update)?),
|
||||
"editor_canvas_layout_migration" => {
|
||||
db_update.editor_canvas_layout_migration.append(
|
||||
editor_canvas_layout_migration_table::parse_table_update(table_update)?,
|
||||
)
|
||||
}
|
||||
"editor_generation_pricing_config" => {
|
||||
db_update.editor_generation_pricing_config.append(
|
||||
editor_generation_pricing_config_table::parse_table_update(table_update)?,
|
||||
@@ -4078,6 +4140,24 @@ impl __sdk::DbUpdate for DbUpdate {
|
||||
diff.editor_canvas = cache
|
||||
.apply_diff_to_table::<EditorCanvas>("editor_canvas", &self.editor_canvas)
|
||||
.with_updates_by_pk(|row| &row.canvas_id);
|
||||
diff.editor_canvas_generation_dialog = cache
|
||||
.apply_diff_to_table::<EditorCanvasGenerationDialog>(
|
||||
"editor_canvas_generation_dialog",
|
||||
&self.editor_canvas_generation_dialog,
|
||||
)
|
||||
.with_updates_by_pk(|row| &row.dialog_key);
|
||||
diff.editor_canvas_layer = cache
|
||||
.apply_diff_to_table::<EditorCanvasLayer>(
|
||||
"editor_canvas_layer",
|
||||
&self.editor_canvas_layer,
|
||||
)
|
||||
.with_updates_by_pk(|row| &row.layer_key);
|
||||
diff.editor_canvas_layout_migration = cache
|
||||
.apply_diff_to_table::<EditorCanvasLayoutMigration>(
|
||||
"editor_canvas_layout_migration",
|
||||
&self.editor_canvas_layout_migration,
|
||||
)
|
||||
.with_updates_by_pk(|row| &row.canvas_id);
|
||||
diff.editor_generation_pricing_config = cache
|
||||
.apply_diff_to_table::<EditorGenerationPricingConfig>(
|
||||
"editor_generation_pricing_config",
|
||||
@@ -4764,6 +4844,15 @@ impl __sdk::DbUpdate for DbUpdate {
|
||||
"editor_canvas" => db_update
|
||||
.editor_canvas
|
||||
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
||||
"editor_canvas_generation_dialog" => db_update
|
||||
.editor_canvas_generation_dialog
|
||||
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
||||
"editor_canvas_layer" => db_update
|
||||
.editor_canvas_layer
|
||||
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
||||
"editor_canvas_layout_migration" => db_update
|
||||
.editor_canvas_layout_migration
|
||||
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
||||
"editor_generation_pricing_config" => db_update
|
||||
.editor_generation_pricing_config
|
||||
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
||||
@@ -5224,6 +5313,15 @@ impl __sdk::DbUpdate for DbUpdate {
|
||||
"editor_canvas" => db_update
|
||||
.editor_canvas
|
||||
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
||||
"editor_canvas_generation_dialog" => db_update
|
||||
.editor_canvas_generation_dialog
|
||||
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
||||
"editor_canvas_layer" => db_update
|
||||
.editor_canvas_layer
|
||||
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
||||
"editor_canvas_layout_migration" => db_update
|
||||
.editor_canvas_layout_migration
|
||||
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
||||
"editor_generation_pricing_config" => db_update
|
||||
.editor_generation_pricing_config
|
||||
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
||||
@@ -5597,6 +5695,9 @@ pub struct AppliedDiff<'r> {
|
||||
editor_asset_group_source_provenance:
|
||||
__sdk::TableAppliedDiff<'r, EditorAssetGroupSourceProvenance>,
|
||||
editor_canvas: __sdk::TableAppliedDiff<'r, EditorCanvas>,
|
||||
editor_canvas_generation_dialog: __sdk::TableAppliedDiff<'r, EditorCanvasGenerationDialog>,
|
||||
editor_canvas_layer: __sdk::TableAppliedDiff<'r, EditorCanvasLayer>,
|
||||
editor_canvas_layout_migration: __sdk::TableAppliedDiff<'r, EditorCanvasLayoutMigration>,
|
||||
editor_generation_pricing_config: __sdk::TableAppliedDiff<'r, EditorGenerationPricingConfig>,
|
||||
editor_generation_runtime_identity_rotation:
|
||||
__sdk::TableAppliedDiff<'r, EditorGenerationRuntimeIdentityRotation>,
|
||||
@@ -5942,6 +6043,21 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> {
|
||||
&self.editor_canvas,
|
||||
event,
|
||||
);
|
||||
callbacks.invoke_table_row_callbacks::<EditorCanvasGenerationDialog>(
|
||||
"editor_canvas_generation_dialog",
|
||||
&self.editor_canvas_generation_dialog,
|
||||
event,
|
||||
);
|
||||
callbacks.invoke_table_row_callbacks::<EditorCanvasLayer>(
|
||||
"editor_canvas_layer",
|
||||
&self.editor_canvas_layer,
|
||||
event,
|
||||
);
|
||||
callbacks.invoke_table_row_callbacks::<EditorCanvasLayoutMigration>(
|
||||
"editor_canvas_layout_migration",
|
||||
&self.editor_canvas_layout_migration,
|
||||
event,
|
||||
);
|
||||
callbacks.invoke_table_row_callbacks::<EditorGenerationPricingConfig>(
|
||||
"editor_generation_pricing_config",
|
||||
&self.editor_generation_pricing_config,
|
||||
@@ -7151,6 +7267,9 @@ impl __sdk::SpacetimeModule for RemoteModule {
|
||||
editor_asset_group_cohort_table::register_table(client_cache);
|
||||
editor_asset_group_source_provenance_table::register_table(client_cache);
|
||||
editor_canvas_table::register_table(client_cache);
|
||||
editor_canvas_generation_dialog_table::register_table(client_cache);
|
||||
editor_canvas_layer_table::register_table(client_cache);
|
||||
editor_canvas_layout_migration_table::register_table(client_cache);
|
||||
editor_generation_pricing_config_table::register_table(client_cache);
|
||||
editor_generation_runtime_identity_rotation_table::register_table(client_cache);
|
||||
editor_project_table::register_table(client_cache);
|
||||
@@ -7302,6 +7421,9 @@ impl __sdk::SpacetimeModule for RemoteModule {
|
||||
"editor_asset_group_cohort",
|
||||
"editor_asset_group_source_provenance",
|
||||
"editor_canvas",
|
||||
"editor_canvas_generation_dialog",
|
||||
"editor_canvas_layer",
|
||||
"editor_canvas_layout_migration",
|
||||
"editor_generation_pricing_config",
|
||||
"editor_generation_runtime_identity_rotation",
|
||||
"editor_project",
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
use super::editor_canvas_layout_migration_input_type::EditorCanvasLayoutMigrationInput;
|
||||
use super::editor_canvas_layout_migration_procedure_result_type::EditorCanvasLayoutMigrationProcedureResult;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
struct ActivateEditorCanvasLayoutAndReturnArgs {
|
||||
pub input: EditorCanvasLayoutMigrationInput,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for ActivateEditorCanvasLayoutAndReturnArgs {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for access to the procedure `activate_editor_canvas_layout_and_return`.
|
||||
///
|
||||
/// Implemented for [`super::RemoteProcedures`].
|
||||
pub trait activate_editor_canvas_layout_and_return {
|
||||
fn activate_editor_canvas_layout_and_return(&self, input: EditorCanvasLayoutMigrationInput) {
|
||||
self.activate_editor_canvas_layout_and_return_then(input, |_, _| {});
|
||||
}
|
||||
|
||||
fn activate_editor_canvas_layout_and_return_then(
|
||||
&self,
|
||||
input: EditorCanvasLayoutMigrationInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<EditorCanvasLayoutMigrationProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
);
|
||||
}
|
||||
|
||||
impl activate_editor_canvas_layout_and_return for super::RemoteProcedures {
|
||||
fn activate_editor_canvas_layout_and_return_then(
|
||||
&self,
|
||||
input: EditorCanvasLayoutMigrationInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<EditorCanvasLayoutMigrationProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
) {
|
||||
self.imp
|
||||
.invoke_procedure_with_callback::<_, EditorCanvasLayoutMigrationProcedureResult>(
|
||||
"activate_editor_canvas_layout_and_return",
|
||||
ActivateEditorCanvasLayoutAndReturnArgs { input },
|
||||
__callback,
|
||||
);
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
use super::editor_canvas_layout_migration_input_type::EditorCanvasLayoutMigrationInput;
|
||||
use super::editor_canvas_layout_migration_procedure_result_type::EditorCanvasLayoutMigrationProcedureResult;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
struct BackfillEditorCanvasLayoutAndReturnArgs {
|
||||
pub input: EditorCanvasLayoutMigrationInput,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for BackfillEditorCanvasLayoutAndReturnArgs {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for access to the procedure `backfill_editor_canvas_layout_and_return`.
|
||||
///
|
||||
/// Implemented for [`super::RemoteProcedures`].
|
||||
pub trait backfill_editor_canvas_layout_and_return {
|
||||
fn backfill_editor_canvas_layout_and_return(&self, input: EditorCanvasLayoutMigrationInput) {
|
||||
self.backfill_editor_canvas_layout_and_return_then(input, |_, _| {});
|
||||
}
|
||||
|
||||
fn backfill_editor_canvas_layout_and_return_then(
|
||||
&self,
|
||||
input: EditorCanvasLayoutMigrationInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<EditorCanvasLayoutMigrationProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
);
|
||||
}
|
||||
|
||||
impl backfill_editor_canvas_layout_and_return for super::RemoteProcedures {
|
||||
fn backfill_editor_canvas_layout_and_return_then(
|
||||
&self,
|
||||
input: EditorCanvasLayoutMigrationInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<EditorCanvasLayoutMigrationProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
) {
|
||||
self.imp
|
||||
.invoke_procedure_with_callback::<_, EditorCanvasLayoutMigrationProcedureResult>(
|
||||
"backfill_editor_canvas_layout_and_return",
|
||||
BackfillEditorCanvasLayoutAndReturnArgs { input },
|
||||
__callback,
|
||||
);
|
||||
}
|
||||
}
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use super::editor_canvas_generation_dialog_type::EditorCanvasGenerationDialog;
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
/// Table handle for the table `editor_canvas_generation_dialog`.
|
||||
///
|
||||
/// Obtain a handle from the [`EditorCanvasGenerationDialogTableAccess::editor_canvas_generation_dialog`] method on [`super::RemoteTables`],
|
||||
/// like `ctx.db.editor_canvas_generation_dialog()`.
|
||||
///
|
||||
/// Users are encouraged not to explicitly reference this type,
|
||||
/// but to directly chain method calls,
|
||||
/// like `ctx.db.editor_canvas_generation_dialog().on_insert(...)`.
|
||||
pub struct EditorCanvasGenerationDialogTableHandle<'ctx> {
|
||||
imp: __sdk::TableHandle<EditorCanvasGenerationDialog>,
|
||||
ctx: std::marker::PhantomData<&'ctx super::RemoteTables>,
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for access to the table `editor_canvas_generation_dialog`.
|
||||
///
|
||||
/// Implemented for [`super::RemoteTables`].
|
||||
pub trait EditorCanvasGenerationDialogTableAccess {
|
||||
#[allow(non_snake_case)]
|
||||
/// Obtain a [`EditorCanvasGenerationDialogTableHandle`], which mediates access to the table `editor_canvas_generation_dialog`.
|
||||
fn editor_canvas_generation_dialog(&self) -> EditorCanvasGenerationDialogTableHandle<'_>;
|
||||
}
|
||||
|
||||
impl EditorCanvasGenerationDialogTableAccess for super::RemoteTables {
|
||||
fn editor_canvas_generation_dialog(&self) -> EditorCanvasGenerationDialogTableHandle<'_> {
|
||||
EditorCanvasGenerationDialogTableHandle {
|
||||
imp: self
|
||||
.imp
|
||||
.get_table::<EditorCanvasGenerationDialog>("editor_canvas_generation_dialog"),
|
||||
ctx: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EditorCanvasGenerationDialogInsertCallbackId(__sdk::CallbackId);
|
||||
pub struct EditorCanvasGenerationDialogDeleteCallbackId(__sdk::CallbackId);
|
||||
|
||||
impl<'ctx> __sdk::Table for EditorCanvasGenerationDialogTableHandle<'ctx> {
|
||||
type Row = EditorCanvasGenerationDialog;
|
||||
type EventContext = super::EventContext;
|
||||
|
||||
fn count(&self) -> u64 {
|
||||
self.imp.count()
|
||||
}
|
||||
fn iter(&self) -> impl Iterator<Item = EditorCanvasGenerationDialog> + '_ {
|
||||
self.imp.iter()
|
||||
}
|
||||
|
||||
type InsertCallbackId = EditorCanvasGenerationDialogInsertCallbackId;
|
||||
|
||||
fn on_insert(
|
||||
&self,
|
||||
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
|
||||
) -> EditorCanvasGenerationDialogInsertCallbackId {
|
||||
EditorCanvasGenerationDialogInsertCallbackId(self.imp.on_insert(Box::new(callback)))
|
||||
}
|
||||
|
||||
fn remove_on_insert(&self, callback: EditorCanvasGenerationDialogInsertCallbackId) {
|
||||
self.imp.remove_on_insert(callback.0)
|
||||
}
|
||||
|
||||
type DeleteCallbackId = EditorCanvasGenerationDialogDeleteCallbackId;
|
||||
|
||||
fn on_delete(
|
||||
&self,
|
||||
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
|
||||
) -> EditorCanvasGenerationDialogDeleteCallbackId {
|
||||
EditorCanvasGenerationDialogDeleteCallbackId(self.imp.on_delete(Box::new(callback)))
|
||||
}
|
||||
|
||||
fn remove_on_delete(&self, callback: EditorCanvasGenerationDialogDeleteCallbackId) {
|
||||
self.imp.remove_on_delete(callback.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EditorCanvasGenerationDialogUpdateCallbackId(__sdk::CallbackId);
|
||||
|
||||
impl<'ctx> __sdk::TableWithPrimaryKey for EditorCanvasGenerationDialogTableHandle<'ctx> {
|
||||
type UpdateCallbackId = EditorCanvasGenerationDialogUpdateCallbackId;
|
||||
|
||||
fn on_update(
|
||||
&self,
|
||||
callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static,
|
||||
) -> EditorCanvasGenerationDialogUpdateCallbackId {
|
||||
EditorCanvasGenerationDialogUpdateCallbackId(self.imp.on_update(Box::new(callback)))
|
||||
}
|
||||
|
||||
fn remove_on_update(&self, callback: EditorCanvasGenerationDialogUpdateCallbackId) {
|
||||
self.imp.remove_on_update(callback.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Access to the `dialog_key` unique index on the table `editor_canvas_generation_dialog`,
|
||||
/// which allows point queries on the field of the same name
|
||||
/// via the [`EditorCanvasGenerationDialogDialogKeyUnique::find`] method.
|
||||
///
|
||||
/// Users are encouraged not to explicitly reference this type,
|
||||
/// but to directly chain method calls,
|
||||
/// like `ctx.db.editor_canvas_generation_dialog().dialog_key().find(...)`.
|
||||
pub struct EditorCanvasGenerationDialogDialogKeyUnique<'ctx> {
|
||||
imp: __sdk::UniqueConstraintHandle<EditorCanvasGenerationDialog, String>,
|
||||
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
|
||||
}
|
||||
|
||||
impl<'ctx> EditorCanvasGenerationDialogTableHandle<'ctx> {
|
||||
/// Get a handle on the `dialog_key` unique index on the table `editor_canvas_generation_dialog`.
|
||||
pub fn dialog_key(&self) -> EditorCanvasGenerationDialogDialogKeyUnique<'ctx> {
|
||||
EditorCanvasGenerationDialogDialogKeyUnique {
|
||||
imp: self.imp.get_unique_constraint::<String>("dialog_key"),
|
||||
phantom: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'ctx> EditorCanvasGenerationDialogDialogKeyUnique<'ctx> {
|
||||
/// Find the subscribed row whose `dialog_key` column value is equal to `col_val`,
|
||||
/// if such a row is present in the client cache.
|
||||
pub fn find(&self, col_val: &String) -> Option<EditorCanvasGenerationDialog> {
|
||||
self.imp.find(col_val)
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub(super) fn register_table(client_cache: &mut __sdk::ClientCache<super::RemoteModule>) {
|
||||
let _table = client_cache
|
||||
.get_or_make_table::<EditorCanvasGenerationDialog>("editor_canvas_generation_dialog");
|
||||
_table.add_unique_constraint::<String>("dialog_key", |row| &row.dialog_key);
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub(super) fn parse_table_update(
|
||||
raw_updates: __ws::v2::TableUpdate,
|
||||
) -> __sdk::Result<__sdk::TableUpdate<EditorCanvasGenerationDialog>> {
|
||||
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
|
||||
__sdk::InternalError::failed_parse(
|
||||
"TableUpdate<EditorCanvasGenerationDialog>",
|
||||
"TableUpdate",
|
||||
)
|
||||
.with_cause(e)
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for query builder access to the table `EditorCanvasGenerationDialog`.
|
||||
///
|
||||
/// Implemented for [`__sdk::QueryTableAccessor`].
|
||||
pub trait editor_canvas_generation_dialogQueryTableAccess {
|
||||
#[allow(non_snake_case)]
|
||||
/// Get a query builder for the table `EditorCanvasGenerationDialog`.
|
||||
fn editor_canvas_generation_dialog(
|
||||
&self,
|
||||
) -> __sdk::__query_builder::Table<EditorCanvasGenerationDialog>;
|
||||
}
|
||||
|
||||
impl editor_canvas_generation_dialogQueryTableAccess for __sdk::QueryTableAccessor {
|
||||
fn editor_canvas_generation_dialog(
|
||||
&self,
|
||||
) -> __sdk::__query_builder::Table<EditorCanvasGenerationDialog> {
|
||||
__sdk::__query_builder::Table::new("editor_canvas_generation_dialog")
|
||||
}
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
pub struct EditorCanvasGenerationDialog {
|
||||
pub dialog_key: String,
|
||||
pub canvas_id: String,
|
||||
pub project_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub dialog_id: String,
|
||||
pub layer_id: String,
|
||||
pub resource_id: String,
|
||||
pub sort_order: u32,
|
||||
pub mode: String,
|
||||
pub status: String,
|
||||
pub source_layer_id: Option<String>,
|
||||
pub generated_layer_id: Option<String>,
|
||||
pub placeholder_x: Option<f64>,
|
||||
pub placeholder_y: Option<f64>,
|
||||
pub placeholder_width: Option<f64>,
|
||||
pub placeholder_height: Option<f64>,
|
||||
pub dialog_json: String,
|
||||
pub created_at: __sdk::Timestamp,
|
||||
pub updated_at: __sdk::Timestamp,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for EditorCanvasGenerationDialog {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
|
||||
/// Column accessor struct for the table `EditorCanvasGenerationDialog`.
|
||||
///
|
||||
/// Provides typed access to columns for query building.
|
||||
pub struct EditorCanvasGenerationDialogCols {
|
||||
pub dialog_key: __sdk::__query_builder::Col<EditorCanvasGenerationDialog, String>,
|
||||
pub canvas_id: __sdk::__query_builder::Col<EditorCanvasGenerationDialog, String>,
|
||||
pub project_id: __sdk::__query_builder::Col<EditorCanvasGenerationDialog, String>,
|
||||
pub owner_user_id: __sdk::__query_builder::Col<EditorCanvasGenerationDialog, String>,
|
||||
pub dialog_id: __sdk::__query_builder::Col<EditorCanvasGenerationDialog, String>,
|
||||
pub layer_id: __sdk::__query_builder::Col<EditorCanvasGenerationDialog, String>,
|
||||
pub resource_id: __sdk::__query_builder::Col<EditorCanvasGenerationDialog, String>,
|
||||
pub sort_order: __sdk::__query_builder::Col<EditorCanvasGenerationDialog, u32>,
|
||||
pub mode: __sdk::__query_builder::Col<EditorCanvasGenerationDialog, String>,
|
||||
pub status: __sdk::__query_builder::Col<EditorCanvasGenerationDialog, String>,
|
||||
pub source_layer_id: __sdk::__query_builder::Col<EditorCanvasGenerationDialog, Option<String>>,
|
||||
pub generated_layer_id:
|
||||
__sdk::__query_builder::Col<EditorCanvasGenerationDialog, Option<String>>,
|
||||
pub placeholder_x: __sdk::__query_builder::Col<EditorCanvasGenerationDialog, Option<f64>>,
|
||||
pub placeholder_y: __sdk::__query_builder::Col<EditorCanvasGenerationDialog, Option<f64>>,
|
||||
pub placeholder_width: __sdk::__query_builder::Col<EditorCanvasGenerationDialog, Option<f64>>,
|
||||
pub placeholder_height: __sdk::__query_builder::Col<EditorCanvasGenerationDialog, Option<f64>>,
|
||||
pub dialog_json: __sdk::__query_builder::Col<EditorCanvasGenerationDialog, String>,
|
||||
pub created_at: __sdk::__query_builder::Col<EditorCanvasGenerationDialog, __sdk::Timestamp>,
|
||||
pub updated_at: __sdk::__query_builder::Col<EditorCanvasGenerationDialog, __sdk::Timestamp>,
|
||||
}
|
||||
|
||||
impl __sdk::__query_builder::HasCols for EditorCanvasGenerationDialog {
|
||||
type Cols = EditorCanvasGenerationDialogCols;
|
||||
fn cols(table_name: &'static str) -> Self::Cols {
|
||||
EditorCanvasGenerationDialogCols {
|
||||
dialog_key: __sdk::__query_builder::Col::new(table_name, "dialog_key"),
|
||||
canvas_id: __sdk::__query_builder::Col::new(table_name, "canvas_id"),
|
||||
project_id: __sdk::__query_builder::Col::new(table_name, "project_id"),
|
||||
owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"),
|
||||
dialog_id: __sdk::__query_builder::Col::new(table_name, "dialog_id"),
|
||||
layer_id: __sdk::__query_builder::Col::new(table_name, "layer_id"),
|
||||
resource_id: __sdk::__query_builder::Col::new(table_name, "resource_id"),
|
||||
sort_order: __sdk::__query_builder::Col::new(table_name, "sort_order"),
|
||||
mode: __sdk::__query_builder::Col::new(table_name, "mode"),
|
||||
status: __sdk::__query_builder::Col::new(table_name, "status"),
|
||||
source_layer_id: __sdk::__query_builder::Col::new(table_name, "source_layer_id"),
|
||||
generated_layer_id: __sdk::__query_builder::Col::new(table_name, "generated_layer_id"),
|
||||
placeholder_x: __sdk::__query_builder::Col::new(table_name, "placeholder_x"),
|
||||
placeholder_y: __sdk::__query_builder::Col::new(table_name, "placeholder_y"),
|
||||
placeholder_width: __sdk::__query_builder::Col::new(table_name, "placeholder_width"),
|
||||
placeholder_height: __sdk::__query_builder::Col::new(table_name, "placeholder_height"),
|
||||
dialog_json: __sdk::__query_builder::Col::new(table_name, "dialog_json"),
|
||||
created_at: __sdk::__query_builder::Col::new(table_name, "created_at"),
|
||||
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Indexed column accessor struct for the table `EditorCanvasGenerationDialog`.
|
||||
///
|
||||
/// Provides typed access to indexed columns for query building.
|
||||
pub struct EditorCanvasGenerationDialogIxCols {
|
||||
pub canvas_id: __sdk::__query_builder::IxCol<EditorCanvasGenerationDialog, String>,
|
||||
pub dialog_key: __sdk::__query_builder::IxCol<EditorCanvasGenerationDialog, String>,
|
||||
pub project_id: __sdk::__query_builder::IxCol<EditorCanvasGenerationDialog, String>,
|
||||
}
|
||||
|
||||
impl __sdk::__query_builder::HasIxCols for EditorCanvasGenerationDialog {
|
||||
type IxCols = EditorCanvasGenerationDialogIxCols;
|
||||
fn ix_cols(table_name: &'static str) -> Self::IxCols {
|
||||
EditorCanvasGenerationDialogIxCols {
|
||||
canvas_id: __sdk::__query_builder::IxCol::new(table_name, "canvas_id"),
|
||||
dialog_key: __sdk::__query_builder::IxCol::new(table_name, "dialog_key"),
|
||||
project_id: __sdk::__query_builder::IxCol::new(table_name, "project_id"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl __sdk::__query_builder::CanBeLookupTable for EditorCanvasGenerationDialog {}
|
||||
@@ -0,0 +1,161 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use super::editor_canvas_layer_type::EditorCanvasLayer;
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
/// Table handle for the table `editor_canvas_layer`.
|
||||
///
|
||||
/// Obtain a handle from the [`EditorCanvasLayerTableAccess::editor_canvas_layer`] method on [`super::RemoteTables`],
|
||||
/// like `ctx.db.editor_canvas_layer()`.
|
||||
///
|
||||
/// Users are encouraged not to explicitly reference this type,
|
||||
/// but to directly chain method calls,
|
||||
/// like `ctx.db.editor_canvas_layer().on_insert(...)`.
|
||||
pub struct EditorCanvasLayerTableHandle<'ctx> {
|
||||
imp: __sdk::TableHandle<EditorCanvasLayer>,
|
||||
ctx: std::marker::PhantomData<&'ctx super::RemoteTables>,
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for access to the table `editor_canvas_layer`.
|
||||
///
|
||||
/// Implemented for [`super::RemoteTables`].
|
||||
pub trait EditorCanvasLayerTableAccess {
|
||||
#[allow(non_snake_case)]
|
||||
/// Obtain a [`EditorCanvasLayerTableHandle`], which mediates access to the table `editor_canvas_layer`.
|
||||
fn editor_canvas_layer(&self) -> EditorCanvasLayerTableHandle<'_>;
|
||||
}
|
||||
|
||||
impl EditorCanvasLayerTableAccess for super::RemoteTables {
|
||||
fn editor_canvas_layer(&self) -> EditorCanvasLayerTableHandle<'_> {
|
||||
EditorCanvasLayerTableHandle {
|
||||
imp: self
|
||||
.imp
|
||||
.get_table::<EditorCanvasLayer>("editor_canvas_layer"),
|
||||
ctx: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EditorCanvasLayerInsertCallbackId(__sdk::CallbackId);
|
||||
pub struct EditorCanvasLayerDeleteCallbackId(__sdk::CallbackId);
|
||||
|
||||
impl<'ctx> __sdk::Table for EditorCanvasLayerTableHandle<'ctx> {
|
||||
type Row = EditorCanvasLayer;
|
||||
type EventContext = super::EventContext;
|
||||
|
||||
fn count(&self) -> u64 {
|
||||
self.imp.count()
|
||||
}
|
||||
fn iter(&self) -> impl Iterator<Item = EditorCanvasLayer> + '_ {
|
||||
self.imp.iter()
|
||||
}
|
||||
|
||||
type InsertCallbackId = EditorCanvasLayerInsertCallbackId;
|
||||
|
||||
fn on_insert(
|
||||
&self,
|
||||
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
|
||||
) -> EditorCanvasLayerInsertCallbackId {
|
||||
EditorCanvasLayerInsertCallbackId(self.imp.on_insert(Box::new(callback)))
|
||||
}
|
||||
|
||||
fn remove_on_insert(&self, callback: EditorCanvasLayerInsertCallbackId) {
|
||||
self.imp.remove_on_insert(callback.0)
|
||||
}
|
||||
|
||||
type DeleteCallbackId = EditorCanvasLayerDeleteCallbackId;
|
||||
|
||||
fn on_delete(
|
||||
&self,
|
||||
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
|
||||
) -> EditorCanvasLayerDeleteCallbackId {
|
||||
EditorCanvasLayerDeleteCallbackId(self.imp.on_delete(Box::new(callback)))
|
||||
}
|
||||
|
||||
fn remove_on_delete(&self, callback: EditorCanvasLayerDeleteCallbackId) {
|
||||
self.imp.remove_on_delete(callback.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EditorCanvasLayerUpdateCallbackId(__sdk::CallbackId);
|
||||
|
||||
impl<'ctx> __sdk::TableWithPrimaryKey for EditorCanvasLayerTableHandle<'ctx> {
|
||||
type UpdateCallbackId = EditorCanvasLayerUpdateCallbackId;
|
||||
|
||||
fn on_update(
|
||||
&self,
|
||||
callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static,
|
||||
) -> EditorCanvasLayerUpdateCallbackId {
|
||||
EditorCanvasLayerUpdateCallbackId(self.imp.on_update(Box::new(callback)))
|
||||
}
|
||||
|
||||
fn remove_on_update(&self, callback: EditorCanvasLayerUpdateCallbackId) {
|
||||
self.imp.remove_on_update(callback.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Access to the `layer_key` unique index on the table `editor_canvas_layer`,
|
||||
/// which allows point queries on the field of the same name
|
||||
/// via the [`EditorCanvasLayerLayerKeyUnique::find`] method.
|
||||
///
|
||||
/// Users are encouraged not to explicitly reference this type,
|
||||
/// but to directly chain method calls,
|
||||
/// like `ctx.db.editor_canvas_layer().layer_key().find(...)`.
|
||||
pub struct EditorCanvasLayerLayerKeyUnique<'ctx> {
|
||||
imp: __sdk::UniqueConstraintHandle<EditorCanvasLayer, String>,
|
||||
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
|
||||
}
|
||||
|
||||
impl<'ctx> EditorCanvasLayerTableHandle<'ctx> {
|
||||
/// Get a handle on the `layer_key` unique index on the table `editor_canvas_layer`.
|
||||
pub fn layer_key(&self) -> EditorCanvasLayerLayerKeyUnique<'ctx> {
|
||||
EditorCanvasLayerLayerKeyUnique {
|
||||
imp: self.imp.get_unique_constraint::<String>("layer_key"),
|
||||
phantom: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'ctx> EditorCanvasLayerLayerKeyUnique<'ctx> {
|
||||
/// Find the subscribed row whose `layer_key` column value is equal to `col_val`,
|
||||
/// if such a row is present in the client cache.
|
||||
pub fn find(&self, col_val: &String) -> Option<EditorCanvasLayer> {
|
||||
self.imp.find(col_val)
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub(super) fn register_table(client_cache: &mut __sdk::ClientCache<super::RemoteModule>) {
|
||||
let _table = client_cache.get_or_make_table::<EditorCanvasLayer>("editor_canvas_layer");
|
||||
_table.add_unique_constraint::<String>("layer_key", |row| &row.layer_key);
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub(super) fn parse_table_update(
|
||||
raw_updates: __ws::v2::TableUpdate,
|
||||
) -> __sdk::Result<__sdk::TableUpdate<EditorCanvasLayer>> {
|
||||
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
|
||||
__sdk::InternalError::failed_parse("TableUpdate<EditorCanvasLayer>", "TableUpdate")
|
||||
.with_cause(e)
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for query builder access to the table `EditorCanvasLayer`.
|
||||
///
|
||||
/// Implemented for [`__sdk::QueryTableAccessor`].
|
||||
pub trait editor_canvas_layerQueryTableAccess {
|
||||
#[allow(non_snake_case)]
|
||||
/// Get a query builder for the table `EditorCanvasLayer`.
|
||||
fn editor_canvas_layer(&self) -> __sdk::__query_builder::Table<EditorCanvasLayer>;
|
||||
}
|
||||
|
||||
impl editor_canvas_layerQueryTableAccess for __sdk::QueryTableAccessor {
|
||||
fn editor_canvas_layer(&self) -> __sdk::__query_builder::Table<EditorCanvasLayer> {
|
||||
__sdk::__query_builder::Table::new("editor_canvas_layer")
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user