Skip to content

Artifact Registration Reference

Decorators mark a class as an artifact and carry its registration defaults. A generated index lists the classes. registerArtifacts binds them in dependency order during the registerArtifacts boot step. The how-to is Registering artifacts.

Files:

Quick Reference

SymbolPackageWhat it is
@injectable, @service, @component, @provide@venizia/ignis-kernel (re-exported by @venizia/ignis)Stereotype decorators and the provider method decorator
IArtifactRegistrationOptions, IArtifactMetadata, IProvideMetadata, ArtifactTypes@venizia/ignis-kernelMetadata shapes and the artifact type vocabulary
IArtifactIndex, TArtifactIndexInput, IApplicationConfigs.artifacts@venizia/ignis-kernelThe index shape and where the application receives it
registerArtifacts(), registerConfiguredArtifacts()RestApplicationRegistration from an index; the boot step
ignis-artifacts, generateArtifactIndex(), checkArtifactIndex()@venizia/ignis-bootThe generator, as a binary and as functions

ArtifactTypes

The artifact kinds a stereotype may declare.

typescript
class ArtifactTypes {
  static readonly COMPONENT = 'component';
  static readonly CONTROLLER = 'controller';
  static readonly SERVICE = 'service';
  static readonly REPOSITORY = 'repository';
  static readonly DATASOURCE = 'datasource';
  static readonly MODEL = 'model';
  static readonly SCHEME_SET: Set<string>;
  static isValid(value: string): boolean;
}

type TArtifactType = TConstValue<typeof ArtifactTypes>;

IArtifactRegistrationOptions

The five options every stereotype accepts. A stereotype stores them on the class; an explicit TMixinOpts passed to controller()/service()/... at a call site still wins.

typescript
interface IArtifactRegistrationOptions<ApplicationType = unknown> {
  binding?: { namespace: string; key: string };
  allowOverride?: boolean;
  scope?: TBindingScope;
  order?: number;
  when?: TArtifactCondition<ApplicationType>;
}

type TArtifactCondition<ApplicationType = unknown> = (opts: {
  application: ApplicationType;
}) => ValueOrPromise<boolean>;
OptionTypeDefaultMeaning
binding{ namespace: string; key: string }<namespace>.<Class>The binding key
allowOverridebooleantrue; bootChecks.binding.allowOverride when that group is setfalse makes a same-key re-registration throw instead of overwriting; true opts one registration out of the app-wide check
scopeTBindingScopeSINGLETON for datasource, component, controller; TRANSIENT for repository, serviceBinding scope
ordernumber0Lower registers first within its kind; ties keep index order
whenTArtifactConditionalways registerSync or async. Runs at the registerArtifacts step, before preConfigure; may read config and env, not another artifact's binding

@injectable

The root stereotype. Every other stereotype calls it.

typescript
const injectable: <ApplicationType = unknown>(
  opts: IArtifactMetadata<ApplicationType>,
) => ClassDecorator;

interface IArtifactMetadata<ApplicationType = unknown> extends IArtifactRegistrationOptions<ApplicationType> {
  type: TArtifactType;
}
OptionTypeDefaultMeaning
typeTArtifactTyperequiredThe artifact kind
...IArtifactRegistrationOptionsSee above

Throws at decoration time when type is not in ArtifactTypes.SCHEME_SET: [injectable][<Class>] Invalid artifact type: '<type>' | Expected one of: component, controller, service, repository, datasource, model.

typescript
@injectable({ type: ArtifactTypes.SERVICE, scope: BindingScopes.SINGLETON })
export class ClockService extends BaseService {}

@service, @component

typescript
const service: <ApplicationType = unknown>(opts?: IArtifactRegistrationOptions<ApplicationType>) => ClassDecorator;
const component: <ApplicationType = unknown>(opts?: IArtifactRegistrationOptions<ApplicationType>) => ClassDecorator;

Options: IArtifactRegistrationOptions, all optional.

typescript
@service()
export class PricingService extends BaseService {}

@component({ when: () => process.env.KAFKA_BROKERS !== undefined, order: -10 })
export class KafkaComponent extends BaseComponent {}

@controller, @repository, @datasource, @model

The four existing decorators accept IArtifactRegistrationOptions in addition to their own options, and record ArtifactTypes.CONTROLLER / REPOSITORY / DATASOURCE / MODEL through @injectable.

DecoratorOwn optionsPlus
@controllerpathbinding, allowOverride, scope, order, when
@repositorymodel, dataSourcesame
@datasourceconnector optionssame
@modeltypesame - metadata only; a model is never registered from an index
typescript
@controller({ path: '/test', when: () => process.env.NODE_ENV !== Environment.PRODUCTION })
export class TestController extends BaseRestController {}

@provide

Marks a component method as the provider of one binding key.

typescript
const provide: (opts: { key: string; scope?: TBindingScope }) => MethodDecorator;

interface IProvideMetadata {
  methodName: string | symbol;
  key: string;
  scope?: TBindingScope;
}
OptionTypeDefaultMeaning
keystringrequiredThe key to bind
scopeTBindingScopeSINGLETONScope of the provided value

When registerArtifacts registers the component, each @provide key is bound toProvider: the provider resolves the component from the container and calls the method. Nothing runs until the first get of the key.

typescript
@component()
export class PlatformComponent extends BaseComponent {
  @provide({ key: HealthCheckBindingKeys.HEALTH_CHECK_OPTIONS })
  healthCheckOptions(): IHealthCheckOptions {
    return { restOptions: { path: '/health-check' } };
  }
}

Notes:

  • Only a component registered through registerArtifacts (an index) gets its @provide keys bound. this.component(Ctor) by hand does not read them.
  • Under bun-runs-source, a return type that is an interface must come from an import type - see the guide.

IArtifactIndex, TArtifactIndexInput

typescript
interface IArtifactIndex {
  dataSources?: ReadonlyArray<TClass<IDataSource>>;
  components?: ReadonlyArray<TClass<BaseComponent>>;
  repositories?: ReadonlyArray<TClass<IRepository>>;
  services?: ReadonlyArray<TClass<IService>>;
  controllers?: ReadonlyArray<TClass<unknown>>;
}

interface IConditionalArtifactIndex {
  when: TArtifactCondition; // ({ application }) => boolean | Promise<boolean>
  index: TArtifactIndexInput;
}

type TArtifactIndexInput = IArtifactIndex | IConditionalArtifactIndex | TArtifactIndexInput[];

IApplicationConfigs.artifacts?: TArtifactIndexInput - one index, a conditional entry, or arrays of them nested to any depth. A conditional entry registers its index only when when answers true; a false answer drops the whole subtree. Use it for the run-mode gate: { when: () => runMode === 'server', index: { controllers: GeneratedArtifacts.controllers } } keeps a worker's routes out of the container. The field names are the const class ArtifactIndexFields (DATA_SOURCES, COMPONENTS, REPOSITORIES, SERVICES, CONTROLLERS, with SCHEME_SET and isValid); registerArtifacts reads the index through it, never through a string literal.

typescript
artifacts: [InventoryArtifacts, GeneratedArtifacts, { components: [HealthCheckComponent] }],

registerArtifacts

typescript
async registerArtifacts(index: TArtifactIndexInput): Promise<void>;

Behavior, in order:

  1. Flattens nested arrays into a list of IArtifactIndex.
  2. For each kind in dependency order - dataSources, components, repositories, services, controllers - collects the classes across every index.
  3. Evaluates every class's when concurrently; a false skips the class and logs at debug Skipped by condition | kind: <field> | class: <Class>.
  4. Stable-sorts the survivors by order (default 0).
  5. Registers each through dataSource() / component() / repository() / service() / controller(), which read the class's decorator defaults (binding, scope, allowOverride).
  6. For a component, binds every @provide key to a lazy provider.

A class registered by hand before this call keeps its earlier position in the binding map; the later registration overwrites the binding unless allowOverride: false, or bootChecks.binding.allowOverride: false, makes it throw.

The step binds and constructs nothing. Datasources are constructed at registerDataSources and components at registerComponents, both after preConfigure(); repositories and services are constructed on their first get (or at verifyBindings when doVerify is on). A subclass without its own decorator inherits the repository metadata (model, dataSource, read through the prototype chain) but not the artifact metadata (binding, scope, order, when, allowOverride, read as own metadata), so the generator does not list it. Decorate the subclass with a bare @repository() - it inherits model, dataSource and operationScope from the nearest decorated parent and registers under its own name - or name it in the index by hand.

registerConfiguredArtifacts

typescript
protected async registerConfiguredArtifacts(): Promise<void>;

The boot step. Calls registerArtifacts(this.configs.artifacts) when the config carries an index; does nothing otherwise.

Position in the boot sequence

BootSteps.REGISTER_ARTIFACTS ('registerArtifacts') sits between staticConfigure and preConfigure. The full BaseApplication sequence:

#Step#Step
1printStartUpInfo8registerDataSources
2validateEnvs9registerComponents
3registerDefaultMiddlewares10registerContributedDataSources
4staticConfigure11wireSecretRotatables
5registerArtifacts12registerControllers
6preConfigure13postConfigure
7hydrateSecrets14verifyBindings
15validateScopeFilterSupport

Every step logs Boot step n/15 <name> at debug. An application that inserts its own step targets these names through BootSequence.insertAfter.

bootChecks

typescript
bootChecks?: {
  binding?: { doVerify: boolean; allowManual: boolean; allowOverride: boolean };
};

One group of three binding decisions on IApplicationConfigs. Without binding, nothing is verified, and hand registration and same-key override stay allowed.

SettingWhat it doesWhen it fails
doVerify: trueThe verifyBindings step (BootSteps.VERIFY_BINDINGS, after postConfigure) resolves every binding in the services and repositories namespaces onceThrows once with every failing key: [verifyBindings] 2 binding(s) cannot be resolved | services.ReportService: Binding key: repositories.Missing is not bounded in context! | ...
allowManual: falseWhile configs.artifacts is set, a service / repository / controller / component / dataSource call inside preConfigure() or postConfigure() is refusedThrows at that call: [service] 'PricingService' is registered by hand inside preConfigure() while 'configs.artifacts' is set and 'bootChecks.binding.allowManual' is false ...
allowOverride: falseEvery artifact registration behaves as if it said allowOverride: false: a key that is already bound is refused. A registration that says allowOverride: true, on the decorator or at the call site, still overridesThrows at that registration: [service] Binding key already registered: 'services.RunModeService' | 'bootChecks.binding.allowOverride' is false ...

Resolving at boot builds the singletons then, so a constructor with a side effect runs during verifyBindings; turn doVerify on in development and UAT. Registrations made by the index step and by the framework's own steps are never counted as manual. The override setting covers the five registration methods only: bind(), set() and a @provide key never pass through it, so a key can still be rebound at runtime.

ignis-artifacts (CLI)

Shipped by @venizia/ignis-boot as a binary. Requires typescript 5 or 6 (peer ^5.0.0 || ^6.0.0; TypeScript 7 no longer exports the JS API the scanner calls) and runs under bun.

ignis-artifacts <generate|check> [--root src] [--out src/generated/artifacts.ts] [--ignore a,b] [--export GeneratedArtifacts]
FlagDefaultMeaning
--rootsrcDirectory to scan, recursively
--outsrc/generated/artifacts.tsPath of the generated file; import paths are relative to it
--ignore**/__tests__/**, **/*.test.ts, **/*.spec.ts, **/generated/**Comma-separated globs, merged with the default ignore list - never replaces it. A decorated class hidden by one of YOUR globs is named in a warning: line on stderr; the defaults stay silent
--exportGeneratedArtifactsName of the exported constant
CommandEffectExit code
generateWrites --out when its content changed; prints wrote <out> | N artifact(s) or up to date <out>0
checkRenders in memory and compares with the file; prints fresh <out> or stale <out> - run: ...0 fresh, 1 stale
anything elsePrints usage2

Default ignore list: **/__tests__/**, **/*.test.ts, **/*.spec.ts, **/generated/**.

Detection rules

A class is emitted when all of the following hold:

  • It is a named export of a .ts file under --root (not export default, not module-private).
  • It is not abstract.
  • It carries a stereotype decorator - component, controller, service, repository, datasource - imported from @venizia/ignis or @venizia/ignis-kernel. Import aliases (import { service as svc }) are resolved. A same-named decorator from another module is ignored.
  • Or it carries @injectable({ type }) where type is a string literal or ArtifactTypes.<NAME>.

@model classes are recognised and never emitted. Every skip is logged with its reason.

Output

Deterministic: imports sorted by path, class names sorted within each field, one field per kind in the order dataSources, components, repositories, services, controllers, empty arrays kept. A field wider than 100 columns wraps one name per line, so the file passes prettier -l unchanged. The header names the regenerate command.

Programmatic API

@venizia/ignis-boot/generator exports the same machinery as functions.

typescript
interface IGenerateOptions {
  root: string;
  out: string;
  ignore?: string[];
  exportName?: string; // default 'GeneratedArtifacts'
}

const generateArtifactIndex: (opts: IGenerateOptions) => {
  content: string;
  artifacts: IScannedArtifact[];
  written: boolean;
};

const checkArtifactIndex: (opts: IGenerateOptions) => {
  isFresh: boolean;
  expected: string;
  actual: string | undefined;
};

interface IScannedArtifact {
  type: TArtifactType;
  className: string;
  filePath: string;
}

class ArtifactScanner {
  static getInstance(): ArtifactScanner;
  scan(opts: { root: string; ignore?: string[] }): IScannedArtifact[];
}

class ArtifactIndexEmitter {
  static render(opts: { artifacts: IScannedArtifact[]; outFile: string; exportName: string }): string;
}
typescript
import { checkArtifactIndex } from '@venizia/ignis-boot/generator';

const { isFresh } = checkArtifactIndex({ root: 'src', out: 'src/generated/artifacts.ts' });

Removed

The deprecated runtime boot API is fully removed - see the changelog for migration.

SymbolStatus
BaseApplication.booter(), registerBooters()Removed
Bootstrapper, BaseArtifactBooter, ControllerBooter, ServiceBooter, RepositoryBooter, DatasourceBooter, BootMixin, discoverFiles(), loadClasses(), isClass()Removed from @venizia/ignis-boot
TMixinOpts.argsRemoved; TMixinOpts is { binding?, allowOverride? }

See Also