Socket
Socket
Sign inDemoInstall

object-reshaper

Package Overview
Dependencies
0
Maintainers
1
Versions
3
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 0.1.0 to 0.2.0

src/tests/invalid.spec.ts

20

CHANGELOG.md
# Changelog
## [0.2.0](https://github.com/dextertanyj/object-reshaper/compare/v0.1.0...v0.2.0) (2023-04-26)
### ⚠ BREAKING CHANGES
* implement support for undefined and null inputs
* introduce type support for undefined and null properties
### Features
* implement support for undefined and null inputs ([20f3a67](https://github.com/dextertanyj/object-reshaper/commit/20f3a671d27c94ce1f6636e0de790ad0a0f71f77))
* introduce type support for undefined and null properties ([feb9e70](https://github.com/dextertanyj/object-reshaper/commit/feb9e70791492a0b596ba3f21aaa60ec4f3fc96a))
### Bug Fixes
* add result type inference for nested schemas ([9999bfa](https://github.com/dextertanyj/object-reshaper/commit/9999bfa5cf0fe6c4789e024d02d3bfbd5cf2c81d))
* add types for nested entities in mapped arrays ([52ed5f1](https://github.com/dextertanyj/object-reshaper/commit/52ed5f1dcd65994fadf22f45e90f2bc362f1699f))
* union array index access return type with undefined ([5906274](https://github.com/dextertanyj/object-reshaper/commit/59062741864fda333991af9d32ff72155e41210e))
## 0.1.0 (2023-04-25)

@@ -4,0 +24,0 @@

2

lib/cjs/reshape.d.ts
import { Reshaper, Schema } from "./types";
export declare const reshaperBuilder: <T extends object, S extends Schema<T>>(schema: S) => Reshaper<T, S>;
export declare const reshaperBuilder: <T, S extends Schema<T>>(schema: S) => Reshaper<T, S>;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.reshaperBuilder = void 0;
const errors_1 = require("./errors");
const fieldAccessorImplementation = (o, field) => {
if (typeof o !== "object" || o === null) {
throw new errors_1.ReshapeError("FieldNotObject");
const readField = (o, field) => {
if (typeof o !== "object") {
return undefined;
}
if (o === null) {
return o;
}
if (!Object.prototype.hasOwnProperty.call(o, field)) {
throw new errors_1.ReshapeError("MissingField");
return undefined;
}
return o[field];
};
const handleArrayAccess = (field, fieldName, path) => {
const arrayAccessor = fieldName.split("[");
const arrayName = arrayAccessor[0];
const arrayIndex = arrayAccessor[1].split("]")[0];
const array = readField(field, arrayName);
if (array === undefined || array === null) {
return array;
}
if (!Array.isArray(array)) {
return undefined;
}
if (arrayIndex === "*") {
const result = handleFlattenedArrayAccess(array, [...path]);
if (path.find((item) => item.endsWith("[*]"))) {
return result.flatMap((item) => item === undefined || item === null ? [] : item);
}
return result;
}
else {
return handleIndexedArrayAccess(array, parseInt(arrayIndex), [...path]);
}
};
const handleIndexedArrayAccess = (array, index, path) => {
if (array.length <= index) {
return undefined;
}
const result = fieldAccessor(array[index], [...path]);
return result;
};
const handleFlattenedArrayAccess = (array, path) => {
return array
.filter((item) => item !== undefined && item !== null)
.map((item) => fieldAccessor(item, [...path]));
};
const fieldAccessor = (data, path) => {
let field = data;
for (let fieldName = path.shift(); fieldName !== undefined; fieldName = path.shift()) {
for (let fieldName = path.shift(); fieldName !== undefined && field !== undefined; fieldName = path.shift()) {
if (fieldName.endsWith("]")) {
const arrayAccessor = fieldName.split("[");
const arrayName = arrayAccessor[0];
const arrayIndex = arrayAccessor[1].split("]")[0];
const array = fieldAccessorImplementation(field, arrayName);
if (!Array.isArray(array)) {
throw new errors_1.ReshapeError("FieldNotArray");
}
if (arrayIndex === "*") {
const result = array.map((item) => fieldAccessor(item, [...path]));
if (path.find((item) => item.endsWith("[*]"))) {
return result.flatMap((item) => item);
}
return result;
}
else {
if (array.length < parseInt(arrayIndex)) {
throw new errors_1.ReshapeError("ArrayIndexOutOfBounds");
}
return fieldAccessor(array[parseInt(arrayIndex)], path);
}
return handleArrayAccess(field, fieldName, [...path]);
}
else {
field = fieldAccessorImplementation(field, fieldName);
field = readField(field, fieldName);
}

@@ -60,10 +77,12 @@ }

if (!Array.isArray(array)) {
throw new errors_1.ReshapeError("FieldNotArray");
result[key] = undefined;
}
result[key] = array.map((item) => {
if (typeof item !== "object") {
throw new errors_1.ReshapeError("FieldNotObject");
}
return objectConstructor(item, subSchema);
});
else {
result[key] = array.map((item) => {
if (typeof item !== "object") {
return undefined;
}
return objectConstructor(item, subSchema);
});
}
}

@@ -70,0 +89,0 @@ }

type IsAny<T> = unknown extends T ? keyof T extends never ? false : true : false;
type ExcludeArrayKeys<T> = T extends ArrayLike<unknown> ? Exclude<keyof T, keyof unknown[]> : keyof T;
type ArrayProperty<T, Key extends keyof T & string> = T[Key] extends unknown[] ? T[Key][number] extends Record<string, unknown> ? `${Key}[${number | "*"}].${PathElement<T[Key][number], ExcludeArrayKeys<T[Key][number]>> & string}` | `${Key}[${number | "*"}].${ExcludeArrayKeys<T[Key][number]> & string}` | `${Key}[${number | "*"}]` | `${Key}` : `${Key}[${number | "*"}]` | `${Key}` : never;
type RecordProperty<T, Key extends keyof T & string> = T[Key] extends Record<string, unknown> ? `${Key}.${PathElement<T[Key], ExcludeArrayKeys<T[Key]>> & string}` | `${Key}.${ExcludeArrayKeys<T[Key]> & string}` : never;
type ConcreteArrayElement<A extends unknown[] | undefined | null> = Exclude<Exclude<A, undefined | null>[number], undefined | null>;
type ArrayProperty<T, Key extends keyof T & string> = T[Key] extends unknown[] | undefined | null ? Exclude<T[Key], undefined | null>[number] extends Record<string, unknown> | undefined | null ? `${Key}[${number | "*"}].${PathElement<ConcreteArrayElement<T[Key]>, ExcludeArrayKeys<ConcreteArrayElement<T[Key]>>> & string}` | `${Key}[${number | "*"}].${ExcludeArrayKeys<ConcreteArrayElement<T[Key]>> & string}` | `${Key}[${number | "*"}]` | `${Key}` : `${Key}[${number | "*"}]` | `${Key}` : never;
type RecordProperty<T, Key extends keyof T & string> = T[Key] extends Record<string, unknown> | undefined | null ? `${Key}.${PathElement<Exclude<T[Key], undefined | null>, ExcludeArrayKeys<T[Key]>> & string}` | `${Key}.${ExcludeArrayKeys<Exclude<T[Key], undefined | null>> & string}` : never;
type PathElement<T, Key extends keyof T> = Key extends string ? IsAny<T[Key]> extends true ? never : ArrayProperty<T, Key> | RecordProperty<T, Key> : never;

@@ -9,15 +10,21 @@ type Path<T> = keyof T extends string ? PathElement<T, keyof T> | keyof T extends infer P ? P extends string | keyof T ? P : keyof T : keyof T : never;

type ArrayChildren<Key extends ArrayTerminal<P>, P> = Key extends `${infer K}[*]` ? P extends `${infer Left}[*].${infer Right}` ? Left extends K ? Right : never : never : never;
type NestedSchema<Path> = Readonly<{
[key: string]: Path | NestedSchema<Path> | SubArraySchema<ArrayTerminal<Path>, Path>;
}>;
type SubArrayDefinition<Key extends ArrayTerminal<P>, P> = {
readonly 0: Key;
readonly 1: Record<string, ArrayChildren<Key, P> | SubArraySchema<ArrayTerminal<ArrayChildren<Key, P>>, ArrayChildren<Key, P>>>;
readonly 1: Record<string, ArrayChildren<Key, P> | NestedSchema<ArrayChildren<Key, P>> | SubArraySchema<ArrayTerminal<ArrayChildren<Key, P>>, ArrayChildren<Key, P>>>;
};
type SubArraySchema<Key extends ArrayTerminal<P>, P> = SubArrayDefinition<Key, P>;
export type Schema<T> = Readonly<{
[key: string]: Path<T> | Schema<T> | SubArraySchema<ArrayTerminal<Path<T>>, Path<T>>;
}>;
type GetFieldType<T, P> = P extends keyof T ? T[P] : P extends `${infer U}[${infer V extends number | "*"}].${infer W}` ? GetFieldType<T, U> extends infer X ? X extends unknown[] ? V extends number ? GetFieldType<X[number], W> : GetFieldType<X[number], W> extends infer Y ? W extends `${string}[*]${string | ""}` ? Y : Y[] : never : never : never : P extends `${infer U}.${infer V}` ? U extends keyof T ? T[U] extends Record<string, unknown> ? GetFieldType<T[U], V> : never : never : P extends `${infer U}[${infer V extends number | "*"}]` ? U extends keyof T ? T[U] extends unknown[] ? V extends number ? T[U][number] : T[U] : never : never : never;
export type Schema<T> = NestedSchema<Path<T>>;
type UndefinedNullWrapper<C, R> = [undefined | null] extends [C] ? R | undefined | null : [undefined] extends [C] ? R | undefined : [null] extends [C] ? R | null : R;
type CheckAndGetSubFieldType<T, U extends keyof T, V> = T[U] extends Record<string, unknown> | undefined | null ? UndefinedNullWrapper<T[U], GetFieldType<Exclude<T[U], undefined | null>, V>> : never;
type CheckAndGetArraySubFieldType<A, V extends number | "*", P> = A extends unknown[] | undefined | null ? Exclude<A, undefined | null>[number] extends infer O ? GetFieldType<Exclude<O, undefined | null>, P> extends infer E ? V extends number ? UndefinedNullWrapper<O, E> | undefined : P extends `${string}[*]${string | ""}` ? UndefinedNullWrapper<A, E> : UndefinedNullWrapper<A, E[]> : never : never : never;
type CheckAndGetArrayFieldType<T, U extends keyof T, V extends number | "*"> = T[U] extends unknown[] | undefined | null ? V extends number ? UndefinedNullWrapper<T[U], Exclude<T[U], undefined | null>[number]> | undefined : UndefinedNullWrapper<T[U], Exclude<Exclude<T[U], undefined | null>[number], undefined | null>[]> : never;
type GetFieldType<T, P> = P extends keyof T ? T[P] : P extends `${infer U}[${infer V extends number | "*"}].${infer W}` ? GetFieldType<T, U> extends infer X ? CheckAndGetArraySubFieldType<X, V, W> : never : P extends `${infer U}.${infer V}` ? U extends keyof T ? CheckAndGetSubFieldType<T, U, V> : never : P extends `${infer U}[${infer V extends number | "*"}]` ? U extends keyof T ? CheckAndGetArrayFieldType<T, U, V> : never : T;
type SubArrayTransformed<T, P extends string, V> = GetFieldType<T, P> extends infer W ? W extends unknown[] | undefined | null ? Exclude<W, undefined | null>[number] extends infer O ? Exclude<O, undefined | null> extends infer E ? UndefinedNullWrapper<W, V extends Schema<E> ? UndefinedNullWrapper<O, Transformed<E, V>>[] : never> : never : never : never : never;
export type Transformed<T, S extends Schema<T>> = {
-readonly [Key in keyof S]: S[Key] extends string ? GetFieldType<T, S[Key]> : S[Key] extends readonly [infer U, infer V] ? U extends string ? GetFieldType<T, U> extends infer W ? W extends unknown[] ? V extends Schema<W[number]> ? Transformed<W[number], V>[] : never : never : never : never : never;
-readonly [Key in keyof S]: S[Key] extends string ? GetFieldType<T, S[Key]> : S[Key] extends Schema<T> ? Transformed<T, S[Key]> : S[Key] extends readonly [infer U, infer V] ? U extends string ? SubArrayTransformed<T, U, V> extends infer X ? X extends never[] ? never : X : never : never : never;
};
export type Reshaper<T, S extends Schema<T>> = (data: T) => Transformed<T, S>;
export {};
import { Reshaper, Schema } from "./types";
export declare const reshaperBuilder: <T extends object, S extends Schema<T>>(schema: S) => Reshaper<T, S>;
export declare const reshaperBuilder: <T, S extends Schema<T>>(schema: S) => Reshaper<T, S>;

@@ -1,38 +0,55 @@

import { ReshapeError } from "./errors";
const fieldAccessorImplementation = (o, field) => {
if (typeof o !== "object" || o === null) {
throw new ReshapeError("FieldNotObject");
const readField = (o, field) => {
if (typeof o !== "object") {
return undefined;
}
if (o === null) {
return o;
}
if (!Object.prototype.hasOwnProperty.call(o, field)) {
throw new ReshapeError("MissingField");
return undefined;
}
return o[field];
};
const handleArrayAccess = (field, fieldName, path) => {
const arrayAccessor = fieldName.split("[");
const arrayName = arrayAccessor[0];
const arrayIndex = arrayAccessor[1].split("]")[0];
const array = readField(field, arrayName);
if (array === undefined || array === null) {
return array;
}
if (!Array.isArray(array)) {
return undefined;
}
if (arrayIndex === "*") {
const result = handleFlattenedArrayAccess(array, [...path]);
if (path.find((item) => item.endsWith("[*]"))) {
return result.flatMap((item) => item === undefined || item === null ? [] : item);
}
return result;
}
else {
return handleIndexedArrayAccess(array, parseInt(arrayIndex), [...path]);
}
};
const handleIndexedArrayAccess = (array, index, path) => {
if (array.length <= index) {
return undefined;
}
const result = fieldAccessor(array[index], [...path]);
return result;
};
const handleFlattenedArrayAccess = (array, path) => {
return array
.filter((item) => item !== undefined && item !== null)
.map((item) => fieldAccessor(item, [...path]));
};
const fieldAccessor = (data, path) => {
let field = data;
for (let fieldName = path.shift(); fieldName !== undefined; fieldName = path.shift()) {
for (let fieldName = path.shift(); fieldName !== undefined && field !== undefined; fieldName = path.shift()) {
if (fieldName.endsWith("]")) {
const arrayAccessor = fieldName.split("[");
const arrayName = arrayAccessor[0];
const arrayIndex = arrayAccessor[1].split("]")[0];
const array = fieldAccessorImplementation(field, arrayName);
if (!Array.isArray(array)) {
throw new ReshapeError("FieldNotArray");
}
if (arrayIndex === "*") {
const result = array.map((item) => fieldAccessor(item, [...path]));
if (path.find((item) => item.endsWith("[*]"))) {
return result.flatMap((item) => item);
}
return result;
}
else {
if (array.length < parseInt(arrayIndex)) {
throw new ReshapeError("ArrayIndexOutOfBounds");
}
return fieldAccessor(array[parseInt(arrayIndex)], path);
}
return handleArrayAccess(field, fieldName, [...path]);
}
else {
field = fieldAccessorImplementation(field, fieldName);
field = readField(field, fieldName);
}

@@ -57,10 +74,12 @@ }

if (!Array.isArray(array)) {
throw new ReshapeError("FieldNotArray");
result[key] = undefined;
}
result[key] = array.map((item) => {
if (typeof item !== "object") {
throw new ReshapeError("FieldNotObject");
}
return objectConstructor(item, subSchema);
});
else {
result[key] = array.map((item) => {
if (typeof item !== "object") {
return undefined;
}
return objectConstructor(item, subSchema);
});
}
}

@@ -67,0 +86,0 @@ }

type IsAny<T> = unknown extends T ? keyof T extends never ? false : true : false;
type ExcludeArrayKeys<T> = T extends ArrayLike<unknown> ? Exclude<keyof T, keyof unknown[]> : keyof T;
type ArrayProperty<T, Key extends keyof T & string> = T[Key] extends unknown[] ? T[Key][number] extends Record<string, unknown> ? `${Key}[${number | "*"}].${PathElement<T[Key][number], ExcludeArrayKeys<T[Key][number]>> & string}` | `${Key}[${number | "*"}].${ExcludeArrayKeys<T[Key][number]> & string}` | `${Key}[${number | "*"}]` | `${Key}` : `${Key}[${number | "*"}]` | `${Key}` : never;
type RecordProperty<T, Key extends keyof T & string> = T[Key] extends Record<string, unknown> ? `${Key}.${PathElement<T[Key], ExcludeArrayKeys<T[Key]>> & string}` | `${Key}.${ExcludeArrayKeys<T[Key]> & string}` : never;
type ConcreteArrayElement<A extends unknown[] | undefined | null> = Exclude<Exclude<A, undefined | null>[number], undefined | null>;
type ArrayProperty<T, Key extends keyof T & string> = T[Key] extends unknown[] | undefined | null ? Exclude<T[Key], undefined | null>[number] extends Record<string, unknown> | undefined | null ? `${Key}[${number | "*"}].${PathElement<ConcreteArrayElement<T[Key]>, ExcludeArrayKeys<ConcreteArrayElement<T[Key]>>> & string}` | `${Key}[${number | "*"}].${ExcludeArrayKeys<ConcreteArrayElement<T[Key]>> & string}` | `${Key}[${number | "*"}]` | `${Key}` : `${Key}[${number | "*"}]` | `${Key}` : never;
type RecordProperty<T, Key extends keyof T & string> = T[Key] extends Record<string, unknown> | undefined | null ? `${Key}.${PathElement<Exclude<T[Key], undefined | null>, ExcludeArrayKeys<T[Key]>> & string}` | `${Key}.${ExcludeArrayKeys<Exclude<T[Key], undefined | null>> & string}` : never;
type PathElement<T, Key extends keyof T> = Key extends string ? IsAny<T[Key]> extends true ? never : ArrayProperty<T, Key> | RecordProperty<T, Key> : never;

@@ -9,15 +10,21 @@ type Path<T> = keyof T extends string ? PathElement<T, keyof T> | keyof T extends infer P ? P extends string | keyof T ? P : keyof T : keyof T : never;

type ArrayChildren<Key extends ArrayTerminal<P>, P> = Key extends `${infer K}[*]` ? P extends `${infer Left}[*].${infer Right}` ? Left extends K ? Right : never : never : never;
type NestedSchema<Path> = Readonly<{
[key: string]: Path | NestedSchema<Path> | SubArraySchema<ArrayTerminal<Path>, Path>;
}>;
type SubArrayDefinition<Key extends ArrayTerminal<P>, P> = {
readonly 0: Key;
readonly 1: Record<string, ArrayChildren<Key, P> | SubArraySchema<ArrayTerminal<ArrayChildren<Key, P>>, ArrayChildren<Key, P>>>;
readonly 1: Record<string, ArrayChildren<Key, P> | NestedSchema<ArrayChildren<Key, P>> | SubArraySchema<ArrayTerminal<ArrayChildren<Key, P>>, ArrayChildren<Key, P>>>;
};
type SubArraySchema<Key extends ArrayTerminal<P>, P> = SubArrayDefinition<Key, P>;
export type Schema<T> = Readonly<{
[key: string]: Path<T> | Schema<T> | SubArraySchema<ArrayTerminal<Path<T>>, Path<T>>;
}>;
type GetFieldType<T, P> = P extends keyof T ? T[P] : P extends `${infer U}[${infer V extends number | "*"}].${infer W}` ? GetFieldType<T, U> extends infer X ? X extends unknown[] ? V extends number ? GetFieldType<X[number], W> : GetFieldType<X[number], W> extends infer Y ? W extends `${string}[*]${string | ""}` ? Y : Y[] : never : never : never : P extends `${infer U}.${infer V}` ? U extends keyof T ? T[U] extends Record<string, unknown> ? GetFieldType<T[U], V> : never : never : P extends `${infer U}[${infer V extends number | "*"}]` ? U extends keyof T ? T[U] extends unknown[] ? V extends number ? T[U][number] : T[U] : never : never : never;
export type Schema<T> = NestedSchema<Path<T>>;
type UndefinedNullWrapper<C, R> = [undefined | null] extends [C] ? R | undefined | null : [undefined] extends [C] ? R | undefined : [null] extends [C] ? R | null : R;
type CheckAndGetSubFieldType<T, U extends keyof T, V> = T[U] extends Record<string, unknown> | undefined | null ? UndefinedNullWrapper<T[U], GetFieldType<Exclude<T[U], undefined | null>, V>> : never;
type CheckAndGetArraySubFieldType<A, V extends number | "*", P> = A extends unknown[] | undefined | null ? Exclude<A, undefined | null>[number] extends infer O ? GetFieldType<Exclude<O, undefined | null>, P> extends infer E ? V extends number ? UndefinedNullWrapper<O, E> | undefined : P extends `${string}[*]${string | ""}` ? UndefinedNullWrapper<A, E> : UndefinedNullWrapper<A, E[]> : never : never : never;
type CheckAndGetArrayFieldType<T, U extends keyof T, V extends number | "*"> = T[U] extends unknown[] | undefined | null ? V extends number ? UndefinedNullWrapper<T[U], Exclude<T[U], undefined | null>[number]> | undefined : UndefinedNullWrapper<T[U], Exclude<Exclude<T[U], undefined | null>[number], undefined | null>[]> : never;
type GetFieldType<T, P> = P extends keyof T ? T[P] : P extends `${infer U}[${infer V extends number | "*"}].${infer W}` ? GetFieldType<T, U> extends infer X ? CheckAndGetArraySubFieldType<X, V, W> : never : P extends `${infer U}.${infer V}` ? U extends keyof T ? CheckAndGetSubFieldType<T, U, V> : never : P extends `${infer U}[${infer V extends number | "*"}]` ? U extends keyof T ? CheckAndGetArrayFieldType<T, U, V> : never : T;
type SubArrayTransformed<T, P extends string, V> = GetFieldType<T, P> extends infer W ? W extends unknown[] | undefined | null ? Exclude<W, undefined | null>[number] extends infer O ? Exclude<O, undefined | null> extends infer E ? UndefinedNullWrapper<W, V extends Schema<E> ? UndefinedNullWrapper<O, Transformed<E, V>>[] : never> : never : never : never : never;
export type Transformed<T, S extends Schema<T>> = {
-readonly [Key in keyof S]: S[Key] extends string ? GetFieldType<T, S[Key]> : S[Key] extends readonly [infer U, infer V] ? U extends string ? GetFieldType<T, U> extends infer W ? W extends unknown[] ? V extends Schema<W[number]> ? Transformed<W[number], V>[] : never : never : never : never : never;
-readonly [Key in keyof S]: S[Key] extends string ? GetFieldType<T, S[Key]> : S[Key] extends Schema<T> ? Transformed<T, S[Key]> : S[Key] extends readonly [infer U, infer V] ? U extends string ? SubArrayTransformed<T, U, V> extends infer X ? X extends never[] ? never : X : never : never : never;
};
export type Reshaper<T, S extends Schema<T>> = (data: T) => Transformed<T, S>;
export {};
import { Reshaper, Schema } from "./types";
export declare const reshaperBuilder: <T extends object, S extends Schema<T>>(schema: S) => Reshaper<T, S>;
export declare const reshaperBuilder: <T, S extends Schema<T>>(schema: S) => Reshaper<T, S>;
//# sourceMappingURL=reshape.d.ts.map
type IsAny<T> = unknown extends T ? keyof T extends never ? false : true : false;
type ExcludeArrayKeys<T> = T extends ArrayLike<unknown> ? Exclude<keyof T, keyof unknown[]> : keyof T;
type ArrayProperty<T, Key extends keyof T & string> = T[Key] extends unknown[] ? T[Key][number] extends Record<string, unknown> ? `${Key}[${number | "*"}].${PathElement<T[Key][number], ExcludeArrayKeys<T[Key][number]>> & string}` | `${Key}[${number | "*"}].${ExcludeArrayKeys<T[Key][number]> & string}` | `${Key}[${number | "*"}]` | `${Key}` : `${Key}[${number | "*"}]` | `${Key}` : never;
type RecordProperty<T, Key extends keyof T & string> = T[Key] extends Record<string, unknown> ? `${Key}.${PathElement<T[Key], ExcludeArrayKeys<T[Key]>> & string}` | `${Key}.${ExcludeArrayKeys<T[Key]> & string}` : never;
type ConcreteArrayElement<A extends unknown[] | undefined | null> = Exclude<Exclude<A, undefined | null>[number], undefined | null>;
type ArrayProperty<T, Key extends keyof T & string> = T[Key] extends unknown[] | undefined | null ? Exclude<T[Key], undefined | null>[number] extends Record<string, unknown> | undefined | null ? `${Key}[${number | "*"}].${PathElement<ConcreteArrayElement<T[Key]>, ExcludeArrayKeys<ConcreteArrayElement<T[Key]>>> & string}` | `${Key}[${number | "*"}].${ExcludeArrayKeys<ConcreteArrayElement<T[Key]>> & string}` | `${Key}[${number | "*"}]` | `${Key}` : `${Key}[${number | "*"}]` | `${Key}` : never;
type RecordProperty<T, Key extends keyof T & string> = T[Key] extends Record<string, unknown> | undefined | null ? `${Key}.${PathElement<Exclude<T[Key], undefined | null>, ExcludeArrayKeys<T[Key]>> & string}` | `${Key}.${ExcludeArrayKeys<Exclude<T[Key], undefined | null>> & string}` : never;
type PathElement<T, Key extends keyof T> = Key extends string ? IsAny<T[Key]> extends true ? never : ArrayProperty<T, Key> | RecordProperty<T, Key> : never;

@@ -9,13 +10,19 @@ type Path<T> = keyof T extends string ? PathElement<T, keyof T> | keyof T extends infer P ? P extends string | keyof T ? P : keyof T : keyof T : never;

type ArrayChildren<Key extends ArrayTerminal<P>, P> = Key extends `${infer K}[*]` ? P extends `${infer Left}[*].${infer Right}` ? Left extends K ? Right : never : never : never;
type NestedSchema<Path> = Readonly<{
[key: string]: Path | NestedSchema<Path> | SubArraySchema<ArrayTerminal<Path>, Path>;
}>;
type SubArrayDefinition<Key extends ArrayTerminal<P>, P> = {
readonly 0: Key;
readonly 1: Record<string, ArrayChildren<Key, P> | SubArraySchema<ArrayTerminal<ArrayChildren<Key, P>>, ArrayChildren<Key, P>>>;
readonly 1: Record<string, ArrayChildren<Key, P> | NestedSchema<ArrayChildren<Key, P>> | SubArraySchema<ArrayTerminal<ArrayChildren<Key, P>>, ArrayChildren<Key, P>>>;
};
type SubArraySchema<Key extends ArrayTerminal<P>, P> = SubArrayDefinition<Key, P>;
export type Schema<T> = Readonly<{
[key: string]: Path<T> | Schema<T> | SubArraySchema<ArrayTerminal<Path<T>>, Path<T>>;
}>;
type GetFieldType<T, P> = P extends keyof T ? T[P] : P extends `${infer U}[${infer V extends number | "*"}].${infer W}` ? GetFieldType<T, U> extends infer X ? X extends unknown[] ? V extends number ? GetFieldType<X[number], W> : GetFieldType<X[number], W> extends infer Y ? W extends `${string}[*]${string | ""}` ? Y : Y[] : never : never : never : P extends `${infer U}.${infer V}` ? U extends keyof T ? T[U] extends Record<string, unknown> ? GetFieldType<T[U], V> : never : never : P extends `${infer U}[${infer V extends number | "*"}]` ? U extends keyof T ? T[U] extends unknown[] ? V extends number ? T[U][number] : T[U] : never : never : never;
export type Schema<T> = NestedSchema<Path<T>>;
type UndefinedNullWrapper<C, R> = [undefined | null] extends [C] ? R | undefined | null : [undefined] extends [C] ? R | undefined : [null] extends [C] ? R | null : R;
type CheckAndGetSubFieldType<T, U extends keyof T, V> = T[U] extends Record<string, unknown> | undefined | null ? UndefinedNullWrapper<T[U], GetFieldType<Exclude<T[U], undefined | null>, V>> : never;
type CheckAndGetArraySubFieldType<A, V extends number | "*", P> = A extends unknown[] | undefined | null ? Exclude<A, undefined | null>[number] extends infer O ? GetFieldType<Exclude<O, undefined | null>, P> extends infer E ? V extends number ? UndefinedNullWrapper<O, E> | undefined : P extends `${string}[*]${string | ""}` ? UndefinedNullWrapper<A, E> : UndefinedNullWrapper<A, E[]> : never : never : never;
type CheckAndGetArrayFieldType<T, U extends keyof T, V extends number | "*"> = T[U] extends unknown[] | undefined | null ? V extends number ? UndefinedNullWrapper<T[U], Exclude<T[U], undefined | null>[number]> | undefined : UndefinedNullWrapper<T[U], Exclude<Exclude<T[U], undefined | null>[number], undefined | null>[]> : never;
type GetFieldType<T, P> = P extends keyof T ? T[P] : P extends `${infer U}[${infer V extends number | "*"}].${infer W}` ? GetFieldType<T, U> extends infer X ? CheckAndGetArraySubFieldType<X, V, W> : never : P extends `${infer U}.${infer V}` ? U extends keyof T ? CheckAndGetSubFieldType<T, U, V> : never : P extends `${infer U}[${infer V extends number | "*"}]` ? U extends keyof T ? CheckAndGetArrayFieldType<T, U, V> : never : T;
type SubArrayTransformed<T, P extends string, V> = GetFieldType<T, P> extends infer W ? W extends unknown[] | undefined | null ? Exclude<W, undefined | null>[number] extends infer O ? Exclude<O, undefined | null> extends infer E ? UndefinedNullWrapper<W, V extends Schema<E> ? UndefinedNullWrapper<O, Transformed<E, V>>[] : never> : never : never : never : never;
export type Transformed<T, S extends Schema<T>> = {
-readonly [Key in keyof S]: S[Key] extends string ? GetFieldType<T, S[Key]> : S[Key] extends readonly [infer U, infer V] ? U extends string ? GetFieldType<T, U> extends infer W ? W extends unknown[] ? V extends Schema<W[number]> ? Transformed<W[number], V>[] : never : never : never : never : never;
-readonly [Key in keyof S]: S[Key] extends string ? GetFieldType<T, S[Key]> : S[Key] extends Schema<T> ? Transformed<T, S[Key]> : S[Key] extends readonly [infer U, infer V] ? U extends string ? SubArrayTransformed<T, U, V> extends infer X ? X extends never[] ? never : X : never : never : never;
};

@@ -22,0 +29,0 @@ export type Reshaper<T, S extends Schema<T>> = (data: T) => Transformed<T, S>;

{
"name": "object-reshaper",
"version": "0.1.0",
"version": "0.2.0",
"description": "TypeScript-first schema-based object transformation",

@@ -5,0 +5,0 @@ "license": "MIT",

@@ -9,2 +9,14 @@ # Object Reshaper

## Introduction
Object Reshaper provides a type safe interface for transforming objects using a schema, allowing users to easily transform objects without having to write boilerplate code.
It supports renaming fields, extracting nested objects, and flattening nested arrays.
Simply define a schema that describes the desired output using dot notation and Object Reshaper will generate a function that transforms input objects into the desired shape.
> :warning: **What Object Reshaper is not**
>
> Object Reshaper is not a replacement for existing object validation libraries.
> In fact, it is strongly recommended that any inputs from users or external sources be validated before being passed to Object Reshaper.
> Object Reshaper does not perform any validation of object properties at runtime and supplying any inputs that do not conform to the expected type will result in undefined behaviour.
## Installation

@@ -53,3 +65,87 @@

const reshaper = reshaperBuilder<Input, typeof schema>(schema);
reshaper({ user: { address: { street: "home" } } }); // => { new: "home" } (Type: { new: string })
reshaper({ user: { address: { street: "home" } } }); // => { street: "home" } (Type: { street: string })
```
## Manipulating Arrays
Accessing array elements
```ts
import { reshaperBuilder, Schema } from "object-reshaper";
type Input = {
data: number[];
};
const schema = {
new: "data[0]",
} as const satisfies Schema<Input>;
const reshaper = reshaperBuilder<Input, typeof schema>(schema);
reshaper({ data: [1, 2, 3] }); // => { new: 1 } (Type: { new: number | undefined })
```
Extracting fields from array elements
```ts
import { reshaperBuilder, Schema } from "object-reshaper";
type Input = {
array: { nested: { within: string } }[];
};
const schema = {
new: "array[*].nested.within",
} as const satisfies Schema<Input>;
const reshaper = reshaperBuilder<Input, typeof schema>(schema);
reshaper({
array: [{ nested: { within: "first" } }, { nested: { within: "second" } }],
}); // => { new: ["first", "second"] } (Type: { new: string[] })
```
Flattening nested arrays
```ts
import { reshaperBuilder, Schema } from "object-reshaper";
type Input = {
data: { nested: number[] }[];
};
const schema = {
new: "data[*].nested[*]",
} as const satisfies Schema<Input>;
const reshaper = reshaperBuilder<Input, typeof schema>(schema);
reshaper({ data: [{ nested: [1, 2] }, { nested: [3, 4] }] });
// => { new: [1,2,3,4] } (Type: { new: number[] })
```
_Support for multidimensional arrays coming soon_
## Transforming Objects Within Arrays
Flatten nested arrays and transform elements
```ts
import { reshaperBuilder, Schema } from "object-reshaper";
type Input = {
data: { nested: { item: { id: number; name: string } }[] }[];
};
const schema = {
new: [
"data[*].nested[*]",
{
new: "item.id",
},
],
} as const satisfies Schema<Input>;
const reshaper = reshaperBuilder<Input, typeof schema>(schema);
reshaper({
data: [
{ nested: [{ item: { id: 1, name: "first" } }] },
{
nested: [
{ item: { id: 2, name: "second" } },
{ item: { id: 3, name: "third" } },
],
},
],
}); // => { new: [{ new: 1 }, { new: 2 }, { new: 3 }] } (Type: { new: { new: number }[] })
```

@@ -1,10 +0,12 @@

import { ReshapeError } from "./errors";
import { Reshaper, Schema, Transformed } from "./types";
const fieldAccessorImplementation = <T>(o: T, field: string): unknown => {
if (typeof o !== "object" || o === null) {
throw new ReshapeError("FieldNotObject");
const readField = <T>(o: T, field: string): unknown => {
if (typeof o !== "object") {
return undefined;
}
if (o === null) {
return o;
}
if (!Object.prototype.hasOwnProperty.call(o, field)) {
throw new ReshapeError("MissingField");
return undefined;
}

@@ -15,2 +17,51 @@ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any

const handleArrayAccess = (
field: unknown,
fieldName: string,
path: string[]
) => {
const arrayAccessor = fieldName.split("[");
const arrayName = arrayAccessor[0];
const arrayIndex = arrayAccessor[1].split("]")[0];
const array = readField(field, arrayName);
if (array === undefined || array === null) {
return array;
}
if (!Array.isArray(array)) {
return undefined;
}
if (arrayIndex === "*") {
const result = handleFlattenedArrayAccess(array, [...path]);
if (path.find((item) => item.endsWith("[*]"))) {
return result.flatMap((item) =>
item === undefined || item === null ? [] : item
);
}
return result;
} else {
return handleIndexedArrayAccess(array, parseInt(arrayIndex), [...path]);
}
};
const handleIndexedArrayAccess = (
array: unknown[],
index: number,
path: string[]
): unknown => {
if (array.length <= index) {
return undefined;
}
const result = fieldAccessor(array[index], [...path]);
return result;
};
const handleFlattenedArrayAccess = (
array: unknown[],
path: string[]
): unknown[] => {
return array
.filter((item) => item !== undefined && item !== null)
.map((item) => fieldAccessor(item, [...path]));
};
const fieldAccessor = <T>(data: T, path: string[]): unknown => {

@@ -20,27 +71,9 @@ let field: unknown = data;

let fieldName = path.shift();
fieldName !== undefined;
fieldName !== undefined && field !== undefined;
fieldName = path.shift()
) {
if (fieldName.endsWith("]")) {
const arrayAccessor = fieldName.split("[");
const arrayName = arrayAccessor[0];
const arrayIndex = arrayAccessor[1].split("]")[0];
const array = fieldAccessorImplementation(field, arrayName);
if (!Array.isArray(array)) {
throw new ReshapeError("FieldNotArray");
}
if (arrayIndex === "*") {
const result = array.map((item) => fieldAccessor(item, [...path]));
if (path.find((item) => item.endsWith("[*]"))) {
return result.flatMap((item) => item);
}
return result;
} else {
if (array.length < parseInt(arrayIndex)) {
throw new ReshapeError("ArrayIndexOutOfBounds");
}
return fieldAccessor(array[parseInt(arrayIndex)], path);
}
return handleArrayAccess(field, fieldName, [...path]);
} else {
field = fieldAccessorImplementation(field, fieldName);
field = readField(field, fieldName);
}

@@ -68,10 +101,11 @@ }

if (!Array.isArray(array)) {
throw new ReshapeError("FieldNotArray");
result[key] = undefined;
} else {
result[key] = array.map((item: unknown) => {
if (typeof item !== "object") {
return undefined;
}
return objectConstructor(item, subSchema);
});
}
result[key] = array.map((item: unknown) => {
if (typeof item !== "object") {
throw new ReshapeError("FieldNotObject");
}
return objectConstructor(item, subSchema);
});
}

@@ -82,5 +116,5 @@ }

export const reshaperBuilder: <T extends object, S extends Schema<T>>(
export const reshaperBuilder: <T, S extends Schema<T>>(
schema: S
) => Reshaper<T, S> = <T extends object, S extends Schema<T>>(
) => Reshaper<T, S> = <T, S extends Schema<T>>(
schema: S

@@ -87,0 +121,0 @@ ): ((data: T) => Transformed<T, S>) => {

@@ -90,2 +90,5 @@ import { describe, expect, test } from "@jest/globals";

const result: typeof expected = reshaper(data);
let typecheck = reshaper(data);
typecheck = expected;
[typecheck];
expect(result).toEqual(expected);

@@ -105,3 +108,3 @@ });

} as const satisfies Schema<typeof data>;
const expected = {
const expected: { new: number | undefined } = {
new: data.sub.array[0],

@@ -111,2 +114,5 @@ };

const result: typeof expected = reshaper(data);
let typecheck = reshaper(data);
typecheck = expected;
[typecheck];
expect(result).toEqual(expected);

@@ -124,3 +130,3 @@ });

} as const satisfies Schema<typeof data>;
const expected = {
const expected: { new: { id: number } | undefined } = {
new: data.sub.array[0],

@@ -130,2 +136,5 @@ };

const result: typeof expected = reshaper(data);
let typecheck = reshaper(data);
typecheck = expected;
[typecheck];
expect(result).toEqual(expected);

@@ -143,3 +152,3 @@ });

} as const satisfies Schema<typeof data>;
const expected = {
const expected: { new: { id: number } | undefined } = {
new: data.sub.array[1],

@@ -149,2 +158,5 @@ };

const result: typeof expected = reshaper(data);
let typecheck = reshaper(data);
typecheck = expected;
[typecheck];
expect(result).toEqual(expected);

@@ -162,3 +174,5 @@ });

} as const satisfies Schema<typeof data>;
const expected = {
const expected: {
new: number | undefined;
} = {
new: data.sub.array[0].id,

@@ -168,2 +182,5 @@ };

const result: typeof expected = reshaper(data);
let typecheck = reshaper(data);
typecheck = expected;
[typecheck];
expect(result).toEqual(expected);

@@ -188,2 +205,5 @@ });

const result: typeof expected = reshaper(data);
let typecheck = reshaper(data);
typecheck = expected;
[typecheck];
expect(result).toEqual(expected);

@@ -206,2 +226,5 @@ });

const result: typeof expected = reshaper(data);
let typecheck = reshaper(data);
typecheck = expected;
[typecheck];
expect(result).toEqual(expected);

@@ -227,2 +250,5 @@ });

const result: typeof expected = reshaper(data);
let typecheck = reshaper(data);
typecheck = expected;
[typecheck];
expect(result).toEqual(expected);

@@ -248,2 +274,5 @@ });

const result: typeof expected = reshaper(data);
let typecheck = reshaper(data);
typecheck = expected;
[typecheck];
expect(result).toEqual(expected);

@@ -273,2 +302,5 @@ });

const result: typeof expected = reshaper(data);
let typecheck = reshaper(data);
typecheck = expected;
[typecheck];
expect(result).toEqual(expected);

@@ -296,2 +328,5 @@ });

const result: typeof expected = reshaper(data);
let typecheck = reshaper(data);
typecheck = expected;
[typecheck];
expect(result).toEqual(expected);

@@ -325,5 +360,72 @@ });

const result: typeof expected = reshaper(data);
let typecheck = reshaper(data);
typecheck = expected;
[typecheck];
expect(result).toEqual(expected);
});
test("should create mapped array with nested object", () => {
const data = {
sub: {
array: [{ sub: { subarray: [1, 2] } }, { sub: { subarray: [3, 4] } }],
},
};
const schema = {
new: [
"sub.array[*]",
{
new: {
new: "sub.subarray[*]",
},
},
],
} as const satisfies Schema<typeof data>;
const expected = {
new: data.sub.array.map((item) => ({
new: { new: item.sub.subarray },
})),
};
const reshaper = reshaperBuilder<typeof data, typeof schema>(schema);
const result: typeof expected = reshaper(data);
let typecheck = reshaper(data);
typecheck = expected;
[typecheck];
expect(result).toEqual(expected);
});
test("should create mapped array with nested mapped array", () => {
const data = {
sub: {
array: [
{ subarray: [{ id: 1 }, { id: 2 }] },
{ subarray: [{ id: 3 }, { id: 4 }] },
],
},
};
const schema = {
new: [
"sub.array[*]",
{
new: [
"subarray[*]",
{
new: "id",
},
],
},
],
} as const satisfies Schema<typeof data>;
const expected = {
new: data.sub.array.map((item) => ({
new: item.subarray.map((item) => ({ new: item.id })),
})),
};
const reshaper = reshaperBuilder<typeof data, typeof schema>(schema);
const result: typeof expected = reshaper(data);
let typecheck = reshaper(data);
typecheck = expected;
[typecheck];
expect(result).toEqual(expected);
});
});
});

@@ -11,11 +11,25 @@ type IsAny<T> = unknown extends T

type ArrayProperty<T, Key extends keyof T & string> = T[Key] extends unknown[]
? T[Key][number] extends Record<string, unknown>
type ConcreteArrayElement<A extends unknown[] | undefined | null> = Exclude<
Exclude<A, undefined | null>[number],
undefined | null
>;
type ArrayProperty<T, Key extends keyof T & string> = T[Key] extends
| unknown[]
| undefined
| null
? Exclude<T[Key], undefined | null>[number] extends
| Record<string, unknown>
| undefined
| null
?
| `${Key}[${number | "*"}].${PathElement<
T[Key][number],
ExcludeArrayKeys<T[Key][number]>
ConcreteArrayElement<T[Key]>,
ExcludeArrayKeys<ConcreteArrayElement<T[Key]>>
> &
string}`
| `${Key}[${number | "*"}].${ExcludeArrayKeys<T[Key][number]> & string}`
| `${Key}[${number | "*"}].${ExcludeArrayKeys<
ConcreteArrayElement<T[Key]>
> &
string}`
| `${Key}[${number | "*"}]`

@@ -26,9 +40,13 @@ | `${Key}`

type RecordProperty<T, Key extends keyof T & string> = T[Key] extends Record<
string,
unknown
>
type RecordProperty<T, Key extends keyof T & string> = T[Key] extends
| Record<string, unknown>
| undefined
| null
?
| `${Key}.${PathElement<T[Key], ExcludeArrayKeys<T[Key]>> & string}`
| `${Key}.${ExcludeArrayKeys<T[Key]> & string}`
| `${Key}.${PathElement<
Exclude<T[Key], undefined | null>,
ExcludeArrayKeys<T[Key]>
> &
string}`
| `${Key}.${ExcludeArrayKeys<Exclude<T[Key], undefined | null>> & string}`
: never;

@@ -63,2 +81,9 @@

type NestedSchema<Path> = Readonly<{
[key: string]:
| Path
| NestedSchema<Path>
| SubArraySchema<ArrayTerminal<Path>, Path>;
}>;
type SubArrayDefinition<Key extends ArrayTerminal<P>, P> = {

@@ -69,2 +94,3 @@ readonly 0: Key;

| ArrayChildren<Key, P>
| NestedSchema<ArrayChildren<Key, P>>
| SubArraySchema<

@@ -82,9 +108,50 @@ ArrayTerminal<ArrayChildren<Key, P>>,

export type Schema<T> = Readonly<{
[key: string]:
| Path<T>
| Schema<T>
| SubArraySchema<ArrayTerminal<Path<T>>, Path<T>>;
}>;
export type Schema<T> = NestedSchema<Path<T>>;
type UndefinedNullWrapper<C, R> = [undefined | null] extends [C]
? R | undefined | null
: [undefined] extends [C]
? R | undefined
: [null] extends [C]
? R | null
: R;
type CheckAndGetSubFieldType<T, U extends keyof T, V> = T[U] extends
| Record<string, unknown>
| undefined
| null
? UndefinedNullWrapper<T[U], GetFieldType<Exclude<T[U], undefined | null>, V>>
: never;
type CheckAndGetArraySubFieldType<A, V extends number | "*", P> = A extends
| unknown[]
| undefined
| null
? Exclude<A, undefined | null>[number] extends infer O
? GetFieldType<Exclude<O, undefined | null>, P> extends infer E
? V extends number
? UndefinedNullWrapper<O, E> | undefined
: P extends `${string}[*]${string | ""}`
? UndefinedNullWrapper<A, E>
: UndefinedNullWrapper<A, E[]>
: never
: never
: never;
// Guaranteed to be invoked only once in a path and is the last key.
type CheckAndGetArrayFieldType<
T,
U extends keyof T,
V extends number | "*"
> = T[U] extends unknown[] | undefined | null
? V extends number
?
| UndefinedNullWrapper<T[U], Exclude<T[U], undefined | null>[number]>
| undefined
: UndefinedNullWrapper<
T[U],
Exclude<Exclude<T[U], undefined | null>[number], undefined | null>[]
>
: never;
type GetFieldType<T, P> = P extends keyof T

@@ -94,24 +161,28 @@ ? T[P]

? GetFieldType<T, U> extends infer X
? X extends unknown[]
? V extends number
? GetFieldType<X[number], W>
: GetFieldType<X[number], W> extends infer Y
? W extends `${string}[*]${string | ""}`
? Y
: Y[]
: never
: never
? CheckAndGetArraySubFieldType<X, V, W>
: never
: P extends `${infer U}.${infer V}`
? U extends keyof T
? T[U] extends Record<string, unknown>
? GetFieldType<T[U], V>
: never
? CheckAndGetSubFieldType<T, U, V>
: never
: P extends `${infer U}[${infer V extends number | "*"}]`
? U extends keyof T
? T[U] extends unknown[]
? V extends number
? T[U][number]
: T[U]
? CheckAndGetArrayFieldType<T, U, V>
: never
: T;
type SubArrayTransformed<T, P extends string, V> = GetFieldType<
T,
P
> extends infer W
? W extends unknown[] | undefined | null
? Exclude<W, undefined | null>[number] extends infer O
? Exclude<O, undefined | null> extends infer E
? UndefinedNullWrapper<
W,
V extends Schema<E>
? UndefinedNullWrapper<O, Transformed<E, V>>[]
: never
>
: never
: never

@@ -124,10 +195,10 @@ : never

? GetFieldType<T, S[Key]>
: S[Key] extends Schema<T>
? Transformed<T, S[Key]>
: S[Key] extends readonly [infer U, infer V]
? U extends string
? GetFieldType<T, U> extends infer W
? W extends unknown[]
? V extends Schema<W[number]>
? Transformed<W[number], V>[]
: never
: never
? SubArrayTransformed<T, U, V> extends infer X
? X extends never[]
? never
: X
: never

@@ -134,0 +205,0 @@ : never

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc