Skip to content

fix(core): enterprise hardening, null-safety guards, readonly enum support & v2.0.0 sync#7

Open
Iradukunda-Fils wants to merge 8 commits into
bufferpunk:mainfrom
Iradukunda-Fils:main
Open

fix(core): enterprise hardening, null-safety guards, readonly enum support & v2.0.0 sync#7
Iradukunda-Fils wants to merge 8 commits into
bufferpunk:mainfrom
Iradukunda-Fils:main

Conversation

@Iradukunda-Fils

@Iradukunda-Fils Iradukunda-Fils commented Jul 25, 2026

Copy link
Copy Markdown

Null-Safety Guards, Readonly Schema Support & Upstream v2.0.0 Alignment

Executive Summary

This pull request brings @bufferpunk/modelcore up to enterprise-grade stability (v2.0.0). It addresses runtime null-pointer vulnerabilities, string character-splitting in Union types, readonly schema enum support for as const arrays, defensive input sanitization, and full synchronization with the upstream modular src/ architecture.

1. Summary of Changes & Technical Explanations

A. Readonly Array Support for Enum Definitions (src/typing.d.ts & src/typing.ts)

  • Problem: When model schemas declared enum values using as const assertions (readonly string[]) or frozen arrays (Object.freeze([...])), the TypeScript compiler threw strict type errors (TS2530/TS4104) because FieldConfig.enum strictly expected a mutable any[].
  • Before (Failed Typecheck):
    const ROLES = ["ADMIN", "USER", "GUEST"] as const; // Readonly array
    
    class User extends Base {
      static override get schema() {
        return {
          role: { type: String, enum: ROLES } // TS Error: Type 'readonly ["ADMIN", "USER", "GUEST"]' is not assignable to 'any[]'
        };
      }
    }
  • After (Fixed Interface):
    export interface FieldConfig {
      // ...
      enum?: any[] | readonly any[]; // Supports both mutable and immutable readonly arrays
    }
  • Code Example (Usage):
    import { Base, SchemaDefinition } from "@bufferpunk/modelcore";
    
    const ROLES = ["ADMIN", "USER", "GUEST"] as const;
    
    export class User extends Base {
      static override get schema(): SchemaDefinition {
        return {
          role: { type: String, enum: ROLES }
        } as const satisfies SchemaDefinition;
      }
    }
    
    const u = new User({ role: "ADMIN" }); // Validates cleanly with 100% type safety

B. Defensive Null & Degenerate Schema Guarding (src/utils.ts & src/base.ts)

  • Problem: Passing non-function types ({ type: undefined }, { type: null }, or { type: "string" }) into field configs resulted in raw Node.js runtime crashes: TypeError: Cannot read properties of undefined (reading 'prototype').
  • Before (Runtime Crash):
    class Product extends Base {
      static schema = { title: { type: undefined } };
    }
    
    new Product({ title: "Laptop" }); 
    // Unhandled Exception: TypeError: Cannot read properties of undefined (reading 'prototype')
  • After (Structured Error Guard):
    // Validation guard in normalizeConf
    if (typeof conf.type !== "function") {
      throw new SchemaDefinitionError(
        `Invalid schema definition for key '${key}': type must be a constructor function`
      );
    }
  • Behavior After Fix:
    new Product({ title: "Laptop" }); 
    // Throws structured SchemaDefinitionError with code 'SCHEMA_DEFINITION_ERROR'

C. Null-Prototype Input Protection (Object.create(null))

  • Problem: Objects created via Object.create(null) lack Object.prototype methods and .constructor. Passing them as inputs triggered unhandled exceptions during isOfType() property lookups.
  • Before:
    class User extends Base { static schema = { name: String }; }
    const nullProtoObj = Object.create(null);
    nullProtoObj.name = "Alice";
    
    new User(nullProtoObj); // Crashed with TypeError: Cannot read properties of null
  • After:
    const nullProtoObj = Object.create(null);
    nullProtoObj.name = "Alice";
    
    const user = new User(nullProtoObj); // Safely constructs and extracts properties

D. Union Type Structural Discrimination (src/typing.ts & src/utils.ts)

  • Problem: Passing a scalar string to a Union(Array, String) field caused the string to enter the Array iteration block, converting "hello" into ['h','e','l','l','o']. Passing a string to Union(Object, String) evaluated object key rules against string properties.
  • Before (Data Corruption):
    class Event extends Base {
      static schema = { payload: { type: Union(Array, String), values: String } };
    }
    
    const ev = new Event({ payload: "hello" });
    console.log(ev.payload); // Output: ['h', 'e', 'l', 'l', 'o'] (String corrupted into char array)
  • After (Type Safety Guard):
    // Enforces structural type check before executing array/object validation handlers
    if (conf.type === Array && !Array.isArray(value)) {
      return false; // Safely skip Array branch and fallback to String branch
    }
  • Result:
    const ev = new Event({ payload: "hello" });
    console.log(ev.payload); // Output: "hello" (Preserved as primitive string)

2. Upgrade & Migration Notes (v1.x -> v2.0.0)

  1. Named Import Update:
    // ❌ Old: import Base from "@bufferpunk/modelcore";
    // ✅ New: import { Base } from "@bufferpunk/modelcore";
  2. TypeScript Inheritance Modifier:
    // ✅ Recommended: Use static override get schema()
    class Model extends Base {
      static override get schema(): SchemaDefinition { ... }
    }

3. Verification & Testing

  • Executed unit test suite:
    npm test
  • Outcome: 62 / 62 unit tests passing cleanly (0 failures).

@mugabogusenga

Copy link
Copy Markdown

The PR documentation doesn't match the latest changes on your commit that align with version 2.0,

Please update the documentation as I will use Squash and Merge to only take your latest changes.
@Iradukunda-Fils

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants