Class-transformer: question: defaultMetadataStorage is no more available in 0.3.2

Created on 19 Jan 2021  路  14Comments  路  Source: typestack/class-transformer

Description

In the previous version it was possible to access to the _defaultMetadataStorage_ which was useful to create custom transformer.

for example :
Minimal code-snippet showcasing the problem


import { TransformOptions } from 'class-transformer';
import { defaultMetadataStorage } from 'class-transformer/storage/'; <== cannot find module anymore

export function doSplit(separator: string | RegExp = ' ') {
  return (value: any) => {
    return value !== undefined && value !== null
      ? String(value).split(separator)
      : [];
  };
}
export function Split(
  separator: string | RegExp,
  options: TransformOptions = {},
): PropertyDecorator {
  const transformFn = doSplit(separator);

  return function (target: any, propertyName: string | symbol): void {
    defaultMetadataStorage.addTransformMetadata({
      target: target.constructor,
      propertyName: propertyName as string,
      transformFn,
      options,
    });
  };
}

Expected behavior

Get the _defaultMetadataStorage_ !

Actual behavior

"Cannot find module 'class-transformer/storage' or its corresponding type declarations."

question

Most helpful comment

Nestjs :)

In fact, nobody needs to access your internal interface. The only thing we want is the ability to add dynamic Transformer.

That is misleading.
I'd argue that everybody who has an advanced use-case absolutely needs access to the internal interface because there is no other way to do that.

E.g. we need access to the column-mapping somehow.
Of course, we would also prefer a public, stable and documented way to do this (instead of accessing internal private parts, that can change without notice between releases).
But we do understand that this is an advanced use-case and we don't expect the library authors to invest lots of effort, just to cover exotic use-cases.

All 14 comments

You should be able to reach it from class-transformer/MetadataStorage.ts, but keep in mind that this is an internal class, and can break anytime.

Hi, we used defaultMetadataStorage to extract all applied validations so we could construct our OpenAPI schema on the fly. I understand that this is a "private" class of sorts, but we're ready to adapt to any changes, as there is no other way for us to automate OpenAPI generation.

_edit 1:_
Strangely, source code doesn't seem to be changed, but imports don't work the same way. Could it be because of changes in tsconfig.json?

_edit 2:_
Found a workaround, @Arnaud-Dev-Nodejs you may want to try this.
import { defaultMetadataStorage } from 'class-transformer/cjs/storage';

Looks like new release changed the npm library definitions to use cjs and other formats to support library package formats. Looks like this is also breaking the package class-validate-jsonschema since it鈥檚 also referencing defaultMetadataStorage...

Strangely, source code doesn't seem to be changed, but imports don't work the same way. Could it be because of changes in tsconfig.json?

Yes, we move to the universal package format for each TypeStack package. However, this should not break your workflow as your build tool should be able to understand this as we reference each entry point properly. https://github.com/typestack/class-transformer/blob/ab093a355be420978273b675d3bbd3a520d1918d/package.json#L9-L12

Can I ask what build tools you use @Igoreso?

Trying @Igoreso 's workaround produces this error:

Could not find a declaration file for module 'class-transformer/cjs/storage'. './node_modules/class-transformer/cjs/storage.js' implicitly has an 'any' type.
  Try `npm i --save-dev @types/class-transformer` if it exists or add a new declaration (.d.ts) file containing `declare module 'class-transformer/cjs/storage';`ts(7016)

I am also not able to see why the "storage" module has vanished from the typescript detection. maybe only the index is not exposed, and defaultMetadataStorage should just be re-exported from the index module? maybe named so it wont break with future releases?

maybe only the index is not exposed, and defaultMetadataStorage should just be re-exported from the index module?

It won't be re-exported, because it's an internal part of the lib and we don't support its API officially. I won't actively limit the access to it, but it will be never included in the barrel export to indicate that is not an official API.

so it wont break with future releases?

It's an internal part of the lib, it will break with future releases.

What we can do here is update the project config if needed to allow importing it directly with every build tool from its direct path. For that, I will need what build tool cannot find it.

Nestjs :)

In fact, nobody needs to access your internal interface. The only thing we want is the ability to add dynamic Transformer.

Nestjs :)

In fact, nobody needs to access your internal interface. The only thing we want is the ability to add dynamic Transformer.

That is misleading.
I'd argue that everybody who has an advanced use-case absolutely needs access to the internal interface because there is no other way to do that.

E.g. we need access to the column-mapping somehow.
Of course, we would also prefer a public, stable and documented way to do this (instead of accessing internal private parts, that can change without notice between releases).
But we do understand that this is an advanced use-case and we don't expect the library authors to invest lots of effort, just to cover exotic use-cases.

same problem here, updating to 0.3.2 breaks the import.
using require syntax instead of imports works though.
Node version 14

// import { defaultMetadataStorage as classTransformerDefaultMetadataStorage, } from "class-transformer/storage";
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
const classTransformerDefaultMetadataStorage = require(`class-transformer/storage`);

You can also import the ES module as import { defaultMetadataStorage } from 'class-transformer/esm5/storage' and then provide types separately with a .d.ts declaration like

declare module 'class-transformer/esm5/storage' {
  import type { MetadataStorage } from 'class-transformer/types/MetadataStorage';

  export const defaultMetadataStorage: MetadataStorage;
}

This is a bit of a hassle though: exposing defaultMetadataStorage or another API for accessing metadata objects would make it much easier to develop libraries that integrate with class-transformer. Note that metadata object types are already part of the public API.

Just wondering @NoNameProvided why you wouldn't want to expose it?
There are needs it seems from this thread.

One I can contribute to is we are testing our Models to make sure the dto (domain transfer object) don't change
Here is the class to do those validation

/* eslint-disable @typescript-eslint/ban-types */
import { ExcludeOptions, ExposeOptions } from "class-transformer";
import { ExcludeMetadata, ExposeMetadata, TypeMetadata } from "class-transformer";
// import { defaultMetadataStorage as classTransformerDefaultMetadataStorage } from "class-transformer/types/storage";
const classTransformerDefaultMetadataStorage = require(`class-transformer/storage`);
import { getMetadataStorage, ValidationOptions, ValidationTypes } from "class-validator";
import { ConstraintMetadata } from "class-validator/types/metadata/ConstraintMetadata";
import { ValidationMetadata } from "class-validator/types/metadata/ValidationMetadata";

export class ValidationConstraintUtils
{
    public static TestExposeMetadata(target: Function, propertyName?: string, options: ExposeOptions = {}): void
    {
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
        // @ts-ignore
        const exposeMetadata: ExposeMetadata = classTransformerDefaultMetadataStorage.findExposeMetadata(target, propertyName);
        expect(exposeMetadata).toBeDefined();
        expect(exposeMetadata.options).toEqual(options);
    }

    public static TestNotExposeMetadata(target: Function, propertyName?: string, options: ExcludeOptions = {}): void
    {
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
        // @ts-ignore
        const exposeMetadata: ExposeMetadata | undefined = classTransformerDefaultMetadataStorage.findExposeMetadata(target, propertyName);
        expect(exposeMetadata).toBeUndefined();
    }

    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-ignore
    public static TestExcludeMetadata(target: Function, propertyName?: string, options: ExcludeMetadata = {}): void
    {
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
        // @ts-ignore
        const excludeMetadata: ExcludeMetadata = classTransformerDefaultMetadataStorage.findExcludeMetadata(target, propertyName);
        expect(excludeMetadata).toBeDefined();
        expect(excludeMetadata.options).toEqual(options);
    }

    public static TestTypeMetadata(target: Function, propertyName: string, type: Function): void
    {
        const typeMetadata: TypeMetadata = classTransformerDefaultMetadataStorage.findTypeMetadata(target, propertyName);
        expect(typeMetadata).toBeDefined();
        expect(typeMetadata.typeFunction()).toBe(type);
    }

    public static TestValidation(target: Function, propertyName: string, validationNames: ValidationTypes[]): void
    {
        const validationChecks: Array<Partial<ValidationMetadata>> = [];
        for (const validationName of validationNames)
        {
            validationChecks.push(expect.objectContaining({
                propertyName: propertyName,
                type: validationName
            }));
        }

        const validation: ValidationMetadata[] = getMetadataStorage().getTargetValidationMetadatas(target, ``, true, false).filter((value: ValidationMetadata) => value.propertyName === propertyName && value.type !== `customValidation`);
        expect(validation).toEqual(
            expect.arrayContaining(validationChecks)
        );
    }

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    public static TestConstraints(target: Function, propertyName: string, constraints: Array<{ name: string; options?: any[]; validationOptions?: ValidationOptions }>): void
    {
        const validationMetadatas: ValidationMetadata[] = getMetadataStorage().getTargetValidationMetadatas(target, ``, true, false)
            .filter((value: ValidationMetadata) => value.propertyName === propertyName && value.type === `customValidation`);

        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        const constraintMetadatas: ConstraintMetadata[] = (getMetadataStorage() as any).constraintMetadatas
            .filter((constraint: ConstraintMetadata) => validationMetadatas.find((validation: ValidationMetadata) => validation.constraintCls === constraint.target));

        const constraintsCheck: Array<Partial<ConstraintMetadata>> = [];
        for (const constraint of constraints)
        {
            const constraintMetadata: ConstraintMetadata | undefined = constraintMetadatas.find((c: ConstraintMetadata) => c.name === constraint.name);
            expect(constraintMetadata).toBeDefined();

            const validationMetadata: ValidationMetadata | undefined = validationMetadatas.find((v: ValidationMetadata) => v.constraintCls === constraintMetadata?.target);
            expect(validationMetadata).toBeDefined();

            if (constraint.validationOptions)
            {
                expect(validationMetadata).toEqual(
                    expect.objectContaining(constraint.validationOptions)
                );
            }

            constraintsCheck.push(expect.objectContaining({name: constraint.name}));

            if (constraint.options)
            {
                expect(validationMetadata?.constraints.length).toBe(constraint.options.length);
                for (let i = 0; i < constraint.options.length; i++)
                {
                    expect(validationMetadata?.constraints[i]).toEqual(constraint.options[i]);
                }
            }
        }

        expect(constraintMetadatas).toEqual(
            expect.arrayContaining(constraintsCheck)
        );
    }
}

and here are test using it

    test(`paramOptions`, async() =>
    {
        ValidationConstraintUtils.TestExposeMetadata(target, `paramOptions`, {name: `param_options`});
        ValidationConstraintUtils.TestValidation(target, `paramOptions`, [ValidationTypes.CONDITIONAL_VALIDATION, ValidationTypes.NESTED_VALIDATION]);
    });

acutally importing it as I shown above failed today.
Another way was this
import { defaultMetadataStorage as classTransformerDefaultMetadataStorage } from "class-transformer/cjs/storage";
with aliasing, otherwise it doesn't get picked...
This is super weird...

TS 4.1.2
Node 14
Using ts-node latest

Yeah, this is a tooling bug with the new format, I am kind of out of depth here, so I will ask about this on SO and hope someone knows whats up.

Was this page helpful?
0 / 5 - 0 ratings