microsoft / tsyringe

Lightweight dependency injection container for JavaScript/TypeScript
MIT License
5.16k stars 173 forks source link

support PropertySource #71

Closed xenoterracide closed 4 years ago

xenoterracide commented 4 years ago

another spring/boot feature that would be nice, is "property sources", what a property source is, is a location of "properties" like foo.bar=baz I imagine this wouldn't be exactly the same for typescript, though it could be.. I imagine the source of this would be either json, or an .env, though spring core itself isn't prescriptive and only supports a few things out of the box.

{
    "foo": {
       "bar": "baz"
     },
    "my-bool": true,
}

or you could override this with a env

FOO_BAR=baz
MYBOOL=true

and if both existed the env var would override the json file.

then you could inject it as so (spring also supports type conversion)

@injectable()
class MyService {
    private _myBool: boolean = false;

    constructor ( @value('foo.bar') private fooBar: String ) {
    }
    get myBool(): boolean {
       return this._myBool;
    }
    @inject() // some marker required to know to call setter? uncertain about this...
    set myBool(@value('my-bool') value: boolean) {
        this._myBool = value;
    }
}

you'd need some sort of collection in the registry, probably something like

// this may also exist by default with the above suggested
container.register( "propertySources", { // probably a special key, possibly a special type
     propertySources: {
         env: new EnvPropertySource,
         json: new JsonPropertySource('myconfig.json')
     }
}

reference for how to use the feature

https://www.baeldung.com/properties-with-spring

and upstream documentation

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/PropertySource.html https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/env/PropertySource.html https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-external-config

I'm really not hoping for the prescriptiveness of spring boot, but maybe just simply the ability to define a collection of property sources and inject values (note: property source values are not in springs container)

probably worth noting spring boot also has this feature https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-external-config-typesafe-configuration-properties

@ConfigurationProperties("foo") // foo is the property prefix, thus my-bool wouldn't go in here
class BarProperties {
    bar: String// no need for value, uses setter or constructor injection
}
Xapphire13 commented 4 years ago

I assume the goal if this is property injection (rather than constructor)?

xenoterracide commented 4 years ago

no, you can do this on the constructor, also with setter injection (note: I disagree with direct field injection)

Xapphire13 commented 4 years ago

(note: I disagree with direct field injection)

👍

Could you give an example use case and sample code of how this may look like with TypeScript and Tsyringe?

xenoterracide commented 4 years ago

updated OP, better?

xenoterracide commented 4 years ago

here's a significantly less powerful workaround (and possibly bad) that doesn't support override of other property sources innately, or even defining ones outside of the env.

export type EnvMetaData = {
  name: string;
  optional: boolean;
};

const envMeta: Array<EnvMetaData> = [];

function env(name: string, { optional = false } = {}) {
  const meta: EnvMetaData = { name, optional };
  envMetadata().push(meta);
  return Reflect.metadata(Symbol.for('env'), meta);
}

// wrap in case we figure out how to do it with a reflection api
export function envMetadata(): Array<EnvMetaData> {
  return envMeta;
}

...

  registerEnv(): void {
    if (process.env.NODE_ENV === 'development') {
      dotenv.config();
    }

    envMetadata().forEach((meta) => {
      if (!meta.optional) {
        assert(process.env[meta.name], `${meta.name} is not set`);
      }
      this.container.register(
        Symbol.for(meta.name),
        { useValue: process.env[meta.name] },
      );
    });

  }

used like

  @env('STRIPE_PUBLISHABLE_KEY')
  static readonly StripePk = Symbol.for('STRIPE_PUBLISHABLE_KEY');

p.s. if you know how to fix this so it gets it from metadata that'd be great! or if there's a way to not duplicate the STRIPE_PUBLISHABLE_KEY part

xenoterracide commented 4 years ago

@Xapphire13 interest in patches for this using this @env?

MeltingMosaic commented 4 years ago

@xenoterracide, does the new Transform (@injectWithTransform) functionality fix this issue for you? You could create a service that exposes the environment, and then a transformer that takes the environment object and selects a value from it.

xenoterracide commented 4 years ago

eh, I kinda switched from tsyringe a while ago.