Skip to content
Open
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
node_modules
pull_request.md
PULL_REQUEST.md
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file.
### Added

- **Extended test coverage**: 8 new tests (60 total) — `{ type: undefined/null/"string" }` schema rejection, `Union(Array, String)` value discrimination, scalar-only coerce tests, and `Union(Object, String)` skipping object-key validation for string values.
- **Readonly enum array support**: Updated `FieldConfig.enum` to allow `readonly any[]` for `as const` schema declarations.

### Changed

Expand Down
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -475,20 +475,35 @@ ModelCore throws typed error classes for every failure mode:
| Error class | Trigger |
|---|---|
| `ValidationError` | Custom `validate` hook throws |
| `TypeValidationError` | Value constructor doesn't match schema type |
| `TypeValidationError` | Value constructor doesn't match schema type, or value has no prototype chain |
| `RequiredError` | Required field is missing |
| `EnumValueError` | Value not in allowed enum set |
| `RangeError` | Value exceeds min/max |
| `ImmutableObjectError` | Write to immutable class |
| `ImmutablePropertyError` | Write to immutable field |
| `SchemaDefinitionError` | Malformed schema definition |
| `SchemaDefinitionError` | Malformed schema definition, `{ type: undefined }`, `{ type: null }`, or missing `static schema` |
| `MissingPropertyError` | Nested required property missing |
| `ValueError` | Array method misuse (e.g., `fill()`) |

All errors extend `ModelCoreError`, which carries `source`, `path`, `expected`, `received`, and `code` properties for programmatic handling.

---

## Defensive guarantees

ModelCore guards against several degenerate inputs that would otherwise cause raw `TypeError` crashes or silent data corruption:

| Input | Before | After |
|---|---|---|
| `{ type: undefined }` in schema | Raw `TypeError: Cannot read properties of undefined` | `SchemaDefinitionError` with path and code |
| `{ type: null }` in schema | Raw `TypeError` | `SchemaDefinitionError` with path and code |
| `Object.create(null)` as field value | Raw `TypeError: Cannot read properties of undefined (reading 'name')` | `TypeValidationError` with message `"...got null-prototype object"` |
| `Union(Array, String)` with string input | String silently split into char array `['h','e','l','l','o']` | String preserved as-is |
| `"42"` for a `Number` field (no `coerce: true`) | Silently accepted as string | `TypeValidationError` — use `coerce: true` to auto-convert |
| Subclass with no `static schema` | Silent empty object | `SchemaDefinitionError` with class name |



## Performance

ModelCore achieves **>200K ops/sec** on full model construction with nested containers (5 fields, 100K iterations, Node 24). Scalar-only models reach **~1.9M ops/sec**.
Expand Down
2 changes: 1 addition & 1 deletion src/typing.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export interface FieldConfig {
optional?: boolean;
required?: boolean;
default?: any;
enum?: any[];
enum?: any[] | readonly any[];
max?: number;
min?: number;
beforeChecks?: (value: any) => any;
Expand Down
29 changes: 29 additions & 0 deletions test/base.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -837,6 +837,8 @@ test("error.expected contains meaningful values (type, min, max, enum)", () => {
}
});

// ==================== NULL & EDGE-CASE GUARDS ====================

test("schema field with { type: undefined } throws SchemaDefinitionError", () => {
class U extends Base {
static schema = { name: { type: undefined } };
Expand Down Expand Up @@ -950,3 +952,30 @@ test("Union(Object, String) with string value preserves string (does not run Obj
assert.equal(typeof u.val, "string");
assert.equal(u.val, "hello");
});

test("supports readonly enum arrays (as const assertions)", () => {
const RO_ROLES = Object.freeze(["ADMIN", "USER", "GUEST"]);
class U extends Base {
static schema = {
role: { type: String, enum: RO_ROLES }
};
}

const u = new U({ role: "ADMIN" });
assert.equal(u.role, "ADMIN");
assert.throws(() => new U({ role: "INVALID" }), /Invalid value for 'role'/);
});

test("handles null-prototype inputs gracefully", () => {
class U extends Base {
static schema = {
name: { type: String }
};
}

const nullProtoObj = Object.create(null);
nullProtoObj.name = "Alice";

const u = new U(nullProtoObj);
assert.equal(u.name, "Alice");
});