delta validation and error handling done

This commit is contained in:
Mykola Bilokonsky 2025-06-09 12:47:37 -04:00
parent 969e6ddc10
commit ba9cbc220d
4 changed files with 556 additions and 6 deletions

View File

@ -0,0 +1,359 @@
import { DeltaV1, DeltaV2 } from "../src/delta";
import {
InvalidDeltaFormatError,
MissingRequiredFieldError,
InvalidPointerError,
validateDeltaNetworkImageV1,
validateDeltaNetworkImageV2
} from "../src/delta-validation";
describe("Delta Validation", () => {
describe("Invalid Delta Formats", () => {
describe("DeltaV1 validation", () => {
it("should throw error for non-object delta", () => {
expect(() => validateDeltaNetworkImageV1(null)).toThrow(InvalidDeltaFormatError);
expect(() => validateDeltaNetworkImageV1("string")).toThrow(InvalidDeltaFormatError);
expect(() => validateDeltaNetworkImageV1(123)).toThrow(InvalidDeltaFormatError);
expect(() => validateDeltaNetworkImageV1([])).toThrow(InvalidDeltaFormatError);
});
it("should throw error for invalid ID types", () => {
const invalidDeltas = [
{ id: null, timeCreated: 123, host: "host", creator: "creator", pointers: [] },
{ id: 123, timeCreated: 123, host: "host", creator: "creator", pointers: [] },
{ id: "", timeCreated: 123, host: "host", creator: "creator", pointers: [] },
{ id: {}, timeCreated: 123, host: "host", creator: "creator", pointers: [] }
];
invalidDeltas.forEach(delta => {
expect(() => validateDeltaNetworkImageV1(delta)).toThrow(InvalidDeltaFormatError);
});
});
it("should throw error for invalid timestamp", () => {
const invalidDeltas = [
{ id: "id", timeCreated: "123", host: "host", creator: "creator", pointers: [] },
{ id: "id", timeCreated: -123, host: "host", creator: "creator", pointers: [] },
{ id: "id", timeCreated: 0, host: "host", creator: "creator", pointers: [] },
{ id: "id", timeCreated: null, host: "host", creator: "creator", pointers: [] }
];
invalidDeltas.forEach(delta => {
expect(() => validateDeltaNetworkImageV1(delta)).toThrow(InvalidDeltaFormatError);
});
});
it("should throw error for invalid host/creator", () => {
const invalidDeltas = [
{ id: "id", timeCreated: 123, host: null, creator: "creator", pointers: [] },
{ id: "id", timeCreated: 123, host: "", creator: "creator", pointers: [] },
{ id: "id", timeCreated: 123, host: 123, creator: "creator", pointers: [] },
{ id: "id", timeCreated: 123, host: "host", creator: null, pointers: [] },
{ id: "id", timeCreated: 123, host: "host", creator: "", pointers: [] },
{ id: "id", timeCreated: 123, host: "host", creator: 123, pointers: [] }
];
invalidDeltas.forEach(delta => {
expect(() => validateDeltaNetworkImageV1(delta)).toThrow(InvalidDeltaFormatError);
});
});
it("should throw error for non-array pointers", () => {
const invalidDeltas = [
{ id: "id", timeCreated: 123, host: "host", creator: "creator", pointers: null },
{ id: "id", timeCreated: 123, host: "host", creator: "creator", pointers: {} },
{ id: "id", timeCreated: 123, host: "host", creator: "creator", pointers: "pointers" },
{ id: "id", timeCreated: 123, host: "host", creator: "creator", pointers: 123 }
];
invalidDeltas.forEach(delta => {
expect(() => validateDeltaNetworkImageV1(delta)).toThrow(InvalidDeltaFormatError);
});
});
it("should throw error for empty pointers array", () => {
const delta = { id: "id", timeCreated: 123, host: "host", creator: "creator", pointers: [] };
expect(() => validateDeltaNetworkImageV1(delta)).toThrow(InvalidDeltaFormatError);
});
it("should throw error for invalid pointer structure", () => {
const invalidPointers = [
[null],
["string"],
[123],
[{ localContext: null, target: "target" }],
[{ localContext: "", target: "target" }],
[{ localContext: 123, target: "target" }],
[{ localContext: "context", target: undefined }],
[{ localContext: "context", target: {} }],
[{ localContext: "context", target: [] }]
];
invalidPointers.forEach(pointers => {
const delta = { id: "id", timeCreated: 123, host: "host", creator: "creator", pointers };
expect(() => validateDeltaNetworkImageV1(delta)).toThrow(InvalidPointerError);
});
});
it("should throw error for invalid targetContext", () => {
const invalidPointers = [
[{ localContext: "context", target: "target", targetContext: null }],
[{ localContext: "context", target: "target", targetContext: "" }],
[{ localContext: "context", target: "target", targetContext: 123 }],
[{ localContext: "context", target: "target", targetContext: {} }]
];
invalidPointers.forEach(pointers => {
const delta = { id: "id", timeCreated: 123, host: "host", creator: "creator", pointers };
expect(() => validateDeltaNetworkImageV1(delta)).toThrow(InvalidPointerError);
});
});
it("should throw error for pointer consistency violation", () => {
// If targetContext exists, target must be a string (reference)
const pointers = [{ localContext: "context", target: 123, targetContext: "property" }];
const delta = { id: "id", timeCreated: 123, host: "host", creator: "creator", pointers };
expect(() => validateDeltaNetworkImageV1(delta)).toThrow(InvalidPointerError);
});
});
describe("DeltaV2 validation", () => {
it("should throw error for non-object delta", () => {
expect(() => validateDeltaNetworkImageV2(null)).toThrow(InvalidDeltaFormatError);
expect(() => validateDeltaNetworkImageV2("string")).toThrow(InvalidDeltaFormatError);
expect(() => validateDeltaNetworkImageV2(123)).toThrow(InvalidDeltaFormatError);
expect(() => validateDeltaNetworkImageV2([])).toThrow(InvalidDeltaFormatError);
});
it("should throw error for invalid pointers object", () => {
const invalidDeltas = [
{ id: "id", timeCreated: 123, host: "host", creator: "creator", pointers: null },
{ id: "id", timeCreated: 123, host: "host", creator: "creator", pointers: [] },
{ id: "id", timeCreated: 123, host: "host", creator: "creator", pointers: "pointers" },
{ id: "id", timeCreated: 123, host: "host", creator: "creator", pointers: 123 }
];
invalidDeltas.forEach(delta => {
expect(() => validateDeltaNetworkImageV2(delta)).toThrow(InvalidDeltaFormatError);
});
});
it("should throw error for empty pointers object", () => {
const delta = { id: "id", timeCreated: 123, host: "host", creator: "creator", pointers: {} };
expect(() => validateDeltaNetworkImageV2(delta)).toThrow(InvalidDeltaFormatError);
});
it("should throw error for invalid pointer keys", () => {
const invalidPointers = [
{ "": "value" }
];
invalidPointers.forEach(pointers => {
const delta = { id: "id", timeCreated: 123, host: "host", creator: "creator", pointers };
expect(() => validateDeltaNetworkImageV2(delta)).toThrow(InvalidPointerError);
});
});
it("should throw error for invalid pointer values", () => {
const invalidPointers = [
{ key: undefined },
{ key: [] },
{ key: true },
{ key: false }
];
invalidPointers.forEach(pointers => {
const delta = { id: "id", timeCreated: 123, host: "host", creator: "creator", pointers };
expect(() => validateDeltaNetworkImageV2(delta)).toThrow(InvalidPointerError);
});
});
it("should throw error for invalid reference format", () => {
const invalidReferences = [
{ key: {} }, // Empty reference
{ key: { ref1: "val1", ref2: "val2" } }, // Multiple keys
{ key: { "": "value" } }, // Empty key
{ key: { ref: "" } }, // Empty value
{ key: { ref: 123 } }, // Non-string value
{ key: { ref: null } } // Null value
];
invalidReferences.forEach(pointers => {
const delta = { id: "id", timeCreated: 123, host: "host", creator: "creator", pointers };
expect(() => validateDeltaNetworkImageV2(delta)).toThrow(InvalidPointerError);
});
});
});
});
describe("Missing Required Fields", () => {
describe("DeltaV1", () => {
it("should throw MissingRequiredFieldError for missing id", () => {
const delta = { timeCreated: 123, host: "host", creator: "creator", pointers: [] };
expect(() => validateDeltaNetworkImageV1(delta)).toThrow(MissingRequiredFieldError);
expect(() => validateDeltaNetworkImageV1(delta)).toThrow(/id/);
});
it("should throw MissingRequiredFieldError for missing timeCreated", () => {
const delta = { id: "id", host: "host", creator: "creator", pointers: [] };
expect(() => validateDeltaNetworkImageV1(delta)).toThrow(MissingRequiredFieldError);
expect(() => validateDeltaNetworkImageV1(delta)).toThrow(/timeCreated/);
});
it("should throw MissingRequiredFieldError for missing host", () => {
const delta = { id: "id", timeCreated: 123, creator: "creator", pointers: [] };
expect(() => validateDeltaNetworkImageV1(delta)).toThrow(MissingRequiredFieldError);
expect(() => validateDeltaNetworkImageV1(delta)).toThrow(/host/);
});
it("should throw MissingRequiredFieldError for missing creator", () => {
const delta = { id: "id", timeCreated: 123, host: "host", pointers: [] };
expect(() => validateDeltaNetworkImageV1(delta)).toThrow(MissingRequiredFieldError);
expect(() => validateDeltaNetworkImageV1(delta)).toThrow(/creator/);
});
it("should throw MissingRequiredFieldError for missing pointers", () => {
const delta = { id: "id", timeCreated: 123, host: "host", creator: "creator" };
expect(() => validateDeltaNetworkImageV1(delta)).toThrow(MissingRequiredFieldError);
expect(() => validateDeltaNetworkImageV1(delta)).toThrow(/pointers/);
});
});
describe("DeltaV2", () => {
it("should throw MissingRequiredFieldError for all missing fields", () => {
const requiredFields = ["id", "timeCreated", "host", "creator", "pointers"];
requiredFields.forEach(field => {
const delta: Record<string, unknown> = {
id: "id",
timeCreated: 123,
host: "host",
creator: "creator",
pointers: { key: "value" }
};
delete delta[field];
expect(() => validateDeltaNetworkImageV2(delta)).toThrow(MissingRequiredFieldError);
expect(() => validateDeltaNetworkImageV2(delta)).toThrow(new RegExp(field));
});
});
});
});
describe("Valid Delta Formats", () => {
it("should accept valid DeltaV1", () => {
const validDeltas = [
{
id: "uuid-123",
timeCreated: 123456789,
host: "host1",
creator: "creator1",
pointers: [{ localContext: "name", target: "Alice" }]
},
{
id: "uuid-456",
timeCreated: 987654321,
host: "host2",
creator: "creator2",
pointers: [
{ localContext: "name", target: "Bob" },
{ localContext: "age", target: 25 },
{ localContext: "active", target: null }
]
},
{
id: "uuid-789",
timeCreated: 111111111,
host: "host3",
creator: "creator3",
pointers: [{ localContext: "friend", target: "user123", targetContext: "friendOf" }]
}
];
validDeltas.forEach(delta => {
expect(() => validateDeltaNetworkImageV1(delta)).not.toThrow();
});
});
it("should accept valid DeltaV2", () => {
const validDeltas = [
{
id: "uuid-123",
timeCreated: 123456789,
host: "host1",
creator: "creator1",
pointers: { name: "Alice" }
},
{
id: "uuid-456",
timeCreated: 987654321,
host: "host2",
creator: "creator2",
pointers: {
name: "Bob",
age: 25,
active: null
}
},
{
id: "uuid-789",
timeCreated: 111111111,
host: "host3",
creator: "creator3",
pointers: { friend: { user123: "friendOf" } }
}
];
validDeltas.forEach(delta => {
expect(() => validateDeltaNetworkImageV2(delta)).not.toThrow();
});
});
});
describe("Delta class integration", () => {
it("should validate when creating DeltaV1 from network image", () => {
const invalidDelta = {
id: "id",
timeCreated: "not-a-number",
host: "host",
creator: "creator",
pointers: [{ localContext: "name", target: "value" }]
};
expect(() => DeltaV1.fromNetworkImage(invalidDelta as never)).toThrow(InvalidDeltaFormatError);
});
it("should validate when creating DeltaV2 from network image", () => {
const invalidDelta = {
id: "id",
timeCreated: 123,
host: "",
creator: "creator",
pointers: { name: "value" }
};
expect(() => DeltaV2.fromNetworkImage(invalidDelta as never)).toThrow(InvalidDeltaFormatError);
});
it("should accept valid network images", () => {
const validV1 = {
id: "uuid-123",
timeCreated: 123456789,
host: "host1",
creator: "creator1",
pointers: [{ localContext: "name", target: "Alice" }]
};
const validV2 = {
id: "uuid-456",
timeCreated: 987654321,
host: "host2",
creator: "creator2",
pointers: { name: "Bob" }
};
expect(() => DeltaV1.fromNetworkImage(validV1)).not.toThrow();
expect(() => DeltaV2.fromNetworkImage(validV2)).not.toThrow();
});
});
});

188
src/delta-validation.ts Normal file
View File

@ -0,0 +1,188 @@
import { DeltaID, PointerTarget, DeltaNetworkImageV1, DeltaNetworkImageV2, PointersV2 } from "./delta";
import { CreatorID, HostID, Timestamp } from "./types";
// Custom error types for delta operations
export class DeltaValidationError extends Error {
constructor(message: string, public field?: string) {
super(message);
this.name = "DeltaValidationError";
}
}
export class InvalidDeltaFormatError extends DeltaValidationError {
constructor(message: string, field?: string) {
super(message, field);
this.name = "InvalidDeltaFormatError";
}
}
export class MissingRequiredFieldError extends DeltaValidationError {
constructor(field: string) {
super(`Missing required field: ${field}`, field);
this.name = "MissingRequiredFieldError";
}
}
export class InvalidPointerError extends DeltaValidationError {
constructor(message: string, pointerIndex?: number) {
super(message, pointerIndex !== undefined ? `pointer[${pointerIndex}]` : undefined);
this.name = "InvalidPointerError";
}
}
// Validation functions
export function validateDeltaId(id: unknown): id is DeltaID {
if (typeof id !== "string" || id.length === 0) {
throw new InvalidDeltaFormatError("Delta ID must be a non-empty string", "id");
}
return true;
}
export function validateTimestamp(timestamp: unknown, field: string): timestamp is Timestamp {
if (typeof timestamp !== "number" || timestamp <= 0) {
throw new InvalidDeltaFormatError(`${field} must be a positive number`, field);
}
return true;
}
export function validateHostId(host: unknown): host is HostID {
if (typeof host !== "string" || host.length === 0) {
throw new InvalidDeltaFormatError("Host ID must be a non-empty string", "host");
}
return true;
}
export function validateCreatorId(creator: unknown): creator is CreatorID {
if (typeof creator !== "string" || creator.length === 0) {
throw new InvalidDeltaFormatError("Creator ID must be a non-empty string", "creator");
}
return true;
}
export function validatePointerTarget(target: unknown): target is PointerTarget {
if (target !== null && typeof target !== "string" && typeof target !== "number") {
throw new InvalidPointerError("Pointer target must be string, number, or null");
}
return true;
}
export function validatePointerV1(pointer: unknown, index: number): pointer is { localContext: string; target: PointerTarget; targetContext?: string } {
if (!pointer || typeof pointer !== "object" || Array.isArray(pointer)) {
throw new InvalidPointerError(`Pointer at index ${index} must be an object`, index);
}
const p = pointer as Record<string, unknown>;
if (typeof p.localContext !== "string" || p.localContext.length === 0) {
throw new InvalidPointerError(`Pointer at index ${index} must have a non-empty localContext`, index);
}
validatePointerTarget(p.target);
if (p.targetContext !== undefined &&
(typeof p.targetContext !== "string" || p.targetContext.length === 0)) {
throw new InvalidPointerError(`Pointer at index ${index} targetContext must be a non-empty string if present`, index);
}
// Validate pointer consistency: if targetContext exists, target must be a string (reference)
if (p.targetContext && typeof p.target !== "string") {
throw new InvalidPointerError(`Pointer at index ${index} with targetContext must have string target (reference)`, index);
}
return true;
}
export function validatePointersV1(pointers: unknown): pointers is Array<{ localContext: string; target: PointerTarget; targetContext?: string }> {
if (!Array.isArray(pointers)) {
throw new InvalidDeltaFormatError("Pointers must be an array", "pointers");
}
if (pointers.length === 0) {
throw new InvalidDeltaFormatError("Delta must have at least one pointer", "pointers");
}
(pointers as unknown[]).forEach((pointer, index) => validatePointerV1(pointer, index));
return true;
}
export function validatePointersV2(pointers: unknown): pointers is PointersV2 {
if (!pointers || typeof pointers !== "object" || Array.isArray(pointers)) {
throw new InvalidDeltaFormatError("Pointers must be an object", "pointers");
}
const keys = Object.keys(pointers);
if (keys.length === 0) {
throw new InvalidDeltaFormatError("Delta must have at least one pointer", "pointers");
}
for (const [key, value] of Object.entries(pointers)) {
if (key.length === 0) {
throw new InvalidPointerError("Pointer key must be a non-empty string");
}
if (value !== null && typeof value !== "string" && typeof value !== "number" && typeof value !== "object") {
throw new InvalidPointerError(`Invalid pointer value for key '${key}'`);
}
// If value is an object (Reference), validate it
if (value && typeof value === "object") {
const refKeys = Object.keys(value);
if (refKeys.length !== 1) {
throw new InvalidPointerError(`Reference pointer '${key}' must have exactly one key-value pair`);
}
const [refKey, refValue] = Object.entries(value)[0];
if (typeof refKey !== "string" || refKey.length === 0) {
throw new InvalidPointerError(`Reference key in pointer '${key}' must be a non-empty string`);
}
if (typeof refValue !== "string" || refValue.length === 0) {
throw new InvalidPointerError(`Reference value in pointer '${key}' must be a non-empty string`);
}
}
}
return true;
}
export function validateDeltaNetworkImageV1(delta: unknown): delta is DeltaNetworkImageV1 {
if (!delta || typeof delta !== "object" || Array.isArray(delta)) {
throw new InvalidDeltaFormatError("Delta must be an object");
}
// Check required fields
if (!("id" in delta)) throw new MissingRequiredFieldError("id");
if (!("timeCreated" in delta)) throw new MissingRequiredFieldError("timeCreated");
if (!("host" in delta)) throw new MissingRequiredFieldError("host");
if (!("creator" in delta)) throw new MissingRequiredFieldError("creator");
if (!("pointers" in delta)) throw new MissingRequiredFieldError("pointers");
// Validate field types
validateDeltaId(delta.id);
validateTimestamp(delta.timeCreated, "timeCreated");
validateHostId(delta.host);
validateCreatorId(delta.creator);
validatePointersV1(delta.pointers);
return true;
}
export function validateDeltaNetworkImageV2(delta: unknown): delta is DeltaNetworkImageV2 {
if (!delta || typeof delta !== "object" || Array.isArray(delta)) {
throw new InvalidDeltaFormatError("Delta must be an object");
}
// Check required fields
if (!("id" in delta)) throw new MissingRequiredFieldError("id");
if (!("timeCreated" in delta)) throw new MissingRequiredFieldError("timeCreated");
if (!("host" in delta)) throw new MissingRequiredFieldError("host");
if (!("creator" in delta)) throw new MissingRequiredFieldError("creator");
if (!("pointers" in delta)) throw new MissingRequiredFieldError("pointers");
// Validate field types
validateDeltaId(delta.id);
validateTimestamp(delta.timeCreated, "timeCreated");
validateHostId(delta.host);
validateCreatorId(delta.creator);
validatePointersV2(delta.pointers);
return true;
}

View File

@ -3,6 +3,7 @@ import Debug from 'debug';
import microtime from 'microtime';
import {PeerAddress} from "./peers";
import {CreatorID, DomainEntityID, HostID, PropertyID, Timestamp, TransactionID} from "./types";
import {validateDeltaNetworkImageV1, validateDeltaNetworkImageV2} from "./delta-validation";
const debug = Debug('rz:delta');
export type DeltaID = string;
@ -75,6 +76,7 @@ export class DeltaV1 extends DeltaNetworkImageV1 {
}
static fromNetworkImage(delta: DeltaNetworkImageV1) {
validateDeltaNetworkImageV1(delta);
return new DeltaV1(delta);
}
}
@ -98,6 +100,7 @@ export class DeltaV2 extends DeltaNetworkImageV2 {
}
static fromNetworkImage(delta: DeltaNetworkImageV2) {
validateDeltaNetworkImageV2(delta);
return new DeltaV2(delta);
}

12
todo.md
View File

@ -4,12 +4,12 @@ This document tracks work needed to achieve full specification compliance, organ
## Phase 1: Foundation (Prerequisites)
### 1.1 Delta Validation & Error Handling
- [ ] Implement delta structure validation
- [ ] Add tests for invalid delta formats
- [ ] Add tests for required fields (id, created, pointers)
- [ ] Implement proper error types for delta operations
- [ ] Add validation for pointer consistency
### 1.1 Delta Validation & Error Handling
- [x] Implement delta structure validation
- [x] Add tests for invalid delta formats
- [x] Add tests for required fields (id, created, pointers)
- [x] Implement proper error types for delta operations
- [x] Add validation for pointer consistency
### 1.2 Complete Transaction Support
- [ ] Implement transaction-based filtering in lossless views