Highlight
这场 Session 介绍了 CKTool JS,一个基于 Node.js 的 CloudKit 命令行工具。它的定位是:让 CloudKit 的日常管理操作可以通过脚本自动化,而不是依赖 Xcode 或网页控制台。
核心内容
CloudKit 应用的数据放在 iCloud container 里。开发时,开发者要反复改 schema、查 record、写测试数据。过去,Apple 平台 App 可以用 CloudKit framework,网页可以用 CloudKit JS,自动化脚本则主要依赖 Xcode 13 引入的 macOS 命令行工具 cktool。
(00:57)CKTool JS 把这个工作流带到 JavaScript。它提供一组 npm package,让脚本可以直接调用 CloudKit 管理和数据库 API。Session 明确说,它支持和 cktool 命令行工具相同的操作类型,也被 CloudKit Console 用来实现添加 record type、查询 record 等功能。
这件事解决的是工具链问题。团队已经有 Node.js 构建脚本、CI 脚本、数据准备脚本时,不必为了 CloudKit schema 和 record 操作切换到另一个工具。schema 可以从 .ckdb 文件导入,record 可以用 JavaScript 查询、创建、更新、删除。
(01:43)CKTool JS 还附带 TypeScript 类型定义。错误的调用方式可以在编译期暴露,IDE 也能补全 API。对于要长期维护的自动化脚本,这比一串裸 JSON 请求更容易审查。
详细内容
配置 CKTool JS
(02:38)CKTool JS 由多个 @apple scope 下的 npm package 组成。主要包是 @apple/cktool.database。如果运行在 Node.js,还需要 @apple/cktool.target.nodejs;如果运行在浏览器,则使用 @apple/cktool.target.browser。
(03:10)因为 CKTool JS 会直接访问 iCloud,脚本需要 token。schema 导入、导出、校验、reset to production 这类管理操作使用 management token。访问用户私有数据需要 user token。两类 token 都从 CloudKit Console 获取。
// Create security object and setup default args
const { CKEnvironment } = require("@apple/cktool.database");
const security = {
"ManagementTokenAuth": "<YOUR_MANAGEMENT_TOKEN>",
"UserTokenAuth": "<YOUR_USER_TOKEN>"
};
const defaultArgs = {
"teamId": "<YOUR_TEAM_ID>",
"containerId": "<YOUR_CONTAINER_ID>",
"environment": CKEnvironment.DEVELOPMENT
};
关键点:
- 第 3 行从
@apple/cktool.database取出CKEnvironment。 - 第 5 到第 8 行保存认证信息。management token 用于 schema 等管理操作,user token 用于访问用户数据。
- 第 10 到第 14 行保存每次调用都会用到的参数:team、container、environment。
CKEnvironment.DEVELOPMENT对应开发环境。Session 建议在开发环境测试 schema 变更,避免影响生产数据。
(07:17)有了认证和默认参数后,还要创建平台配置和 API 对象。createConfiguration 来自 target package,所以 Node.js 和浏览器会使用不同的底层配置。
// Create configuration and API objects
const { createConfiguration } = require("@apple/cktool.target.nodejs");
const { PromisesApi } = require("@apple/cktool.database");
const configuration = createConfiguration();
const api = new PromisesApi({
"configuration": configuration,
"security": security
});
关键点:
- 第 3 行使用 Node.js target package 提供的
createConfiguration。 - 第 4 行引入
PromisesApi,它包含访问 iCloud 的异步方法。 - 第 6 行创建平台相关配置。
- 第 7 到第 10 行把配置和 token 交给
PromisesApi,后续的 schema 和 record 操作都从api发起。
用 .ckdb 文件管理 schema
(08:40)Session 用一个硬币收藏 App 做例子。这个 App 有 Countries、Coins、Components 等 record type。schema 可以写成 CloudKit Schema Language 文本文件,约定扩展名是 .ckdb。
(09:05)应用新 schema 前,通常先把开发环境 reset 到生产环境状态。CKTool JS 提供 resetToProduction。随后用 importSchema 把本地 .ckdb 文件上传到 container。
// Create a function to apply a schema
const { File } = require("@apple/cktool.target.nodejs");
const fs = require("fs/promises");
const path = require("path");
const importMySchema = async () => {
const schemaPath = "<YOUR_SCHEMA_FILE>.ckdb";
const buffer = await fs.readFile(schemaPath);
const file = new File([buffer], schemaPath);
await api.importSchema({ ...defaultArgs, "file": file });
}
// Chain the calls
api.resetToProduction(defaultArgs)
.then(() => importMySchema());
关键点:
- 第 3 行引入 CKTool JS 的
File,用于构造上传文件。 - 第 4 行使用 Node.js 的 promise 版文件系统 API 读取 schema 文件。
- 第 8 行指定
.ckdbschema 文件路径。 - 第 9 行把文件读入
Buffer。 - 第 10 行把
Buffer包装成 CKTool JS 可以上传的File。 - 第 11 行调用
api.importSchema,并通过展开语法复用defaultArgs。 - 第 15 到第 16 行先执行
resetToProduction,成功后再导入 schema,保证两个异步步骤按顺序运行。
这个流程适合放进 CI。schema 文件进入版本控制,脚本每次先把开发环境恢复到生产状态,再应用当前分支的 schema,测试就能在可预期的 container 状态下运行。
查询 record
(10:52)除了 schema,CKTool JS 也能读写数据。Session 提醒,record 的 field value 会在客户端做类型和范围检查。大整数这类 JavaScript 原生类型不适合直接表示的值,需要使用 CKTool JS 的类型转换或 field value factory function。
(12:02)访问 record 前,先整理数据库参数。这里选择 private database 和默认 zone。
// Create a database arguments object.
const {
CKDatabaseType, CKEnvironment
} = require("@apple/cktool.database");
const databaseArgs = {
"containerID": "<YOUR_CONTAINER_ID>",
"environment": CKEnvironment.DEVELOPMENT,
"databaseType": CKDatabaseType.PRIVATE,
"zoneName": "_defaultZone"
};
关键点:
- 第 3 到第 5 行引入 database type 和 environment 常量。
- 第 8 行指定目标 container。
- 第 9 行继续使用开发环境。
- 第 10 行把操作限定在 private database。
- 第 11 行指定默认 zone,后续查询、创建、更新、删除都会复用这个对象。
(12:16)查询使用 queryRecords。示例根据国家的三位 ISO code 查找 Countries record。
// Define helper function for querying records
const { CKDBQueryFilterType } = require("@apple/cktool.database");
const countryQueryRecordForCountryCode3 = async (countryCode3) => {
const response = await api.queryRecords({
...databaseArgs,
"body": {
"query": {
"recordType": "Countries",
"filters": [{
"fieldName": "isoCode3",
"fieldValue": makeRecordFieldValue.string(countryCode3),
"type": CKDBQueryFilterType.EQUALS
}]
}
}
});
return response.result.records[0];
}
关键点:
- 第 3 行引入查询过滤器类型。
- 第 4 行定义异步 helper,输入是三位国家代码。
- 第 6 行展开
databaseArgs,避免每次重复传 container、environment、database、zone。 - 第 8 到第 15 行描述 CloudKit 查询体。
- 第 9 行查询
Countriesrecord type。 - 第 11 到第 13 行用
makeRecordFieldValue.string(countryCode3)构造字符串 field value,并用EQUALS做相等过滤。 - 第 18 行从
response.result.records取第一个匹配 record。
创建 record
(12:58)创建 record 前,要把普通 JavaScript 值转换成 CloudKit record field value。示例里,硬币的国家字段是 reference,发行年份是 Int64,面值是 Double。
// Define a helper function for creating field values
const {
makeRecordFieldValue, CKDBRecordReferenceAction
} = require("@apple/cktool.database");
const makeCoinFieldValues = ({ countryRecordName, issueYear, nominalValue }) => ({
"country": makeRecordFieldValue.reference({
recordName: countryRecordName,
action: CKDBRecordReferenceAction.DELETE_SELF
}),
"issueYear": makeRecordFieldValue.int64(issueYear),
"nominalValue": makeRecordFieldValue.double(nominalValue)
});
关键点:
- 第 3 到第 5 行引入 field value factory 和 reference action 常量。
- 第 7 行定义 helper,把原始硬币数据转换成 CloudKit field value 字典。
- 第 8 到第 11 行把国家 record name 转成 reference field value。
- 第 10 行使用
DELETE_SELF,表示被引用 record 删除时,这条 reference 所在 record 也会按该 action 处理。 - 第 12 行把发行年份转成 Int64 field value。
- 第 13 行把面值转成 Double field value。
(13:26)真正创建数据时,调用 createRecord,把 record type 和 fields 放进 request body。
// Define helper method for creating coins
const coinCreateRecord = async (fields) => {
const response = await api.createRecord({
...databaseArgs,
"body": {
"recordType": "Coins",
"fields": fields
},
});
return response.result.record;
}
关键点:
- 第 3 行定义创建硬币 record 的异步 helper。
- 第 5 行继续复用
databaseArgs。 - 第 7 行指定 record type 为
Coins。 - 第 8 行把上一个 helper 生成的 field value 字典传给 CloudKit。
- 第 11 行返回服务器创建后的 record。
(13:48)创建硬币前,脚本先查询国家 record,再把国家的 recordName 写入 reference。
// Call coin creation method with field values
const countryRecord = await countryQueryRecordForCountryCode3("USA");
const coinRecord1 = await coinCreateRecord(
makeCoinFieldValues({
"countryRecordName": countryRecord.recordName,
"issueYear": 2007,
"nominalValue": 0.10
})
);
关键点:
- 第 3 行查询 ISO code 为
USA的国家 record。 - 第 5 行调用创建 record 的 helper。
- 第 7 行把国家 record 的
recordName传给 reference field。 - 第 8 行设置硬币发行年份。
- 第 9 行设置硬币面值。
- 第 5 到第 11 行整体返回新建的
coinRecord1,后续更新或删除会继续用到它。
更新和删除 record
(14:16)更新 record 使用 updateRecord。Session 特别标注,更新时需要传入 recordChangeTag。
// Define helper method for updating coins.
// Note that recordChangeTag is required
const coinUpdate =
async (recordName, recordChangeTag, fields) => {
const response = await api.updateRecord({
...databaseArgs,
"recordName": recordName,
"body": {
"recordType": "Coins",
"recordChangeTag": recordChangeTag,
"fields": fields
}
});
return response.result.record;
}
关键点:
- 第 5 行的 helper 接收 record name、record change tag 和新字段。
- 第 7 行复用数据库参数。
- 第 8 行指定要更新的 record。
- 第 10 行指定 record type。
- 第 11 行传入
recordChangeTag,用于更新 CloudKit record。 - 第 12 行提交新的 field value 字典。
- 第 15 行返回更新后的 record。
(14:57)删除 record 更直接,只要传入数据库参数和 record name。
// Deleting a record
await api.deleteRecord({
...databaseArgs,
"recordName": coinRecord1.recordName
});
关键点:
- 第 3 行调用异步删除方法。
- 第 4 行复用 container、environment、database、zone 参数。
- 第 5 行指定要删除的 record name。
- Session 的示例在开发环境操作。生产环境脚本应先查询确认目标 record,再执行删除。
核心启发
1. 给 CloudKit schema 做 PR 级验证
- 做什么:把
.ckdbschema 文件放进仓库,每个 PR 跑一次 CKTool JS 脚本。 - 为什么值得做:Session 展示了
resetToProduction和importSchema的组合,可以把开发环境恢复到生产状态后再应用当前 schema。 - 怎么开始:准备 management token,在 CI 中执行
api.resetToProduction(defaultArgs).then(() => importMySchema()),失败时阻止合并。
2. 为测试准备可重复的数据集
- 做什么:在测试前创建一组固定的
Countries、Coins或业务 record,测试后删除。 - 为什么值得做:CKTool JS 可以用 JavaScript 查询、创建、更新、删除 record,适合接入已有 Node.js 测试工具链。
- 怎么开始:先用
queryRecords找依赖 record,再用makeRecordFieldValue构造字段,最后调用createRecord写入测试数据。
3. 做一个 schema 导入前的本地检查工具
- 做什么:提交 schema 前,检查
.ckdb文件是否存在、是否能读入、目标 environment 是否为 development。 - 为什么值得做:Session 的导入示例需要读取本地文件、包装成
File、调用importSchema。这些步骤都适合在脚本里提前校验。 - 怎么开始:复用
fs.readFile和 CKTool JSFile的导入流程,在真正调用api.importSchema前打印 team、container、environment。
4. 给运营或支持团队做受限数据修复脚本
- 做什么:把常见修复动作封装成参数化脚本,例如按唯一字段查询 record,再修正某个 field。
- 为什么值得做:Session 展示了按
isoCode3查询 record、用recordChangeTag更新 record 的完整路径。 - 怎么开始:把查询条件限制在唯一字段,更新时传入当前 record 的
recordName和recordChangeTag,只允许脚本操作开发者明确列出的 record type。
关联 Session
- Automate CloudKit tests with cktool and declarative schema — CKTool JS 延续了这场 session 里的 CloudKit 自动化思路,把 schema 和测试准备工作带进 JavaScript 工具链。
- Meet CloudKit Console — CKTool JS 被用于实现 CloudKit Console 的部分功能,理解 Console 有助于理解这些 API 的使用场景。
- What’s new in CloudKit Console — 同一年 CloudKit Console 继续增强网页端管理能力,和 CKTool JS 的脚本化管理形成互补。
- Optimize your use of Core Data and CloudKit — 如果 App 使用 Core Data 同步到 CloudKit,这场 session 讲如何测试和优化同步流程。
评论
GitHub Issues · utterances