Commit 5ac83ffb by luoqi

feat(auth): 品牌/诊所展示名从摄入数据派生 — 多品牌宿主不再显示 GUID

FRIDAY 的 source_unit / clinic_id 都是 GUID(jvs-dw 是"瑞尔/瑞泰"中文名),工作台此前
只能显示 GUID。诊所名机制(host.clinicNames + clinic_directory + CLI 派生)本就存在且
支持文件源 —— 缺的是①导出漏了 organization_name 列 ②friday manifest 没配 directory
③品牌名没有对应机制。

- 契约:TokenDictionary 加 brands 档;Host 加 sourceUnitNames(对称 clinicNames,
  迁移 20260720104938)
- manifest:新增 brand_directory 声明(与 clinic_directory 同形)
- CLI refresh-clinic-names 泛化:一次派生两类字典,单侧声明不误清另一侧;空结果不覆盖
- session:getDisplayNames 一次取两类,dictionary.brands 与 clinics 同款合并
  (服务端派生打底、宿主换票传的覆盖优先)
- friday export.sh:med_check/Mongo 补 organization_name,新增 tenant_info 品牌主档
  (apply 服务 tenant_apply.tenant_info,列名实测 name/name_ab 非 short_name)
- ️ export.sh 加固:所有导出改 tmp→校验非空→mv,裸重定向在库抖动时会清空好文件
  (2026-07-20 踩,62MB med_emr_info.json 被清零)

验证:派生诊所名 11 家 / 品牌名 45 个;friday session 下发 brands 45(成都安玉牙种植
医院/欣美/森德口腔)+ clinics 11;jvs-dw 对照 brands 键不下发、clinics 5 家照旧。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
parent 17ae97bd
...@@ -36,8 +36,16 @@ mysql_csv() { ...@@ -36,8 +36,16 @@ mysql_csv() {
for (const line of lines) { if (line === "") continue; for (const line of lines) { if (line === "") continue;
out.push(line.split("\t").map((f) => q(f === "NULL" ? "" : unesc(f))).join(",")); } out.push(line.split("\t").map((f) => q(f === "NULL" ? "" : unesc(f))).join(",")); }
process.stdout.write(out.join("\n") + "\n"); process.stdout.write(out.join("\n") + "\n");
' > "$out" ' > "$out.tmp"
echo " $out: $(($(wc -l < "$out") - 1)) rows" # ⚠️ 先写 .tmp,校验有数据才替换正式文件 —— 测试库抖动时裸重定向会把好文件清空(2026-07-20 踩)
local n=$(($(wc -l < "$out.tmp") - 1))
if [ "$n" -lt 1 ]; then
rm -f "$out.tmp"
echo " ✗ $out: 导出为空(库不可达?),保留原文件不覆盖" >&2
return 1
fi
mv "$out.tmp" "$out"
echo " $out: $n rows"
} }
echo "── MySQL 导出 ──" echo "── MySQL 导出 ──"
...@@ -48,9 +56,11 @@ mysql_csv "SELECT id,customer_id,contacts_tel,is_default,contacts_type,tel_type, ...@@ -48,9 +56,11 @@ mysql_csv "SELECT id,customer_id,contacts_tel,is_default,contacts_type,tel_type,
# 预约(全状态;transforms 丢草稿 10) # 预约(全状态;transforms 丢草稿 10)
mysql_csv "SELECT id,patient_appointment_id,organization_id,tenant_id,appointment_date,appointment_start,appointment_status,doctor_user_id,appointment_time_length,in_time,created_gmt_at,updated_gmt_at FROM \`arrail-appointment-server\`.appointment_base" appointment_base.csv mysql_csv "SELECT id,patient_appointment_id,organization_id,tenant_id,appointment_date,appointment_start,appointment_status,doctor_user_id,appointment_time_length,in_time,created_gmt_at,updated_gmt_at FROM \`arrail-appointment-server\`.appointment_base" appointment_base.csv
# 影像档案(挂 emr_id 病历号;modality 经 std_check_class 字典翻) # 影像档案(挂 emr_id 病历号;modality 经 std_check_class 字典翻)
mysql_csv "SELECT id,tenant_id,organization_id,patient_id,emr_id,class_code,file_name,file_type,file_url,files_size,shooting_time,created_gmt_at,updated_gmt_at FROM emr.med_check" med_check.csv mysql_csv "SELECT id,tenant_id,organization_id,organization_name,patient_id,emr_id,class_code,file_name,file_type,file_url,files_size,shooting_time,created_gmt_at,updated_gmt_at FROM emr.med_check" med_check.csv
# 影像类型字典(class_code UUID 全局唯一,lookup 不需 tenant 限定) # 影像类型字典(class_code UUID 全局唯一,lookup 不需 tenant 限定)
mysql_csv "SELECT class_code,class_name,tenant_id FROM emr.std_check_class" std_check_class.csv mysql_csv "SELECT class_code,class_name,tenant_id FROM emr.std_check_class" std_check_class.csv
# 品牌主档(apply 服务 tenant_info;brand_directory 派生 host.sourceUnitNames → 前端显示品牌中文名)
mysql_csv "SELECT tenant_id,name,name_ab FROM tenant_apply.tenant_info" tenant_info.csv
# 诊断字典(linkCode → std_code;K 码用,非 K 落自由文本) # 诊断字典(linkCode → std_code;K 码用,非 K 落自由文本)
mysql_csv "SELECT diag_code,diag_name,std_code FROM emr.std_diag" std_diag.csv mysql_csv "SELECT diag_code,diag_name,std_code FROM emr.std_diag" std_diag.csv
# 治疗计划(头:方案名/期望;行:牙位/术式/价格区间)→ treatment_planned # 治疗计划(头:方案名/期望;行:牙位/术式/价格区间)→ treatment_planned
...@@ -95,6 +105,7 @@ docker run --rm mongo:7 mongosh "$FRIDAY_MONGO_URI" --quiet --eval ' ...@@ -95,6 +105,7 @@ docker run --rm mongo:7 mongosh "$FRIDAY_MONGO_URI" --quiet --eval '
emr_sub_id: sid(d.emrSubId), emr_id: sid(d.emrId), emr_sub_id: sid(d.emrSubId), emr_id: sid(d.emrId),
appointment_id: sid(d.appointmentId), appointment_id: sid(d.appointmentId),
tenant_id: sid(d.tenantId), organization_id: sid(d.organizationId), tenant_id: sid(d.tenantId), organization_id: sid(d.organizationId),
organization_name: flat(d.organizationName), // 诊所名(clinic_directory 派生 host.clinicNames)
patient_id: sid(d.patientId), user_id: sid(d.userId), user_name: flat(d.userName), patient_id: sid(d.patientId), user_id: sid(d.userId), user_name: flat(d.userName),
clinic_time: flat(d.clinicTime), visit_indicator: flat(d.visitIndicator), status: flat(d.status), clinic_time: flat(d.clinicTime), visit_indicator: flat(d.visitIndicator), status: flat(d.status),
diag: d.diag ?? [], treat: d.treat ?? [], dispose: d.dispose ?? [], examine: d.examine ?? [], diag: d.diag ?? [], treat: d.treat ?? [], dispose: d.dispose ?? [], examine: d.examine ?? [],
...@@ -108,8 +119,21 @@ docker run --rm mongo:7 mongosh "$FRIDAY_MONGO_URI" --quiet --eval ' ...@@ -108,8 +119,21 @@ docker run --rm mongo:7 mongosh "$FRIDAY_MONGO_URI" --quiet --eval '
print((first ? "" : ",") + JSON.stringify(o)); first = false; print((first ? "" : ",") + JSON.stringify(o)); first = false;
}); });
print("]"); print("]");
' > med_emr_info.json ' > med_emr_info.json.tmp
node -e "const a=JSON.parse(require('fs').readFileSync('med_emr_info.json','utf-8')); console.log(' med_emr_info.json:', a.length, 'docs')" # 同款保护:解析成功且非空才替换(mongosh 连接失败会产出空/半截文件)
node -e "
const fs=require('fs');
let a; try { a = JSON.parse(fs.readFileSync('med_emr_info.json.tmp','utf-8')); } catch (e) {
fs.unlinkSync('med_emr_info.json.tmp');
console.error(' ✗ med_emr_info.json: 导出无效(库不可达?),保留原文件不覆盖'); process.exit(1);
}
if (!Array.isArray(a) || a.length === 0) {
fs.unlinkSync('med_emr_info.json.tmp');
console.error(' ✗ med_emr_info.json: 导出为空,保留原文件不覆盖'); process.exit(1);
}
fs.renameSync('med_emr_info.json.tmp','med_emr_info.json');
console.log(' med_emr_info.json:', a.length, 'docs');
"
echo "── 导出完成 ──" echo "── 导出完成 ──"
ls -la *.csv *.json | awk '{print " " $NF, $5}' ls -la *.csv *.json | awk '{print " " $NF, $5}'
...@@ -27,6 +27,21 @@ identity_namespace_field: tenant_id # patients.source_unit ← 源品牌 ...@@ -27,6 +27,21 @@ identity_namespace_field: tenant_id # patients.source_unit ← 源品牌
amount_unit: yuan amount_unit: yuan
timezone: Asia/Shanghai timezone: Asia/Shanghai
# 诊所名字典来源:病历表自带 organization_name(宿主原生列)。
# `pnpm refresh-clinic-names -- --dir=./data/friday` 据此 DISTINCT 出 id→名,写 host.clinicNames;
# /auth/session 合并进 dictionary.clinics 下发 → 工作台显示诊所中文名而非 GUID。
clinic_directory:
table: med_emr_info
id_field: organization_id
name_field: organization_name
# 品牌名字典来源:apply 服务 tenant_info(品牌主档)。同 CLI 一并派生 → host.sourceUnitNames,
# 经 /auth/session 下发 dictionary.brands;多品牌 SaaS 的 source_unit 是 GUID,靠它显示中文名。
brand_directory:
table: tenant_info
id_field: tenant_id
name_field: name
# ── 增量水位声明(2026-07 与 jvs-dw 统一;file 源放 manifest 顶层)── # ── 增量水位声明(2026-07 与 jvs-dw 统一;file 源放 manifest 顶层)──
# file 装载不消费水位(每轮全量装载,幂等去重兜底);每轮跑完与 jvs-dw 走同一段 finally # file 装载不消费水位(每轮全量装载,幂等去重兜底);每轮跑完与 jvs-dw 走同一段 finally
# 记账 cursor_after = run_start(sync_logs)。用途:delta 导出 WHERE 模板 / push 回放起点。 # 记账 cursor_after = run_start(sync_logs)。用途:delta 导出 WHERE 模板 / push 回放起点。
...@@ -57,6 +72,7 @@ tables: ...@@ -57,6 +72,7 @@ tables:
- { table: med_check, file: med_check.csv } # 影像档案(挂 emr_id 病历号) - { table: med_check, file: med_check.csv } # 影像档案(挂 emr_id 病历号)
- { table: std_check_class, file: std_check_class.csv } # 影像类型字典(class_code→class_name) - { table: std_check_class, file: std_check_class.csv } # 影像类型字典(class_code→class_name)
- { table: std_diag, file: std_diag.csv } # 诊断字典(linkCode→std_code;K 码用) - { table: std_diag, file: std_diag.csv } # 诊断字典(linkCode→std_code;K 码用)
- { table: tenant_info, file: tenant_info.csv } # 品牌主档(brand_directory 派生品牌名)
- { table: customer_treat_plan, file: customer_treat_plan.csv } # 治疗计划头(方案名/期望) - { table: customer_treat_plan, file: customer_treat_plan.csv } # 治疗计划头(方案名/期望)
- { table: customer_treat_plan_item, file: customer_treat_plan_item.csv } # 治疗计划行(牙位/术式/价格) - { table: customer_treat_plan_item, file: customer_treat_plan_item.csv } # 治疗计划行(牙位/术式/价格)
- { table: patient_settlement, file: patient_settlement.csv } # 消费头单(status 1/3 且金额≥0) - { table: patient_settlement, file: patient_settlement.csv } # 消费头单(status 1/3 且金额≥0)
......
-- AlterTable
ALTER TABLE "hosts" ADD COLUMN "source_unit_names" JSONB NOT NULL DEFAULT '{}';
...@@ -128,6 +128,12 @@ model Host { ...@@ -128,6 +128,12 @@ model Host {
/// 形状: { "66701845dd2342e19f9e9f576c4ffe9c": "北京朝阳公园诊所", ... } /// 形状: { "66701845dd2342e19f9e9f576c4ffe9c": "北京朝阳公园诊所", ... }
clinicNames Json @default("{}") @map("clinic_names") clinicNames Json @default("{}") @map("clinic_names")
/// 品牌名字典:source_unit id 中文名。**从摄入数据派生**(FRIDAY tenant_info.name ),
/// clinicNames 同机制, /auth/session 合并进 dictionary.brands 下发。
/// 多品牌宿主(source_unit GUID)需要它显示品牌名;jvs-dw source_unit 本身即中文名,留空即可。
/// 形状: { "3a9d9c680c364df9a84bfd5de91e9324": "XX口腔", ... }
sourceUnitNames Json @default("{}") @map("source_unit_names")
/// 执行结果回执地址(宿主接收 POST HTTPS endpoint);null = 不启用回执(opt-in) /// 执行结果回执地址(宿主接收 POST HTTPS endpoint);null = 不启用回执(opt-in)
callbackUrl String? @map("callback_url") callbackUrl String? @map("callback_url")
/// 回执验签用的 HMAC 共享密钥;PAC **签名**,故存**原文**(不同于 appSecret 存哈希) /// 回执验签用的 HMAC 共享密钥;PAC **签名**,故存**原文**(不同于 appSecret 存哈希)
......
/** /**
* refresh-clinic-names — 从摄入源派生「诊所 id → 名字」写入 host.clinicNames。 * refresh-clinic-names — 从摄入源派生展示名字典写入 host:
* - `clinic_directory` → host.clinicNames (诊所 id → 名)
* - `brand_directory` → host.sourceUnitNames (品牌/source_unit id → 名;多品牌宿主用)
* *
* /auth/session 会把 host.clinicNames 合并进 dictionary.clinics 下发, * /auth/session 会把 host.clinicNames 合并进 dictionary.clinics 下发,
* 前端显示诊所名不再依赖宿主换票时传 dictionary(见 Host.clinicNames 注释)。 * 前端显示诊所名不再依赖宿主换票时传 dictionary(见 Host.clinicNames 注释)。
* *
* 源在 manifest 的 `clinic_directory: { table, id_field, name_field }` 声明: * 源在 manifest 的 `clinic_directory` / `brand_directory: { table, id_field, name_field }` 声明:
* - sql_source 模式:SELECT DISTINCT id,name FROM <table>(连 DW,env 覆盖连接) * - sql_source 模式:SELECT DISTINCT id,name FROM <table>(连 DW,env 覆盖连接)
* - 文件模式:读 tables[] 里对应 <table> 的 CSV/JSON,去重 * - 文件模式:读 tables[] 里对应 <table> 的 CSV/JSON,去重
* *
...@@ -71,7 +73,7 @@ function deriveFromFile( ...@@ -71,7 +73,7 @@ function deriveFromFile(
dir: { table: string; id_field: string; name_field: string }, dir: { table: string; id_field: string; name_field: string },
): Record<string, string> { ): Record<string, string> {
const t = tables.find((x) => x.table === dir.table); const t = tables.find((x) => x.table === dir.table);
if (!t) throw new Error(`clinic_directory.table=${dir.table} 不在 tables[] 里`); if (!t) throw new Error(`directory.table=${dir.table} 不在 tables[] 里`);
const p = path.resolve(manifestDir, t.file); const p = path.resolve(manifestDir, t.file);
const raw = fs.readFileSync(p, 'utf-8'); const raw = fs.readFileSync(p, 'utf-8');
const rows: Array<Record<string, unknown>> = t.file.toLowerCase().endsWith('.json') const rows: Array<Record<string, unknown>> = t.file.toLowerCase().endsWith('.json')
...@@ -101,19 +103,25 @@ async function main(): Promise<void> { ...@@ -101,19 +103,25 @@ async function main(): Promise<void> {
); );
const hostName = flag('host') || manifest.host_name; const hostName = flag('host') || manifest.host_name;
const cd = manifest.clinic_directory; const cd = manifest.clinic_directory;
if (!cd) { const bd = manifest.brand_directory;
logger.error(`manifest 未声明 clinic_directory —— 无法派生诊所名`); if (!cd && !bd) {
logger.error(`manifest 未声明 clinic_directory / brand_directory —— 无可派生的名字典`);
process.exit(1); process.exit(1);
} }
const map = manifest.sql_source // 两类字典同机制派生(声明了才跑);sql_source 走 CH,文件源读 tables[]
? await deriveFromClickhouse(manifest.sql_source, cd, logger) const derive = async (d: { table: string; id_field: string; name_field: string }) =>
: deriveFromFile(manifestDir, manifest.tables ?? [], cd); manifest.sql_source
? await deriveFromClickhouse(manifest.sql_source, d, logger)
: deriveFromFile(manifestDir, manifest.tables ?? [], d);
const n = Object.keys(map).length; const clinicMap = cd ? await derive(cd) : {};
logger.log(`派生诊所名 ${n} 家`); const brandMap = bd ? await derive(bd) : {};
if (n === 0) { const nc = Object.keys(clinicMap).length;
logger.warn('派生结果为空,跳过写入(不清空既有 clinicNames)'); const nb = Object.keys(brandMap).length;
logger.log(`派生:诊所名 ${nc} 家 / 品牌名 ${nb} 个`);
if (nc === 0 && nb === 0) {
logger.warn('派生结果全空,跳过写入(不清空既有字典)');
process.exit(0); process.exit(0);
} }
...@@ -122,8 +130,18 @@ async function main(): Promise<void> { ...@@ -122,8 +130,18 @@ async function main(): Promise<void> {
const prisma = app.get(PrismaService); const prisma = app.get(PrismaService);
const host = await prisma.host.findFirst({ where: { name: hostName } }); const host = await prisma.host.findFirst({ where: { name: hostName } });
if (!host) throw new Error(`host "${hostName}" 不存在`); if (!host) throw new Error(`host "${hostName}" 不存在`);
await prisma.host.update({ where: { id: host.id }, data: { clinicNames: map } }); // 空结果不覆盖既有(单侧声明时不误清另一侧)
logger.log(`✓ 已写入 host="${hostName}" clinicNames ${n} 家(样例: ${Object.values(map).slice(0, 3).join(' / ')})`); await prisma.host.update({
where: { id: host.id },
data: {
...(nc > 0 ? { clinicNames: clinicMap } : {}),
...(nb > 0 ? { sourceUnitNames: brandMap } : {}),
},
});
if (nc > 0)
logger.log(`✓ host="${hostName}" clinicNames ${nc} 家(样例: ${Object.values(clinicMap).slice(0, 3).join(' / ')})`);
if (nb > 0)
logger.log(`✓ host="${hostName}" sourceUnitNames ${nb} 个(样例: ${Object.values(brandMap).slice(0, 3).join(' / ')})`);
} finally { } finally {
await app.close(); await app.close();
} }
......
...@@ -105,13 +105,19 @@ export class AuthController { ...@@ -105,13 +105,19 @@ export class AuthController {
) { ) {
// 诊所名字典:服务端派生(host.clinicNames)打底,宿主换票传的 dictionary.clinics 覆盖优先。 // 诊所名字典:服务端派生(host.clinicNames)打底,宿主换票传的 dictionary.clinics 覆盖优先。
// 这样前端显示诊所名不再依赖每次登录都传全 —— 服务端补齐 GUID→中文名,登录传的仅作覆盖。 // 这样前端显示诊所名不再依赖每次登录都传全 —— 服务端补齐 GUID→中文名,登录传的仅作覆盖。
const serverClinicNames = await this.auth.getClinicNames(user.hostId); const serverNames = await this.auth.getDisplayNames(user.hostId);
const loginDict = user.dictionary; const loginDict = user.dictionary;
const mergedClinics = { ...serverClinicNames, ...(loginDict?.clinics ?? {}) }; const mergedClinics = { ...serverNames.clinics, ...(loginDict?.clinics ?? {}) };
// 品牌名同机制:服务端 host.sourceUnitNames 打底,登录传的覆盖。
// 多品牌宿主(source_unit 是 GUID)靠它显示中文名;jvs-dw 的 source_unit 本身即名字,恒空不影响。
const mergedBrands = { ...serverNames.brands, ...(loginDict?.brands ?? {}) };
const dictionary = const dictionary =
Object.keys(mergedClinics).length > 0 || loginDict?.users Object.keys(mergedClinics).length > 0 ||
Object.keys(mergedBrands).length > 0 ||
loginDict?.users
? { ? {
...(Object.keys(mergedClinics).length > 0 ? { clinics: mergedClinics } : {}), ...(Object.keys(mergedClinics).length > 0 ? { clinics: mergedClinics } : {}),
...(Object.keys(mergedBrands).length > 0 ? { brands: mergedBrands } : {}),
...(loginDict?.users ? { users: loginDict.users } : {}), ...(loginDict?.users ? { users: loginDict.users } : {}),
} }
: undefined; : undefined;
......
...@@ -119,13 +119,19 @@ export class AuthService { ...@@ -119,13 +119,19 @@ export class AuthService {
return (host?.actionUrls as Record<string, string | null>) ?? {}; return (host?.actionUrls as Record<string, string | null>) ?? {};
} }
/// 服务端诊所名字典(从摄入数据派生,存 host.clinicNames)。session 合并进 dictionary.clinics。 /// 服务端展示名字典(均从摄入数据派生):诊所 host.clinicNames / 品牌 host.sourceUnitNames。
async getClinicNames(hostId: string): Promise<Record<string, string>> { /// session 分别合并进 dictionary.clinics / dictionary.brands(宿主换票传的同名项覆盖优先)。
async getDisplayNames(
hostId: string,
): Promise<{ clinics: Record<string, string>; brands: Record<string, string> }> {
const host = await this.prisma.host.findUnique({ const host = await this.prisma.host.findUnique({
where: { id: hostId }, where: { id: hostId },
select: { clinicNames: true }, select: { clinicNames: true, sourceUnitNames: true },
}); });
return (host?.clinicNames as Record<string, string>) ?? {}; return {
clinics: (host?.clinicNames as Record<string, string>) ?? {},
brands: (host?.sourceUnitNames as Record<string, string>) ?? {},
};
} }
async exchangeToken(req: TokenExchangeRequest): Promise<TokenExchangeResponse> { async exchangeToken(req: TokenExchangeRequest): Promise<TokenExchangeResponse> {
......
...@@ -201,6 +201,18 @@ export const ColdImportManifestSchema = z ...@@ -201,6 +201,18 @@ export const ColdImportManifestSchema = z
}) })
.optional(), .optional(),
/// 品牌名字典来源(可选)—— 声明哪张表的哪两列 = 品牌(source_unit)id → 名字。
/// 同 `refresh-clinic-names` CLI 一并派生,写 host.sourceUnitNames,
/// 经 /auth/session 合并进 dictionary.brands 下发。多品牌宿主(source_unit 为 GUID)用;
/// source_unit 本身即中文名的宿主(jvs-dw)不必配。
brand_directory: z
.object({
table: z.string().min(1),
id_field: z.string().min(1),
name_field: z.string().min(1),
})
.optional(),
/// **Layer A.5 transforms**(可选)— 在 sources 之后、AssemblerEngine 之前跑。 /// **Layer A.5 transforms**(可选)— 在 sources 之后、AssemblerEngine 之前跑。
/// 6 operator 白名单:project / split_json_array / derive / route_by_pattern / pick_first_nonzero / filter。 /// 6 operator 白名单:project / split_json_array / derive / route_by_pattern / pick_first_nonzero / filter。
/// 详见 apps/pac-docs/content/docs/architecture/data-ingestion.mdx §五。 /// 详见 apps/pac-docs/content/docs/architecture/data-ingestion.mdx §五。
......
...@@ -15,8 +15,11 @@ export const TokenDictionarySchema = z ...@@ -15,8 +15,11 @@ export const TokenDictionarySchema = z
.object({ .object({
clinics: z.record(z.string(), z.string()).optional(), clinics: z.record(z.string(), z.string()).optional(),
users: z.record(z.string(), z.string()).optional(), users: z.record(z.string(), z.string()).optional(),
/// 品牌(source_unit)id→名。多品牌宿主(FRIDAY)source_unit 是 GUID,前端需名字显示;
/// 单品牌/名字即 source_unit 的宿主(jvs-dw '瑞尔')可不传。服务端 host.sourceUnitNames 补齐。
brands: z.record(z.string(), z.string()).optional(),
}) })
.describe('Optional id→displayName dictionary (clinics / users), host-provided'); .describe('Optional id→displayName dictionary (clinics / users / brands), host-provided');
export type TokenDictionary = z.infer<typeof TokenDictionarySchema>; export type TokenDictionary = z.infer<typeof TokenDictionarySchema>;
// `.strict()` on public-facing input: unknown keys are rejected with a clear // `.strict()` on public-facing input: unknown keys are rejected with a clear
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment