@graftjs/core
v0.7.0
Published
Type-safe bidirectional schema mapping - core engine
Maintainers
Readme
@graftjs/core
Type-safe bidirectional schema mapping — core engine
The foundation of Graft: a compile-time type-safe, runtime-validated library for mapping between two object schemas. @graftjs/core provides the low-level primitives that power Graft's schema mapping capabilities.
Overview
@graftjs/core is a dependency-free package that provides:
- Type-level utilities for compile-time safety (no
number → stringwiring allowed) - Runtime mapping engine with precompiled specs for fast execution
- Security guards to prevent prototype pollution
- Collision detection to catch configuration mistakes
- Bidirectional mapping from a single configuration
Most users will want @graftjs/zod instead, which provides automatic schema introspection and validation. Use @graftjs/core directly only if you're:
- Building an adapter for another schema library (Valibot, TypeBox, etc.)
- Need maximum control over the mapping process
- Working without a schema validation library
Installation
npm install @graftjs/core
# or
pnpm add @graftjs/core
# or
yarn add @graftjs/coreQuick Example
import { compileSpec, executeForward, executeBackward } from '@graftjs/core';
// Define your mapping configuration
const config = {
userId: 'user_id',
email: 'email_address',
createdAt: 'created_at'
};
// Compile once (at app startup)
const spec = compileSpec(config);
// Execute many times (hot path)
const apiUser = executeForward(dbUser, spec);
// { userId: '123', email: '[email protected]', createdAt: '2024-01-01' }
const dbUser = executeBackward(apiUser, spec);
// { user_id: '123', email_address: '[email protected]', created_at: '2024-01-01' }Core Concepts
Deep Paths (v0.2)
Graft supports nested object mapping using dot notation. You can map flat fields to nested structures and vice versa:
// ✅ Map flat source to nested target
const config = {
'profile.name': 'user_name',
'profile.address.city': 'city',
'profile.address.country': 'country_code'
};
// ✅ Map nested source to flat target
const config = {
userName: 'user.profile.name',
userCity: 'user.profile.address.city'
};
// ✅ Mix flat and nested paths
const config = {
id: 'user_id',
'profile.name': 'user_name'
};Intermediate objects are automatically created with null prototypes for security.
Precompiled Specs
Graft separates compilation (expensive, done once) from execution (cheap, done many times):
// Compile once
const spec = compileSpec(config, {
strict: 'to',
validate: 'both'
});
// Execute many times in hot path
for (const dbRecord of thousands) {
const apiRecord = executeForward(dbRecord, spec);
// Fast! No repeated parsing or validation
}Bidirectional Mapping
One configuration gives you both directions:
const spec = compileSpec({ userId: 'user_id' });
// Forward: Left → Right
const right = executeForward({ user_id: '123' }, spec);
// { userId: '123' }
// Backward: Right → Left
const left = executeBackward({ userId: '123' }, spec);
// { user_id: '123' }The backward mapping is automatically inverted from the forward mapping.
Security First
Prototype pollution prevention is built-in:
// ❌ Throws GraftSecurityError immediately
const spec = compileSpec({
'__proto__': 'evil'
});
// ❌ Also throws
const spec = compileSpec({
normal: '__proto__'
});All output objects use Object.create(null) to prevent pollution attacks.
API Reference
Type Utilities
MappingConfig<From, To>
Type for mapping configuration objects. Ensures type compatibility at compile time.
type UserDB = {
user_id: string;
email_address: string;
age: number;
};
type UserAPI = {
id: string;
email: string;
age: number;
};
// ✅ Valid - types are compatible
const config: MappingConfig<UserDB, UserAPI> = {
id: 'user_id', // string → string ✓
email: 'email_address', // string → string ✓
age: 'age' // number → number ✓
};
// ❌ Invalid - would fail type check
const bad: MappingConfig<UserDB, UserAPI> = {
id: 'age' // number → string ✗
};AssertAllToKeysMapped<From, To, Config>
Enforces that all required target fields are mapped (strict mode).
type From = { a: string; b: number };
type To = { x: string; y: number; z: boolean };
// ❌ Type error - missing mapping for 'z'
type Bad = AssertAllToKeysMapped<From, To, { x: 'a', y: 'b' }>;
// Shows: { __error: "Missing required mappings"; __missing: "z" }
// ✅ Valid - all required fields mapped
type Good = AssertAllToKeysMapped<From, To, { x: 'a', y: 'b', z: 'a' }>;GraftOptions
Configuration options for compilation and execution:
type GraftOptions = {
strict?: 'none' | 'to' | 'from' | 'both'; // Default: 'to'
validate?: 'none' | 'from' | 'to' | 'both'; // Not used in core (for adapters)
};- strict: Enforce complete field coverage
'to': All required target fields must be mapped'from': All required source fields must be reproducible'both': Both directions enforced'none': No enforcement (permissive)
Runtime Functions
compileSpec<From, To>(config, options?)
Compile a mapping configuration into an optimized specification.
Parameters:
config: MappingConfig<From, To>- Mapping configurationoptions?: GraftOptions- Optional configuration
Returns: CompiledSpec - Frozen, immutable spec ready for execution
Throws:
GraftSecurityError- If dangerous keys detected (__proto__, etc.)GraftCollisionError- If multiple sources map to same target
const spec = compileSpec(
{
userId: 'user_id',
email: 'email_address'
},
{
strict: 'to'
}
);executeForward<To>(input, spec)
Execute forward mapping (From → To).
Parameters:
input: Record<string, unknown>- Source objectspec: CompiledSpec- Compiled specification
Returns: To - Mapped target object
Throws:
GraftMappingError- If mapping fails
const output = executeForward({ user_id: '123', email_address: '[email protected]' }, spec);
// { userId: '123', email: '[email protected]' }executeBackward<From>(input, spec)
Execute backward mapping (To → From).
Parameters:
input: Record<string, unknown>- Target objectspec: CompiledSpec- Compiled specification
Returns: From - Mapped source object
Throws:
GraftMappingError- If mapping fails
const output = executeBackward({ userId: '123', email: '[email protected]' }, spec);
// { user_id: '123', email_address: '[email protected]' }Security Functions
validateConfigKeys(config)
Validate configuration keys for security issues.
Throws: GraftSecurityError if dangerous keys detected
validateConfigKeys({ userId: 'user_id' }); // ✓ Safe
validateConfigKeys({ '__proto__': 'evil' }); // ✗ ThrowscreateSafeObject()
Create a safe object with null prototype (prevents pollution).
const obj = createSafeObject();
obj['__proto__'] = { polluted: true };
console.log({}.polluted); // undefined - no pollution!safeGet(obj, key) / safeSet(obj, key, value)
Safe property access with runtime guards.
safeGet(obj, 'name'); // ✓ Safe
safeGet(obj, '__proto__'); // ✗ Throws GraftSecurityErrorNested Grafts (v0.5)
GRAFT_BRAND
Symbol used to identify Graft instances for nested composition.
import { GRAFT_BRAND } from '@graftjs/core';
// Check if an object is a Graft instance
function isGraft(obj: unknown): boolean {
return typeof obj === 'object' &&
obj !== null &&
GRAFT_BRAND in obj;
}GraftLike
Minimal interface for nested graft detection:
type GraftLike = {
readonly [GRAFT_BRAND]: true;
readonly mapRight: (input: unknown) => unknown;
readonly mapLeft: (input: unknown) => unknown;
};NestedGraftConfig
Configuration for nested graft composition:
type NestedGraftConfig = {
readonly from: string; // Source path
readonly graft: GraftLike; // Nested graft to apply
};Usage in config:
const spec = compileSpec({
id: 'user_id',
address: {
from: 'address_data', // Source path
graft: addressGraft // Any object with GRAFT_BRAND
}
});Path Utilities (v0.2)
parsePath(path)
Parse a dot-notation path into segments.
parsePath("profile.address.city"); // ["profile", "address", "city"]
parsePath("id"); // ["id"]isNestedPath(path)
Check if a path contains nested segments.
isNestedPath("profile.address.city"); // true
isNestedPath("id"); // falsegetNestedValue(obj, path) / setNestedValue(obj, path, value)
Safely traverse nested objects. Throws GraftSecurityError if any path segment is dangerous.
const obj = { profile: { address: { city: "NYC" } } };
getNestedValue(obj, "profile.address.city"); // "NYC"
getNestedValue(obj, "profile.missing"); // undefined
setNestedValue(obj, "profile.address.zip", "10001");
// Creates intermediate objects as neededDeepKeys<T, Depth>
Generate a union type of all valid paths into an object type.
import type { DeepKeys, DeepValue } from '@graftjs/core';
type User = {
id: string;
profile: {
name: string;
address: { city: string };
};
};
type UserPaths = DeepKeys<User>;
// "id" | "profile" | "profile.name" | "profile.address" | "profile.address.city"
type CityType = DeepValue<User, "profile.address.city">; // stringError Classes
All errors extend GraftError for easy catching:
try {
const spec = compileSpec(config);
} catch (error) {
if (error instanceof GraftError) {
// Handle graft-specific errors
}
}GraftCollisionError
Multiple sources mapping to same target.
// Properties
error.targetKey: string; // The conflicting target key
error.sourceKeys: string[]; // Conflicting source keysGraftSecurityError
Dangerous key detected (prototype pollution attempt).
// Properties
error.dangerousKey: string; // The dangerous key that was blockedGraftConfigError
Invalid mapping configuration.
GraftMappingError
Runtime mapping failure.
// Properties
error.direction: 'forward' | 'backward'; // Which direction failedGraftNestedError
Nested graft execution failure.
// Properties
error.mapping: { from: string; to: string }; // The mapping that failed
error.direction: 'forward' | 'backward'; // Which direction failed
error.cause: Error; // The underlying errorExample handling:
try {
const result = executeForward(input, spec);
} catch (error) {
if (error instanceof GraftNestedError) {
console.error(`Nested graft failed: ${error.mapping.from} → ${error.mapping.to}`);
console.error(`Direction: ${error.direction}`);
console.error(`Cause: ${error.cause.message}`);
}
}Advanced Usage
Building a Custom Adapter
Here's how to build an adapter for another schema library:
import { compileSpec, executeForward, executeBackward } from '@graftjs/core';
import type { MappingConfig, GraftOptions } from '@graftjs/core';
// Example: Valibot adapter
function createGraft<Left, Right>(
leftSchema: ValibotSchema,
rightSchema: ValibotSchema,
config: MappingConfig<Left, Right>,
options?: GraftOptions
) {
// 1. Introspect schemas to generate auto-mappings
const autoMappings = generateAutoMappings(leftSchema, rightSchema);
// 2. Merge with manual config
const mergedConfig = { ...autoMappings, ...config };
// 3. Compile the spec
const spec = compileSpec<Left, Right>(mergedConfig, options);
// 4. Return methods that wrap with validation
return {
toRight(input: unknown): Right {
const parsed = parse(leftSchema, input);
const mapped = executeForward<Right>(parsed, spec);
return parse(rightSchema, mapped);
},
toLeft(input: unknown): Left {
const parsed = parse(rightSchema, input);
const mapped = executeBackward<Left>(parsed, spec);
return parse(leftSchema, mapped);
},
mapRight(input: Left): Right {
const mapped = executeForward<Right>(input, spec);
return parse(rightSchema, mapped);
},
mapLeft(input: Right): Left {
const mapped = executeBackward<Left>(input, spec);
return parse(leftSchema, mapped);
},
spec
};
}Performance Optimization
Do:
- ✅ Compile specs once at app startup
- ✅ Reuse specs across many executions
- ✅ Use
executeForward/executeBackwarddirectly for hot paths
Don't:
- ❌ Recompile specs for every execution
- ❌ Create new configs in loops
- ❌ Use spread operators in hot paths
// ✅ Good - compile once
const spec = compileSpec(config);
for (const item of thousands) {
const mapped = executeForward(item, spec);
}
// ❌ Bad - recompiling on every iteration
for (const item of thousands) {
const spec = compileSpec(config); // Wasteful!
const mapped = executeForward(item, spec);
}Type-Level Features
Path Type Generation
@graftjs/core provides type utilities for generating valid paths into objects:
import type { TopLevelKeys, RequiredKeys, OptionalKeys } from '@graftjs/core';
type User = {
id: string;
name?: string;
age: number | undefined;
};
type Top = TopLevelKeys<User>; // "id" | "name" | "age"
type Req = RequiredKeys<User>; // "id"
type Opt = OptionalKeys<User>; // "name" | "age"Compatibility Checking
The type system ensures source and target types are compatible:
import type { CompatibleSourceKey } from '@graftjs/core';
type From = { id: string; age: number; name: string };
type To = { userId: string; userAge: number };
// "id" is compatible with "userId" (both string)
type CanMapId = CompatibleSourceKey<From, To, "userId">; // "id"
// "age" is NOT compatible with "userId" (number vs string)
type CannotMap = CompatibleSourceKey<From, To, "userId">; // neverCurrent Capabilities (v0.5)
Supported features:
- ✅ Deep path mapping with dot notation
- ✅ Transforms with forward/backward functions
- ✅ Nested graft composition via
GRAFT_BRAND - ✅ Error handling with
GraftNestedError
Planned features:
- Array element mapping
- Union type support
- Deep auto-mapping
See the roadmap for upcoming features.
TypeScript Configuration
For best results, enable strict mode in your tsconfig.json:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true
}
}License
MIT
See Also
@graftjs/zod- High-level Zod integration (recommended for most users)- Main Documentation - Overview and roadmap
