From b52a1777511ecb2b45e3e9e519639555908fd7cd Mon Sep 17 00:00:00 2001 From: Toni Oriol Date: Sun, 27 Oct 2024 13:12:40 +0100 Subject: [PATCH] feat: working --- .gitignore | 1 + README.md | 16 +- dist/index.d.ts | 83 --------- dist/index.js | 108 ----------- dist/types.d.ts | 103 ----------- dist/types.js | 3 - src/__tests__/index.test.ts | 353 +++++++++++++----------------------- src/index.ts | 36 ++-- src/types.ts | 38 ++-- 9 files changed, 168 insertions(+), 573 deletions(-) delete mode 100644 dist/index.d.ts delete mode 100644 dist/index.js delete mode 100644 dist/types.d.ts delete mode 100644 dist/types.js diff --git a/.gitignore b/.gitignore index 3c3629e..f06235c 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules +dist diff --git a/README.md b/README.md index 4231270..d2c938f 100644 --- a/README.md +++ b/README.md @@ -37,11 +37,11 @@ interface UserDTO { const schema = [ { to: 'fullName', - fn: ({ entity }) => `${entity.firstName} ${entity.lastName}` + fn: ({ source }) => `${source.firstName} ${source.lastName}` }, { to: 'yearOfBirth', - fn: ({ entity }) => new Date().getFullYear() - entity.age + fn: ({ source }) => new Date().getFullYear() - source.age } ]; @@ -92,7 +92,7 @@ interface PricedProduct { const schema = [ { to: 'finalPrice', - fn: ({ entity, extra }) => entity.price * (1 + extra.taxRate) + fn: ({ source, extra }) => source.price * (1 + extra.taxRate) } ]; @@ -107,14 +107,14 @@ const pricedProduct = mutate( ## API Reference -### `mutate(schema, entity, extra?)` +### `mutate(schema, source, extra?)` -Transforms a source entity into a target type based on the provided schema. +Transforms a source source into a target type based on the provided schema. #### Parameters - `schema`: Array of transformation rules defining how properties should be mapped or transformed -- `entity`: Source entity to transform +- `source`: Source source to transform - `extra`: (Optional) Additional data to pass to transformation functions #### Schema Options @@ -131,7 +131,7 @@ Transforms a source entity into a target type based on the provided schema. ```typescript { to: keyof Target; - fn: (args: { entity: Source; extra?: Extra }) => unknown; + fn: (args: { source: Source; extra?: Extra }) => unknown; } ``` @@ -140,7 +140,7 @@ Transforms a source entity into a target type based on the provided schema. { to: keyof Target; from: keyof Source; - fn: (args: { entity: Source; from?: keyof Source; extra?: Extra }) => unknown; + fn: (args: { source: Source; from?: keyof Source; extra?: Extra }) => unknown; } ``` diff --git a/dist/index.d.ts b/dist/index.d.ts deleted file mode 100644 index 27bc0c8..0000000 --- a/dist/index.d.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { Extra, Schema } from './types'; -export * from './types'; -/** - * Transforms data from one shape to another using a declarative schema. - * The schema defines how properties should be mapped or transformed from - * the source object to create the target object. - * - * @typeParam From - The type of the source object - * @typeParam To - The type of the target object - * @typeParam TExtra - The type of any extra context data (defaults to Extra) - * - * @param schema - Array of transformation rules defining the mapping between source and target - * @param entity - Source object to transform - * @param extra - Optional additional context data available to transformation functions - * - * @returns A new object matching the target type specification - * - * @example - * Basic property mapping: - * ```typescript - * import { mutate } from 'mutant'; - * - * const schema = [ - * { from: 'firstName', to: 'givenName' }, - * { from: 'lastName', to: 'familyName' } - * ]; - * - * const result = mutate(schema, { - * firstName: 'John', - * lastName: 'Doe' - * }); - * // Result: { givenName: 'John', familyName: 'Doe' } - * ``` - * - * @example - * Using transformation functions: - * ```typescript - * import { mutate } from 'mutant'; - * - * const schema = [ - * { - * to: 'fullName', - * fn: ({ entity }) => `${entity.firstName} ${entity.lastName}` - * }, - * { - * from: 'age', - * to: 'isAdult', - * fn: ({ entity }) => entity.age >= 18 - * } - * ]; - * - * const result = mutate(schema, { - * firstName: 'John', - * lastName: 'Doe', - * age: 25 - * }); - * // Result: { fullName: 'John Doe', isAdult: true } - * ``` - * - * @example - * Using extra context: - * ```typescript - * import { mutate } from 'mutant'; - * - * const schema = [ - * { - * to: 'greeting', - * fn: ({ entity, extra }) => - * `${extra.greeting}, ${entity.firstName}!` - * } - * ]; - * - * const result = mutate(schema, - * { firstName: 'John' }, - * { greeting: 'Hello' } - * ); - * // Result: { greeting: 'Hello, John!' } - * ``` - * - * @throws {TypeError} If schema or entity are null/undefined - * @throws {Error} If schema contains invalid transformation rules - */ -export declare const mutate: (schema: Schema[], entity: From, extra?: TExtra) => To; diff --git a/dist/index.js b/dist/index.js deleted file mode 100644 index 010df0e..0000000 --- a/dist/index.js +++ /dev/null @@ -1,108 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __exportStar = (this && this.__exportStar) || function(m, exports) { - for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.mutate = void 0; -__exportStar(require("./types"), exports); -/** - * Transforms data from one shape to another using a declarative schema. - * The schema defines how properties should be mapped or transformed from - * the source object to create the target object. - * - * @typeParam From - The type of the source object - * @typeParam To - The type of the target object - * @typeParam TExtra - The type of any extra context data (defaults to Extra) - * - * @param schema - Array of transformation rules defining the mapping between source and target - * @param entity - Source object to transform - * @param extra - Optional additional context data available to transformation functions - * - * @returns A new object matching the target type specification - * - * @example - * Basic property mapping: - * ```typescript - * import { mutate } from 'mutant'; - * - * const schema = [ - * { from: 'firstName', to: 'givenName' }, - * { from: 'lastName', to: 'familyName' } - * ]; - * - * const result = mutate(schema, { - * firstName: 'John', - * lastName: 'Doe' - * }); - * // Result: { givenName: 'John', familyName: 'Doe' } - * ``` - * - * @example - * Using transformation functions: - * ```typescript - * import { mutate } from 'mutant'; - * - * const schema = [ - * { - * to: 'fullName', - * fn: ({ entity }) => `${entity.firstName} ${entity.lastName}` - * }, - * { - * from: 'age', - * to: 'isAdult', - * fn: ({ entity }) => entity.age >= 18 - * } - * ]; - * - * const result = mutate(schema, { - * firstName: 'John', - * lastName: 'Doe', - * age: 25 - * }); - * // Result: { fullName: 'John Doe', isAdult: true } - * ``` - * - * @example - * Using extra context: - * ```typescript - * import { mutate } from 'mutant'; - * - * const schema = [ - * { - * to: 'greeting', - * fn: ({ entity, extra }) => - * `${extra.greeting}, ${entity.firstName}!` - * } - * ]; - * - * const result = mutate(schema, - * { firstName: 'John' }, - * { greeting: 'Hello' } - * ); - * // Result: { greeting: 'Hello, John!' } - * ``` - * - * @throws {TypeError} If schema or entity are null/undefined - * @throws {Error} If schema contains invalid transformation rules - */ -const mutate = (schema, entity, extra) => schema.reduce((acc, rule) => { - const value = 'fn' in rule - ? rule.fn({ entity, from: 'from' in rule ? rule.from : undefined, extra }) - : entity[rule.from]; - return { - ...acc, - [rule.to]: value - }; -}, {}); -exports.mutate = mutate; diff --git a/dist/types.d.ts b/dist/types.d.ts deleted file mode 100644 index e808096..0000000 --- a/dist/types.d.ts +++ /dev/null @@ -1,103 +0,0 @@ -/** - * Type representing additional context data that can be passed to mutations. - * This allows passing arbitrary data to transformation functions for complex scenarios. - * - * @example - * ```typescript - * import { Extra } from 'mutant'; - * - * const extra: Extra = { - * timezone: 'UTC', - * locale: 'en-US' - * }; - * ``` - */ -export type Extra = Record; -/** - * Arguments passed to mutation functions. This interface defines the structure - * of parameters available to transformation functions. - * - * @typeParam From - The type of the source object being transformed - */ -export interface MutateFnArgs { - /** The complete source object being mutated */ - entity: From; - /** - * Source property key if using direct property mapping. - * Only available when the schema rule includes a 'from' property. - */ - from?: keyof From; - /** - * Additional context data passed to the mutation. - * Useful for providing external configuration or dependencies. - */ - extra?: Extra; -} -/** - * Type definition for a mutation function that transforms data. - * These functions receive the source entity and optional context data, - * and return the transformed value. - * - * @typeParam From - The type of the source object being transformed - * - * @example - * ```typescript - * import { MutateFn } from 'mutant'; - * - * const fullNameFn: MutateFn = ({ entity }) => - * `${entity.firstName} ${entity.lastName}`; - * ``` - */ -export type MutateFn = (args: MutateFnArgs) => unknown; -/** - * Schema definition for a single property mutation. - * Supports three forms of property transformation: - * 1. Direct mapping from source to target property - * 2. Custom function transformation - * 3. Combined mapping and transformation - * - * @typeParam From - The type of the source object - * @typeParam To - The type of the target object - * - * @example - * ```typescript - * import { Schema } from 'mutant'; - * - * // Direct mapping - * const directMapping: Schema = { - * from: 'sourceField', - * to: 'targetField' - * }; - * - * // Custom function - * const functionMapping: Schema = { - * to: 'targetField', - * fn: ({ entity }) => transform(entity) - * }; - * - * // Combined mapping and function - * const combinedMapping: Schema = { - * from: 'sourceField', - * to: 'targetField', - * fn: ({ entity, from }) => transform(entity[from]) - * }; - * ``` - */ -export type Schema = { - /** Target property key in the output object */ - to: keyof To; - /** Source property key in the input object */ - from: keyof From; -} | { - /** Target property key in the output object */ - to: keyof To; - /** Transformation function to generate the target value */ - fn: MutateFn; -} | { - /** Target property key in the output object */ - to: keyof To; - /** Source property key in the input object */ - from: keyof From; - /** Optional transformation function to modify the mapped value */ - fn: MutateFn; -}; diff --git a/dist/types.js b/dist/types.js deleted file mode 100644 index dd80c60..0000000 --- a/dist/types.js +++ /dev/null @@ -1,3 +0,0 @@ -"use strict"; -// types.ts -Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts index 2ad1691..b6a6433 100644 --- a/src/__tests__/index.test.ts +++ b/src/__tests__/index.test.ts @@ -1,249 +1,148 @@ -import { mutate, Schema } from '../'; +import { mutate, Schema } from '../' + +interface SourceUser { + firstName: string + lastName: string + age: number + email: string + address: { + street: string + city: string + country: string + } +} + +interface TargetUser { + fullName: string + userAge: number + contactEmail: string + location: string + isAdult: boolean +} describe('mutate', () => { - // Test basic direct property mapping - describe('direct property mapping', () => { - interface Source { - id: number; - name: string; - email: string; + const sourceUser: SourceUser = { + firstName: 'John', + lastName: 'Doe', + age: 25, + email: 'john.doe@example.com', + address: { + street: '123 Main St', + city: 'New York', + country: 'USA' } + } - interface Target { - userId: number; - userName: string; - userEmail: string; - } - - const schema: Schema[] = [ - { from: 'id', to: 'userId' }, - { from: 'name', to: 'userName' }, - { from: 'email', to: 'userEmail' }, - ]; - - it('should map properties directly', () => { - const source: Source = { - id: 1, - name: 'John Doe', - email: 'john@example.com', - }; - - const result = mutate(schema, source); - - expect(result).toEqual({ - userId: 1, - userName: 'John Doe', - userEmail: 'john@example.com', - }); - }); - - it('should handle undefined values', () => { - const source = { - id: undefined, - name: 'John Doe', - email: null, - } as unknown as Source; - - const result = mutate(schema, source); - - expect(result).toEqual({ - userId: undefined, - userName: 'John Doe', - userEmail: null, - }); - }); - }); - - // Test custom transformation functions - describe('custom transformation functions', () => { - interface Source { - firstName: string; - lastName: string; - age: number; - scores: number[]; - } + it('should perform direct property mapping', () => { + const schema: Schema>[] = [ + { from: 'email', to: 'contactEmail' } + ] - interface Target { - fullName: string; - isAdult: boolean; - averageScore: number; - } + const result = mutate(schema, sourceUser) + expect(result).toEqual({ contactEmail: 'john.doe@example.com' }) + }) - const schema: Schema[] = [ + it('should handle custom transformation functions', () => { + const schema: Schema>[] = [ { to: 'fullName', - fn: ({ entity }) => `${entity.firstName} ${entity.lastName}`, - }, - { - to: 'isAdult', - fn: ({ entity }) => entity.age >= 18, - }, + fn: ({ source }) => `${source.firstName} ${source.lastName}` + } + ] + + const result = mutate(schema, sourceUser) + expect(result).toEqual({ fullName: 'John Doe' }) + }) + + it('should handle transformation with both "from" and "fn"', () => { + const schema: Schema>[] = [ { - to: 'averageScore', - fn: ({ entity }) => - entity.scores.reduce((sum, score) => sum + score, 0) / entity.scores.length, - }, - ]; - - it('should apply custom transformations', () => { - const source: Source = { - firstName: 'John', - lastName: 'Doe', - age: 25, - scores: [85, 92, 78], - }; - - const result = mutate(schema, source); - - expect(result).toEqual({ - fullName: 'John Doe', - isAdult: true, - averageScore: 85, - }); - }); - - it('should handle edge cases in custom transformations', () => { - const source: Source = { - firstName: '', - lastName: '', - age: 17, - scores: [], - }; - - const result = mutate(schema, source); - - expect(result).toEqual({ - fullName: ' ', - isAdult: false, - averageScore: NaN, // Division by zero - }); - }); - }); - - // Test combined mapping and transformation - describe('combined mapping and transformation', () => { - interface Source { - price: number; - quantity: number; - category: string; - } + to: 'userAge', + fn: ({ source }) => source['age'] + 1 + } + ] - interface Target { - total: number; - displayCategory: string; - } + const result = mutate(schema, sourceUser) + expect(result).toEqual({ userAge: 26 }) + }) - const schema: Schema[] = [ + it('should handle extra data in transformations', () => { + const schema: Schema>[] = [ { - to: 'total', - fn: ({ entity }) => entity.price * entity.quantity, + to: 'location', + fn: ({ source, extra }) => + `${source.address.city}, ${source.address.country}${extra?.separator}` + } + ] + + const result = mutate(schema, sourceUser, { separator: ' | ' }) + expect(result).toEqual({ location: 'New York, USA | ' }) + }) + + it('should handle multiple transformations', () => { + const schema: Schema[] = [ + { + to: 'fullName', + fn: ({ source }) => `${source.firstName} ${source.lastName}` }, { - to: 'displayCategory', - fn: ({ entity, from }) => entity.category.toUpperCase(), + from: 'age', + to: 'userAge' }, - ]; - - it('should combine mapping and transformation', () => { - const source: Source = { - price: 10, - quantity: 3, - category: 'electronics', - }; - - const result = mutate(schema, source); - - expect(result).toEqual({ - total: 30, - displayCategory: 'ELECTRONICS', - }); - }); - }); - - // Test extra data usage - describe('extra data usage', () => { - interface Source { - price: number; - } - - interface Target { - finalPrice: number; - priceInUSD: number; - } - - interface ExtraData { - taxRate: number; - exchangeRate: number; - } - - const schema: Schema[] = [ { - to: 'finalPrice', - fn: ({ entity, extra }) => - entity.price * (1 + (extra as ExtraData).taxRate), + from: 'email', + to: 'contactEmail' }, { - to: 'priceInUSD', - fn: ({ entity, extra }) => - entity.price * (extra as ExtraData).exchangeRate, + to: 'location', + fn: ({ source }) => + `${source.address.city}, ${source.address.country}` }, - ]; - - it('should use extra data in transformations', () => { - const source: Source = { price: 100 }; - const extra: ExtraData = { - taxRate: 0.2, - exchangeRate: 1.5, - }; - - const result = mutate(schema, source, extra); - - expect(result).toEqual({ - finalPrice: 120, - priceInUSD: 150, - }); - }); - - it('should handle missing extra data', () => { - const source: Source = { price: 100 }; - - const result = mutate(schema, source); - - expect(result).toEqual({ - finalPrice: NaN, // undefined taxRate - priceInUSD: NaN, // undefined exchangeRate - }); - }); - }); - - // Test error cases - describe('error handling', () => { - interface Source { - id: number; + { + to: 'isAdult', + fn: ({ source }) => source.age >= 18 + } + ] + + const result = mutate(schema, sourceUser) + expect(result).toEqual({ + fullName: 'John Doe', + userAge: 25, + contactEmail: 'john.doe@example.com', + location: 'New York, USA', + isAdult: true + }) + }) + + it('should set null for undefined transformations', () => { + const schema: Schema[] = [ + { + to: 'optionalField', + from: 'nonexistentField' as keyof SourceUser + } + ] + + const result = mutate(schema, sourceUser) + expect(result).toEqual({ optionalField: null }) + }) + + it('should handle empty schema', () => { + const schema: Schema[] = [] + const result = mutate(schema, sourceUser) + expect(result).toEqual({}) + }) + + it('should handle null source values', () => { + const sourceWithNull = { + ...sourceUser, + email: null as unknown as string } - interface Target { - userId: number; - } + const schema: Schema>[] = [ + { from: 'email', to: 'contactEmail' } + ] - it('should handle empty schema', () => { - const source: Source = { id: 1 }; - const result = mutate([], source); - expect(result).toEqual({}); - }); - - it('should handle null/undefined entity', () => { - const schema: Schema[] = [ - { from: 'id', to: 'userId' }, - ]; - - expect(() => { - mutate(schema, null as unknown as Source); - }).toThrow(); - - expect(() => { - mutate(schema, undefined as unknown as Source); - }).toThrow(); - }); - }); -}); + const result = mutate(schema, sourceWithNull) + expect(result).toEqual({ contactEmail: null }) + }) +}) diff --git a/src/index.ts b/src/index.ts index 4f6e4e7..b91264e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,27 +3,25 @@ import { Extra, Schema } from './types' export * from './types' /** - * Transforms a source entity into a target type based on the provided schema - * @template From - The source type being mutated from - * @template To - The target type being mutated to + * Mutates an object from the Source type into the Target type based on the provided schema + * @template Source - The source type being mutated from + * @template Target - The target type being mutated to * @template TExtra - Type of additional data passed to mutation functions - * @param schema - Array of transformation rules - * @param entity - Source entity to transform + * @param schema - Array of mutation rules + * @param source - Source object to mutate * @param extra - Optional extra data to pass to mutation functions - * @returns Transformed entity matching target type + * @returns Mutated object matching Target type */ -export const mutate = ( - schema: Schema[], - entity: From, +export const mutate = ( + schema: Schema[], + source: Source, extra?: TExtra -): To => - schema.reduce((acc, rule) => { - const value = 'fn' in rule - ? rule.fn({ entity, from: 'from' in rule ? rule.from : undefined, extra }) - : entity[rule.from] - - return { +): Target => + schema.reduce( + (acc, { from, to, fn }) => ({ ...acc, - [rule.to]: value - } - }, {} as To) + [to]: fn ? fn({ source, from, extra }) : from && source[from] ? source[from] : null + }), + {} as Target + ) + diff --git a/src/types.ts b/src/types.ts index e09a312..ff5f078 100644 --- a/src/types.ts +++ b/src/types.ts @@ -7,44 +7,38 @@ export type Extra = Record * Arguments passed to a mutation function * @template From - The source type being mutated from */ -export interface MutateFnArgs { - /** The source entity being transformed */ - entity: From +export interface MutateFnArgs { + /** The source object being transformed */ + source: Source /** Optional source property key */ - from?: keyof From + from?: keyof Source /** Optional extra data to assist with transformation */ extra?: Extra } /** - * Function that performs a custom transformation on a source entity + * Function that performs a custom transformation on a source source * @template From - The source type being mutated from */ -export type MutateFn = (args: MutateFnArgs) => unknown +export type MutateFn = (args: MutateFnArgs) => unknown /** * Defines how a property should be transformed from source to target type * @template From - The source type being mutated from * @template To - The target type being mutated to */ -export type Schema = - | { +export type Schema = | { /** Target property key */ - to: keyof To + to: keyof Target /** Source property key for direct mapping */ - from: keyof From -} - | { - /** Target property key */ - to: keyof To + from: keyof Source /** Custom transformation function */ - fn: MutateFn -} - | { + fn?: MutateFn +} | { /** Target property key */ - to: keyof To - /** Source property key */ - from: keyof From - /** Custom transformation function that receives the source property value */ - fn: MutateFn + to: keyof Target + /** Source property key for direct mapping */ + from?: keyof Source + /** Custom transformation function */ + fn: MutateFn }