feat(db,contracts): full v0.1 data model migrations and shared zod contracts (W1-1)
- migrations 0002-0012 split by domain (foundation/instances/content/identity/ sources/assets/policy/execution/agent/ops/organization): 41 tables, uuid pk, created_at/updated_at + touch trigger, text+CHECK status sets, pgcrypto/pg_trgm - key constraints: source_release(site_id, site_release_id), observation(feed_id, guid), external_identity accepted partial unique, download_task(instance_id, torrent_hash), one active acquisition_plan per slot, one active path_binding per entity/season, asset_location root+path uniqueness, organization_record change_seq/change_xid cursor - secret table (ciphertext/nonce/kdf) + secret_ref columns; outbox dispatch fields (ADR-002) - src/types.ts generated from the migrated schema (scripts/gen-types.ts), drift-checked in tests; int8 -> number, date -> string parsers; migrator accepts migrationTableSchema - contracts: Row/Create/Update schemas per domain, public organization-record API (changeSeq cursor), GlobalSettings, CapabilityDescriptor + 14 capability IO schemas, typed SSE events, Api endpoint table - tests: migration up/down round trip on real PG in an isolated schema, constraint checks, contracts<->db column/enum sync, positive/negative parse for every exported schema Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
1 parent
7ba712ea55
commit
cc8b3a7df4
42 files changed
+7026
-25
No files matched your search
@@ -0,0 +1,198 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { z } from "zod";
|
||||
import * as C from "../src";
|
||||
|
||||
// 与 packages/db/src/types.ts(从迁移反射生成)做文本级对照:
|
||||
// 1) 每张表的列 ⇔ 对应 Row schema 的字段(camelCase),只允许显式列出的差异;
|
||||
// 2) 每个 CHECK 枚举 ⇔ contracts 中对应 z.enum 的取值。
|
||||
// 只读文件,不引入 @umt/db 依赖。
|
||||
|
||||
const typesSrc = readFileSync(new URL("../../db/src/types.ts", import.meta.url), "utf8");
|
||||
|
||||
const camel = (s: string) => s.replace(/_([a-z0-9])/g, (_, c: string) => c.toUpperCase());
|
||||
|
||||
function tableColumns(): Map<string, string[]> {
|
||||
const out = new Map<string, string[]>();
|
||||
for (const m of typesSrc.matchAll(/export interface (\w+)Table \{\n([\s\S]*?)\n\}/g)) {
|
||||
const cols = [...m[2]!.matchAll(/^ {2}(\w+):/gm)].map((x) => x[1]!);
|
||||
out.set(m[1]!, cols);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function enumAliases(): Map<string, string[]> {
|
||||
const out = new Map<string, string[]>();
|
||||
for (const m of typesSrc.matchAll(/export type (\w+) =([^;]+);/g)) {
|
||||
if (m[1] === "Timestamptz" || m[1] === "Json" || m[2]!.includes("<")) continue;
|
||||
out.set(
|
||||
m[1]!,
|
||||
[...m[2]!.matchAll(/"([^"]*)"/g)].map((x) => x[1]!),
|
||||
);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
type Obj = z.ZodObject;
|
||||
|
||||
/** 表(PascalCase)→ Row schema 与允许的差异 */
|
||||
const ROWS: Record<string, { row: Obj; dbOnly?: string[]; apiOnly?: string[] }> = {
|
||||
AcquisitionPlan: { row: C.AcquisitionPlan },
|
||||
AcquisitionSlot: { row: C.AcquisitionSlot },
|
||||
AgentRun: { row: C.AgentRun },
|
||||
ApiKey: { row: C.ApiKey, dbOnly: ["keyHash"] },
|
||||
Approval: { row: C.Approval },
|
||||
Artifact: { row: C.Artifact },
|
||||
AssetLocation: { row: C.AssetLocation },
|
||||
AssetOperation: { row: C.AssetOperation },
|
||||
AssetUnitCoverage: { row: C.AssetUnitCoverage },
|
||||
AuditEvent: { row: C.AuditEvent },
|
||||
AutomationGrant: { row: C.AutomationGrant },
|
||||
ContentUnit: { row: C.ContentUnit },
|
||||
Decision: { row: C.Decision },
|
||||
DownloadTask: { row: C.DownloadTask },
|
||||
DownloaderInstance: { row: C.DownloaderInstance },
|
||||
ExternalIdentity: { row: C.ExternalIdentity },
|
||||
FeedSource: { row: C.FeedSource },
|
||||
IdentityRevision: { row: C.IdentityRevision },
|
||||
LibraryRoot: { row: C.LibraryRoot },
|
||||
MediaAsset: { row: C.MediaAsset },
|
||||
MediaEntity: { row: C.MediaEntity },
|
||||
MediaServerInstance: { row: C.MediaServerInstance },
|
||||
Observation: { row: C.Observation },
|
||||
OrganizationFileResult: { row: C.OrganizationFileResult },
|
||||
OrganizationRecord: { row: C.OrganizationRecord, dbOnly: ["changeXid"] },
|
||||
OutboxEvent: { row: C.OutboxEvent },
|
||||
PathBinding: { row: C.PathBinding },
|
||||
PathMapping: { row: C.PathMapping },
|
||||
PolicyRevision: { row: C.PolicyRevision },
|
||||
PreferenceSuppression: { row: C.PreferenceSuppression },
|
||||
PreferenceTag: { row: C.PreferenceTag },
|
||||
ReconcileRun: { row: C.ReconcileRun },
|
||||
Secret: { row: C.Secret, dbOnly: ["ciphertext", "nonce", "authTag"] },
|
||||
ServerMapping: { row: C.ServerMapping },
|
||||
Settings: { row: C.SettingEntry },
|
||||
Site: { row: C.Site },
|
||||
SiteAccount: { row: C.SiteAccount },
|
||||
SourceRelease: { row: C.SourceRelease },
|
||||
Subscription: { row: C.Subscription },
|
||||
Task: { row: C.Task },
|
||||
TitleAlias: { row: C.TitleAlias },
|
||||
};
|
||||
|
||||
/** DB 枚举类型名 → contracts 枚举 */
|
||||
const ENUMS: Record<string, z.ZodEnum> = {
|
||||
AcquisitionPlanPurpose: C.PlanPurpose,
|
||||
AcquisitionPlanStatus: C.PlanStatus,
|
||||
AcquisitionPlanPriority: C.AcquisitionPriority,
|
||||
AcquisitionSlotStatus: C.SlotStatus,
|
||||
AgentRunTrigger: C.AgentTrigger,
|
||||
AgentRunStatus: C.AgentRunStatus,
|
||||
ApprovalStatus: C.ApprovalStatus,
|
||||
ArtifactKind: C.ArtifactKind,
|
||||
AssetLocationLocationKind: C.LocationKind,
|
||||
AssetLocationStatus: C.LocationStatus,
|
||||
AssetOperationPhase: C.OperationPhase,
|
||||
AssetOperationKind: C.OperationKind,
|
||||
AssetOperationStatus: C.OperationStatus,
|
||||
AssetUnitCoverageCoverage: C.CoverageKind,
|
||||
AssetUnitCoverageSegment: C.Segment,
|
||||
AutomationGrantStatus: C.GrantStatus,
|
||||
ContentUnitKind: C.ContentUnitKind,
|
||||
ContentUnitContentType: C.ContentType,
|
||||
ContentUnitSegment: C.Segment,
|
||||
ContentUnitStatus: C.ContentUnitStatus,
|
||||
DecisionKind: C.DecisionKind,
|
||||
DecisionStatus: C.DecisionStatus,
|
||||
DecisionSeverity: C.Severity,
|
||||
DownloadTaskOrigin: C.DownloadOrigin,
|
||||
DownloadTaskStatus: C.DownloadStatus,
|
||||
DownloadTaskPauseReason: C.PauseReason,
|
||||
DownloaderInstanceKind: C.DownloaderKind,
|
||||
DownloaderInstanceHealthStatus: C.InstanceHealth,
|
||||
ExternalIdentityEntityType: C.ExternalEntityType,
|
||||
ExternalIdentityStatus: C.IdentityStatus,
|
||||
ExternalIdentityEvidenceLevel: C.EvidenceLevel,
|
||||
ExternalIdentityVerifyStatus: C.IdentityVerifyStatus,
|
||||
FeedSourcePurpose: C.FeedPurpose,
|
||||
FeedSourceKind: C.FeedKind,
|
||||
FeedSourceHealthStatus: C.FeedHealth,
|
||||
IdentityRevisionAction: C.IdentityRevisionAction,
|
||||
IdentityRevisionFromStatus: C.IdentityStatus,
|
||||
IdentityRevisionToStatus: C.IdentityStatus,
|
||||
IdentityRevisionActorType: C.ActorType,
|
||||
LibraryRootMediaKind: C.LibraryMediaKind,
|
||||
LibraryRootManagementMode: C.ManagementMode,
|
||||
LibraryRootHealthStatus: C.InstanceHealth,
|
||||
MediaAssetKind: C.AssetKind,
|
||||
MediaAssetStatus: C.AssetStatus,
|
||||
MediaEntityKind: C.MediaKind,
|
||||
MediaEntityStatus: C.MediaEntityStatus,
|
||||
MediaServerInstanceKind: C.MediaServerKind,
|
||||
MediaServerInstanceHealthStatus: C.InstanceHealth,
|
||||
ObservationStatus: C.ObservationStatus,
|
||||
OrganizationFileResultRole: C.FileRole,
|
||||
OrganizationFileResultMode: C.OrganizationAction,
|
||||
OrganizationFileResultStatus: C.FileResultStatus,
|
||||
OrganizationRecordTrigger: C.OrganizationTrigger,
|
||||
OrganizationRecordActorType: C.ActorType,
|
||||
OrganizationRecordSourcePathView: C.SourcePathView,
|
||||
OrganizationRecordAction: C.OrganizationAction,
|
||||
OrganizationRecordStatus: C.OrganizationStatus,
|
||||
OutboxEventStatus: C.OutboxStatus,
|
||||
PathBindingBindingKind: C.BindingKind,
|
||||
PathBindingStatus: C.BindingStatus,
|
||||
PathMappingViewKind: C.PathViewKind,
|
||||
PathMappingVerifyStatus: C.PathMappingVerifyStatus,
|
||||
PolicyRevisionKind: C.PolicyKind,
|
||||
PolicyRevisionStatus: C.PolicyStatus,
|
||||
PreferenceTagKind: C.PreferenceKind,
|
||||
PreferenceTagOrigin: C.PreferenceOrigin,
|
||||
PreferenceTagStatus: C.PreferenceStatus,
|
||||
ReconcileRunKind: C.ReconcileKind,
|
||||
ReconcileRunMode: C.ReconcileMode,
|
||||
ReconcileRunStatus: C.ReconcileStatus,
|
||||
SecretPurpose: C.SecretPurpose,
|
||||
ServerMappingItemType: C.ServerItemType,
|
||||
ServerMappingMatchStatus: C.ServerMatchStatus,
|
||||
SiteAdapter: C.SiteAdapter,
|
||||
SiteHealthStatus: C.SiteHealth,
|
||||
SiteCircuitState: C.CircuitState,
|
||||
SiteAccountAuthKind: C.SiteAuthKind,
|
||||
SiteAccountStatus: C.SiteAccountStatus,
|
||||
SourceReleaseResolutionStatus: C.ResolutionStatus,
|
||||
SourceReleaseAvailability: C.ReleaseAvailability,
|
||||
SubscriptionMediaKind: C.SubscriptionMediaKind,
|
||||
SubscriptionMode: C.SubscriptionMode,
|
||||
SubscriptionSeasonScope: C.SeasonScope,
|
||||
SubscriptionOrigin: C.SubscriptionOrigin,
|
||||
SubscriptionPriority: C.AcquisitionPriority,
|
||||
SubscriptionStatus: C.SubscriptionStatus,
|
||||
TaskStatus: C.TaskStatus,
|
||||
TitleAliasKind: C.AliasKind,
|
||||
TitleAliasSource: C.AliasSource,
|
||||
};
|
||||
|
||||
describe("contracts ⇔ db 表结构同步", () => {
|
||||
const tables = tableColumns();
|
||||
|
||||
it("每张表都有 Row schema", () => {
|
||||
expect([...tables.keys()].sort()).toEqual(Object.keys(ROWS).sort());
|
||||
});
|
||||
|
||||
for (const [table, spec] of Object.entries(ROWS)) {
|
||||
it(`${table} 列与 Row 字段一致`, () => {
|
||||
const dbCols = (tables.get(table) ?? []).map(camel).filter((c) => !spec.dbOnly?.includes(c));
|
||||
const apiCols = Object.keys(spec.row.shape).filter((c) => !spec.apiOnly?.includes(c));
|
||||
expect(apiCols.sort()).toEqual(dbCols.sort());
|
||||
});
|
||||
}
|
||||
|
||||
it("每个 DB 枚举都有对应 contracts 枚举且取值一致", () => {
|
||||
const aliases = enumAliases();
|
||||
expect([...aliases.keys()].sort()).toEqual(Object.keys(ENUMS).sort());
|
||||
for (const [name, values] of aliases) {
|
||||
expect(ENUMS[name]!.options, name).toEqual(values);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user