fix: baseline invalidation registry contracts
This commit is contained in:
+117
-2
@@ -25,6 +25,10 @@ type RegistryReference = Readonly<{
|
||||
targetField: string;
|
||||
}>;
|
||||
type RegistryConsumer = Readonly<{ path: string; token: string }>;
|
||||
type RegistrySnapshotProjection = Readonly<{
|
||||
singletonRowKey: string;
|
||||
canonicalArrayKeyFields: Readonly<Record<string, readonly string[]>>;
|
||||
}>;
|
||||
type RegistrySpecification = Readonly<{
|
||||
registryId: string;
|
||||
owner: string;
|
||||
@@ -38,6 +42,7 @@ type RegistrySpecification = Readonly<{
|
||||
keyField?: string;
|
||||
uniqueFields?: readonly string[];
|
||||
uniqueFieldSets?: readonly (readonly string[])[];
|
||||
snapshotProjection?: RegistrySnapshotProjection;
|
||||
allowedValues?: Readonly<Record<string, readonly unknown[]>>;
|
||||
references?: readonly RegistryReference[];
|
||||
breakingFields?: readonly string[];
|
||||
@@ -215,6 +220,111 @@ function projectRegistryRows(
|
||||
return projected;
|
||||
}
|
||||
|
||||
function snapshotKeyField(
|
||||
value: unknown,
|
||||
fieldPath: string,
|
||||
): FieldLookup {
|
||||
return fieldPath === "$value"
|
||||
? Object.freeze({ found: true, value })
|
||||
: lookupField(value, fieldPath);
|
||||
}
|
||||
|
||||
function replaceProjectedField(
|
||||
row: RegistryRow,
|
||||
fieldPath: string,
|
||||
value: unknown,
|
||||
): boolean {
|
||||
const segments = fieldPath.split(".");
|
||||
const finalSegment = segments.pop();
|
||||
if (!finalSegment) return false;
|
||||
let current = row;
|
||||
for (const segment of segments) {
|
||||
const next = current[segment];
|
||||
if (!next || typeof next !== "object" || Array.isArray(next)) {
|
||||
return false;
|
||||
}
|
||||
current = next as RegistryRow;
|
||||
}
|
||||
current[finalSegment] = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
function projectSnapshotRows(
|
||||
specification: RegistrySpecification,
|
||||
exportedValue: unknown,
|
||||
validatedRows: RegistryRows,
|
||||
): RegistryRows | null {
|
||||
const projection = specification.snapshotProjection;
|
||||
if (!projection) return validatedRows;
|
||||
if (
|
||||
!exportedValue ||
|
||||
typeof exportedValue !== "object" ||
|
||||
Array.isArray(exportedValue)
|
||||
) {
|
||||
failures.push(
|
||||
`${specification.registryId} snapshot projection requires an object export`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const canonicalRow = canonicalizeRegistryValue(exportedValue) as RegistryRow;
|
||||
for (const [fieldPath, keyFields] of Object.entries(
|
||||
projection.canonicalArrayKeyFields,
|
||||
)) {
|
||||
const selected = lookupField(exportedValue, fieldPath);
|
||||
if (!selected.found || !Array.isArray(selected.value)) {
|
||||
failures.push(
|
||||
`${specification.registryId} snapshot field ${fieldPath} must be an array`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
const keyedItems: Array<{ identity: string; value: unknown }> = [];
|
||||
const identities = new Set<string>();
|
||||
for (const [index, item] of selected.value.entries()) {
|
||||
const keyValues = keyFields.map((field) => snapshotKeyField(item, field));
|
||||
const missingIndex = keyValues.findIndex((field) => !field.found);
|
||||
if (missingIndex >= 0) {
|
||||
failures.push(
|
||||
`${specification.registryId} snapshot field ${fieldPath}[${index}] is missing key ${keyFields[missingIndex]}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
const identity = JSON.stringify(
|
||||
keyValues.map((field) => canonicalizeRegistryValue(field.value)),
|
||||
);
|
||||
if (identities.has(identity)) {
|
||||
failures.push(
|
||||
`${specification.registryId} snapshot field ${fieldPath} duplicates key ${identity}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
identities.add(identity);
|
||||
keyedItems.push({
|
||||
identity,
|
||||
value: canonicalizeRegistryValue(item),
|
||||
});
|
||||
}
|
||||
keyedItems.sort((left, right) =>
|
||||
left.identity.localeCompare(right.identity),
|
||||
);
|
||||
if (
|
||||
!replaceProjectedField(
|
||||
canonicalRow,
|
||||
fieldPath,
|
||||
keyedItems.map((item) => item.value),
|
||||
)
|
||||
) {
|
||||
failures.push(
|
||||
`${specification.registryId} snapshot field ${fieldPath} cannot be projected`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return Object.freeze({
|
||||
[projection.singletonRowKey]: canonicalRow,
|
||||
});
|
||||
}
|
||||
|
||||
async function filesBelow(directory: string): Promise<string[]> {
|
||||
try {
|
||||
const entries = await readdir(directory, { withFileTypes: true });
|
||||
@@ -367,14 +477,19 @@ for (const specification of governance.registries) {
|
||||
...(specification.uniqueFieldSets
|
||||
? { uniqueFieldSets: specification.uniqueFieldSets }
|
||||
: {}),
|
||||
...(specification.snapshotProjection
|
||||
? { snapshotProjection: specification.snapshotProjection }
|
||||
: {}),
|
||||
});
|
||||
const snapshotRows = projectSnapshotRows(specification, rows, registryRows);
|
||||
if (!snapshotRows) continue;
|
||||
snapshots.push({
|
||||
registryId: specification.registryId,
|
||||
owner: specification.owner,
|
||||
source: sourcePath ?? specification.path,
|
||||
rowCount: Object.keys(registryRows).length,
|
||||
rowCount: Object.keys(snapshotRows).length,
|
||||
contract,
|
||||
rows: canonicalizeRegistryValue(registryRows),
|
||||
rows: canonicalizeRegistryValue(snapshotRows),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user