opinionated-machine 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Igor Savin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,139 @@
1
+ # opinionated-machine
2
+ Very opinionated DI framework for fastify, built on top of awilix
3
+
4
+ ## Basic usage
5
+
6
+ Define a module, or several modules, that will be used for resolving dependency graphs, using awilix:
7
+
8
+ ```ts
9
+ import { AbstractModule, asSingletonClass, asMessageQueueHandlerClass, asJobWorkerClass, asJobQueueClass, asControllerClass } from 'opinionated-machine'
10
+
11
+ export type ModuleDependencies = {
12
+ service: Service
13
+ messageQueueConsumer: MessageQueueConsumer
14
+ jobWorker: JobWorker
15
+ queueManager: QueueManager
16
+ }
17
+
18
+ export class MyModule extends AbstractModule<ModuleDependencies, ExternalDependencies> {
19
+ resolveDependencies(
20
+ diOptions: DependencyInjectionOptions,
21
+ _externalDependencies: ExternalDependencies,
22
+ ): MandatoryNameAndRegistrationPair<ModuleDependencies> {
23
+ return {
24
+ service: asSingletonClass(Service),
25
+
26
+ // by default init and disposal methods from `message-queue-toolkit` consumers
27
+ // will be assumed. If different values are necessary, pass second config object
28
+ // and specify "asyncInit" and "asyncDispose" fields
29
+ messageQueueConsumer: asMessageQueueHandlerClass(MessageQueueConsumer, {
30
+ queueName: MessageQueueConsumer.QUEUE_ID,
31
+ diOptions,
32
+ }),
33
+
34
+ // by default init and disposal methods from `background-jobs-commons` job workers
35
+ // will be assumed. If different values are necessary, pass second config object
36
+ // and specify "asyncInit" and "asyncDispose" fields
37
+ jobWorker: asJobWorkerClass(JobWorker, {
38
+ queueName: JobWorker.QUEUE_ID,
39
+ diOptions,
40
+ }),
41
+
42
+ // by default disposal methods from `background-jobs-commons` job queue manager
43
+ // will be assumed. If different values are necessary, specify "asyncDispose" fields
44
+ // in the second config object
45
+ queueManager: asJobQueueClass(
46
+ QueueManager,
47
+ {
48
+ diOptions,
49
+ },
50
+ {
51
+ asyncInit: (manager) => manager.start(resolveJobQueuesEnabled(options)),
52
+ },
53
+ ),
54
+ }
55
+ }
56
+
57
+ // controllers will be automatically registered on fastify app
58
+ resolveControllers() {
59
+ return {
60
+ controller: asControllerClass(MyController),
61
+ }
62
+ }
63
+ }
64
+ ```
65
+
66
+ ## Defining controllers
67
+
68
+ Controllers require using fastify-api-contracts and allow to define application routes.
69
+
70
+ ```ts
71
+ import { buildFastifyNoPayloadRoute } from '@lokalise/fastify-api-contracts'
72
+ import { buildDeleteRoute } from '@lokalise/universal-ts-utils/api-contracts/apiContracts'
73
+ import { z } from 'zod'
74
+ import { AbstractController } from 'opinionated-machine'
75
+
76
+ const BODY_SCHEMA = z.object({})
77
+ const PATH_PARAMS_SCHEMA = z.object({
78
+ userId: z.string(),
79
+ })
80
+
81
+ const contract = buildDeleteRoute({
82
+ successResponseBodySchema: BODY_SCHEMA,
83
+ requestPathParamsSchema: PATH_PARAMS_SCHEMA,
84
+ pathResolver: (pathParams) => `/users/${pathParams.userId}`,
85
+ })
86
+
87
+ export class MyController extends AbstractController<typeof MyController.contracts> {
88
+ public static contracts = { deleteItem: contract } as const
89
+
90
+ public buildRoutes() {
91
+ return {
92
+ deleteItem: buildFastifyNoPayloadRoute(
93
+ MyController.contracts.deleteItem,
94
+ async (req, reply) => {
95
+ req.log.info(req.params.userId)
96
+ await reply.status(204).send()
97
+ },
98
+ ),
99
+ }
100
+ }
101
+ }
102
+ ```
103
+
104
+ ## Putting it all together
105
+
106
+ Typical usage with a fastify app looks like this:
107
+
108
+ ```ts
109
+ import { serializerCompiler, validatorCompiler } from 'fastify-type-provider-zod'
110
+ import { createContainer } from 'awilix'
111
+ import { fastify } from 'fastify'
112
+ import { DIContext } from 'opinionated-machine'
113
+
114
+ const module = new MyModule()
115
+ const container = createContainer({
116
+ injectionMode: 'PROXY',
117
+ })
118
+
119
+ const context = new DIContext<ModuleDependencies>(container, {
120
+ messageQueueConsumersEnabled: [MessageQueueConsumer.QUEUE_ID],
121
+ jobQueuesEnabled: false,
122
+ jobWorkersEnabled: false,
123
+ periodicJobsEnabled: false,
124
+ })
125
+
126
+ context.registerDependencies({
127
+ modules: [module],
128
+ {} // dependency overrides if necessary, usually for testing purposes
129
+ })
130
+
131
+ const app = fastify()
132
+ app.setValidatorCompiler(validatorCompiler)
133
+ app.setSerializerCompiler(serializerCompiler)
134
+
135
+ app.after(() => {
136
+ context.registerRoutes(app)
137
+ })
138
+ await app.ready()
139
+ ```
@@ -0,0 +1,7 @@
1
+ export { DIContext, type DependencyInjectionOptions, type registerDependenciesParams, } from './lib/DIContext.js';
2
+ export { AbstractModule } from './lib/AbstractModule.js';
3
+ export { ENABLE_ALL, resolveJobQueuesEnabled, isJobQueueEnabled, isMessageQueueConsumerEnabled, isJobWorkersEnabled, } from './lib/diConfigUtils.js';
4
+ export { AbstractController } from './lib/AbstractController.js';
5
+ export { asJobQueueClass, asJobWorkerClass, asMessageQueueHandlerClass, asControllerClass, asSingletonClass, } from './lib/resolverFunctions.js';
6
+ export type { JobQueueModuleOptions, MessageQueueConsumerModuleOptions, JobWorkerModuleOptions, } from './lib/resolverFunctions.js';
7
+ export type { InferRequestFromContract } from './lib/typeUtils.js';
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { DIContext, } from './lib/DIContext.js';
2
+ export { AbstractModule } from './lib/AbstractModule.js';
3
+ export { ENABLE_ALL, resolveJobQueuesEnabled, isJobQueueEnabled, isMessageQueueConsumerEnabled, isJobWorkersEnabled, } from './lib/diConfigUtils.js';
4
+ export { AbstractController } from './lib/AbstractController.js';
5
+ export { asJobQueueClass, asJobWorkerClass, asMessageQueueHandlerClass, asControllerClass, asSingletonClass, } from './lib/resolverFunctions.js';
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,GAGV,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AACxD,OAAO,EACL,UAAU,EACV,uBAAuB,EACvB,iBAAiB,EACjB,6BAA6B,EAC7B,mBAAmB,GACpB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAA;AAChE,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,0BAA0B,EAC1B,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,4BAA4B,CAAA"}
@@ -0,0 +1,5 @@
1
+ import type { RouteType } from '@lokalise/fastify-api-contracts';
2
+ import type { DeleteRouteDefinition, GetRouteDefinition, PayloadRouteDefinition } from '@lokalise/universal-ts-utils/api-contracts/apiContracts';
3
+ export declare abstract class AbstractController<APIContracts extends Record<string, DeleteRouteDefinition<any, any, any> | GetRouteDefinition<any, any, any> | PayloadRouteDefinition<any, any, any>>> {
4
+ abstract buildRoutes(): Record<keyof APIContracts, RouteType>;
5
+ }
@@ -0,0 +1,3 @@
1
+ export class AbstractController {
2
+ }
3
+ //# sourceMappingURL=AbstractController.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AbstractController.js","sourceRoot":"","sources":["../../lib/AbstractController.ts"],"names":[],"mappings":"AAOA,MAAM,OAAgB,kBAAkB;CAYvC"}
@@ -0,0 +1,9 @@
1
+ import type { Resolver } from 'awilix';
2
+ import type { DependencyInjectionOptions } from './DIContext.js';
3
+ export type MandatoryNameAndRegistrationPair<T> = {
4
+ [U in keyof T]: Resolver<T[U]>;
5
+ };
6
+ export declare abstract class AbstractModule<ModuleDependencies, ExternalDependencies = never> {
7
+ abstract resolveDependencies(diOptions?: DependencyInjectionOptions, externalDependencies?: ExternalDependencies): MandatoryNameAndRegistrationPair<ModuleDependencies>;
8
+ abstract resolveControllers(): MandatoryNameAndRegistrationPair<unknown>;
9
+ }
@@ -0,0 +1,3 @@
1
+ export class AbstractModule {
2
+ }
3
+ //# sourceMappingURL=AbstractModule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AbstractModule.js","sourceRoot":"","sources":["../../lib/AbstractModule.ts"],"names":[],"mappings":"AAOA,MAAM,OAAgB,cAAc;CAOnC"}
@@ -0,0 +1,26 @@
1
+ import type { AwilixContainer, NameAndRegistrationPair } from 'awilix';
2
+ import { AwilixManager } from 'awilix-manager';
3
+ import type { FastifyInstance } from 'fastify';
4
+ import type { AbstractModule } from './AbstractModule.js';
5
+ import type { ENABLE_ALL } from './diConfigUtils.js';
6
+ export type registerDependenciesParams<Dependencies, ExternalDependencies> = {
7
+ modules: readonly AbstractModule<unknown, ExternalDependencies>[];
8
+ dependencyOverrides?: NameAndRegistrationPair<Dependencies>;
9
+ };
10
+ export type DependencyInjectionOptions = {
11
+ jobQueuesEnabled?: false | typeof ENABLE_ALL | string[];
12
+ jobWorkersEnabled?: false | typeof ENABLE_ALL | string[];
13
+ messageQueueConsumersEnabled?: false | typeof ENABLE_ALL | string[];
14
+ periodicJobsEnabled?: false | typeof ENABLE_ALL;
15
+ };
16
+ export declare class DIContext<Dependencies extends object, ExternalDependencies = never> {
17
+ private readonly options;
18
+ readonly awilixManager: AwilixManager;
19
+ readonly diContainer: AwilixContainer<Dependencies>;
20
+ private readonly controllerResolvers;
21
+ constructor(diContainer: AwilixContainer, options: DependencyInjectionOptions);
22
+ registerDependencies(params: registerDependenciesParams<Dependencies, ExternalDependencies>, externalDependencies?: ExternalDependencies): void;
23
+ registerRoutes(app: FastifyInstance): void;
24
+ destroy(): Promise<void>;
25
+ init(): Promise<void>;
26
+ }
@@ -0,0 +1,62 @@
1
+ import { AwilixManager } from 'awilix-manager';
2
+ export class DIContext {
3
+ options;
4
+ awilixManager;
5
+ diContainer;
6
+ // biome-ignore lint/suspicious/noExplicitAny: all controllers are controllers
7
+ controllerResolvers;
8
+ constructor(diContainer, options) {
9
+ this.options = options;
10
+ this.diContainer = diContainer;
11
+ this.awilixManager = new AwilixManager({
12
+ asyncDispose: true,
13
+ asyncInit: true,
14
+ diContainer,
15
+ eagerInject: true,
16
+ strictBooleanEnforced: true,
17
+ });
18
+ this.controllerResolvers = [];
19
+ }
20
+ registerDependencies(params, externalDependencies) {
21
+ const _dependencyOverrides = params.dependencyOverrides ?? {};
22
+ const diConfig = {};
23
+ for (const module of params.modules) {
24
+ const resolvedDIConfig = module.resolveDependencies(this.options, externalDependencies);
25
+ for (const key in resolvedDIConfig) {
26
+ // @ts-expect-error we can't really ensure type-safety here
27
+ diConfig[key] = resolvedDIConfig[key];
28
+ }
29
+ this.controllerResolvers.push(...Object.values(module.resolveControllers()));
30
+ }
31
+ this.diContainer.register(diConfig);
32
+ for (const [dependencyKey, _dependencyValue] of Object.entries(_dependencyOverrides)) {
33
+ const dependencyValue = { ..._dependencyValue };
34
+ // preserve lifetime from original resolver
35
+ const originalResolver = this.diContainer.getRegistration(dependencyKey);
36
+ // @ts-ignore
37
+ if (dependencyValue.lifetime !== originalResolver.lifetime) {
38
+ // @ts-ignore
39
+ dependencyValue.lifetime = originalResolver.lifetime;
40
+ }
41
+ this.diContainer.register(dependencyKey, dependencyValue);
42
+ }
43
+ }
44
+ registerRoutes(app) {
45
+ for (const controllerResolver of this.controllerResolvers) {
46
+ // biome-ignore lint/suspicious/noExplicitAny: any controller works here
47
+ const controller = controllerResolver.resolve(this.diContainer);
48
+ const routes = controller.buildRoutes();
49
+ for (const route of Object.values(routes)) {
50
+ app.route(route);
51
+ }
52
+ }
53
+ }
54
+ async destroy() {
55
+ await this.awilixManager.executeDispose();
56
+ await this.diContainer.dispose();
57
+ }
58
+ async init() {
59
+ await this.awilixManager.executeInit();
60
+ }
61
+ }
62
+ //# sourceMappingURL=DIContext.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DIContext.js","sourceRoot":"","sources":["../../lib/DIContext.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAkB9C,MAAM,OAAO,SAAS;IACH,OAAO,CAA4B;IACpC,aAAa,CAAe;IAC5B,WAAW,CAA+B;IAC1D,8EAA8E;IAC7D,mBAAmB,CAAiB;IAErD,YAAY,WAA4B,EAAE,OAAmC;QAC3E,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;QACtB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAA;QAC9B,IAAI,CAAC,aAAa,GAAG,IAAI,aAAa,CAAC;YACrC,YAAY,EAAE,IAAI;YAClB,SAAS,EAAE,IAAI;YACf,WAAW;YACX,WAAW,EAAE,IAAI;YACjB,qBAAqB,EAAE,IAAI;SAC5B,CAAC,CAAA;QACF,IAAI,CAAC,mBAAmB,GAAG,EAAE,CAAA;IAC/B,CAAC;IAED,oBAAoB,CAClB,MAAsE,EACtE,oBAA2C;QAE3C,MAAM,oBAAoB,GAAG,MAAM,CAAC,mBAAmB,IAAI,EAAE,CAAA;QAC7D,MAAM,QAAQ,GAA0C,EAAE,CAAA;QAE1D,KAAK,MAAM,MAAM,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACpC,MAAM,gBAAgB,GAAG,MAAM,CAAC,mBAAmB,CAAC,IAAI,CAAC,OAAO,EAAE,oBAAoB,CAAC,CAAA;YAEvF,KAAK,MAAM,GAAG,IAAI,gBAAgB,EAAE,CAAC;gBACnC,2DAA2D;gBAC3D,QAAQ,CAAC,GAAG,CAAC,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAA;YACvC,CAAC;YAED,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAC3B,GAAI,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,kBAAkB,EAAE,CAAyB,CACvE,CAAA;QACH,CAAC;QACD,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAA;QAEnC,KAAK,MAAM,CAAC,aAAa,EAAE,gBAAgB,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,oBAAoB,CAAC,EAAE,CAAC;YACrF,MAAM,eAAe,GAAG,EAAE,GAAI,gBAAsC,EAAE,CAAA;YAEtE,2CAA2C;YAC3C,MAAM,gBAAgB,GAAG,IAAI,CAAC,WAAW,CAAC,eAAe,CAAC,aAAa,CAAC,CAAA;YACxE,aAAa;YACb,IAAI,eAAe,CAAC,QAAQ,KAAK,gBAAgB,CAAC,QAAQ,EAAE,CAAC;gBAC3D,aAAa;gBACb,eAAe,CAAC,QAAQ,GAAG,gBAAgB,CAAC,QAAQ,CAAA;YACtD,CAAC;YAED,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,aAAa,EAAE,eAAe,CAAC,CAAA;QAC3D,CAAC;IACH,CAAC;IAED,cAAc,CAAC,GAAoB;QACjC,KAAK,MAAM,kBAAkB,IAAI,IAAI,CAAC,mBAAmB,EAAE,CAAC;YAC1D,wEAAwE;YACxE,MAAM,UAAU,GAA4B,kBAAkB,CAAC,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;YACxF,MAAM,MAAM,GAAG,UAAU,CAAC,WAAW,EAAE,CAAA;YACvC,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC1C,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;YAClB,CAAC;QACH,CAAC;IACH,CAAC;IAED,KAAK,CAAC,OAAO;QACX,MAAM,IAAI,CAAC,aAAa,CAAC,cAAc,EAAE,CAAA;QACzC,MAAM,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,CAAA;IAClC,CAAC;IAED,KAAK,CAAC,IAAI;QACR,MAAM,IAAI,CAAC,aAAa,CAAC,WAAW,EAAE,CAAA;IACxC,CAAC;CACF"}
@@ -0,0 +1,6 @@
1
+ import type { DependencyInjectionOptions } from './DIContext.js';
2
+ export declare const ENABLE_ALL: unique symbol;
3
+ export declare const resolveJobQueuesEnabled: (options: DependencyInjectionOptions) => boolean | string[];
4
+ export declare const isJobWorkersEnabled: (enabled?: false | typeof ENABLE_ALL | string[], name?: string) => boolean;
5
+ export declare const isJobQueueEnabled: (enabled?: false | typeof ENABLE_ALL | string[], name?: string) => boolean;
6
+ export declare const isMessageQueueConsumerEnabled: (messageQueueConsumersEnabled?: false | typeof ENABLE_ALL | string[], name?: string) => boolean;
@@ -0,0 +1,41 @@
1
+ export const ENABLE_ALL = Symbol.for('ENABLE_ALL');
2
+ export const resolveJobQueuesEnabled = (options) => {
3
+ const { jobQueuesEnabled } = options;
4
+ if (!jobQueuesEnabled) {
5
+ return false;
6
+ }
7
+ if (jobQueuesEnabled === ENABLE_ALL) {
8
+ return true;
9
+ }
10
+ if (Array.isArray(jobQueuesEnabled)) {
11
+ return jobQueuesEnabled.length ? jobQueuesEnabled : false;
12
+ }
13
+ return false;
14
+ };
15
+ export const isJobWorkersEnabled = (enabled, name) => isEnabled(enabled, name);
16
+ export const isJobQueueEnabled = (enabled, name) => {
17
+ if (!enabled) {
18
+ return false;
19
+ }
20
+ if (Array.isArray(enabled) && (!name || enabled.includes(name))) {
21
+ return true;
22
+ }
23
+ if (enabled === ENABLE_ALL) {
24
+ return true;
25
+ }
26
+ return false;
27
+ };
28
+ export const isMessageQueueConsumerEnabled = (messageQueueConsumersEnabled, name) => isEnabled(messageQueueConsumersEnabled, name);
29
+ const isEnabled = (option, name) => {
30
+ if (!option) {
31
+ return false;
32
+ }
33
+ if (name && Array.isArray(option)) {
34
+ return option.includes(name);
35
+ }
36
+ if (option === ENABLE_ALL) {
37
+ return true;
38
+ }
39
+ return false;
40
+ };
41
+ //# sourceMappingURL=diConfigUtils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diConfigUtils.js","sourceRoot":"","sources":["../../lib/diConfigUtils.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,UAAU,GAAG,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,CAAA;AAElD,MAAM,CAAC,MAAM,uBAAuB,GAAG,CACrC,OAAmC,EACf,EAAE;IACtB,MAAM,EAAE,gBAAgB,EAAE,GAAG,OAAO,CAAA;IACpC,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACtB,OAAO,KAAK,CAAA;IACd,CAAC;IACD,IAAI,gBAAgB,KAAK,UAAU,EAAE,CAAC;QACpC,OAAO,IAAI,CAAA;IACb,CAAC;IAED,IAAI,KAAK,CAAC,OAAO,CAAC,gBAAgB,CAAC,EAAE,CAAC;QACpC,OAAO,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,KAAK,CAAA;IAC3D,CAAC;IAED,OAAO,KAAK,CAAA;AACd,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,mBAAmB,GAAG,CACjC,OAA8C,EAC9C,IAAa,EACJ,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;AAEtC,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAC/B,OAA8C,EAC9C,IAAa,EACJ,EAAE;IACX,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,KAAK,CAAA;IACd,CAAC;IAED,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;QAChE,OAAO,IAAI,CAAA;IACb,CAAC;IAED,IAAI,OAAO,KAAK,UAAU,EAAE,CAAC;QAC3B,OAAO,IAAI,CAAA;IACb,CAAC;IAED,OAAO,KAAK,CAAA;AACd,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,6BAA6B,GAAG,CAC3C,4BAAmE,EACnE,IAAa,EACJ,EAAE,CAAC,SAAS,CAAC,4BAA4B,EAAE,IAAI,CAAC,CAAA;AAE3D,MAAM,SAAS,GAAG,CAChB,MAAwD,EACxD,IAAa,EACJ,EAAE;IACX,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,KAAK,CAAA;IACd,CAAC;IACD,IAAI,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAClC,OAAO,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAA;IAC9B,CAAC;IAED,IAAI,MAAM,KAAK,UAAU,EAAE,CAAC;QAC1B,OAAO,IAAI,CAAA;IACb,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC,CAAA"}
@@ -0,0 +1,19 @@
1
+ import type { BuildResolver, BuildResolverOptions, Constructor, DisposableResolver } from 'awilix';
2
+ import type { DependencyInjectionOptions } from './DIContext.js';
3
+ export declare function asSingletonClass<T = object>(Type: Constructor<T>, opts?: BuildResolverOptions<T>): BuildResolver<T> & DisposableResolver<T>;
4
+ export declare function asControllerClass<T = object>(Type: Constructor<T>, opts?: BuildResolverOptions<T>): BuildResolver<T> & DisposableResolver<T>;
5
+ export type MessageQueueConsumerModuleOptions = {
6
+ queueName: string;
7
+ diOptions: DependencyInjectionOptions;
8
+ };
9
+ export declare function asMessageQueueHandlerClass<T = object>(Type: Constructor<T>, mqOptions: MessageQueueConsumerModuleOptions, opts?: BuildResolverOptions<T>): BuildResolver<T> & DisposableResolver<T>;
10
+ export type JobWorkerModuleOptions = {
11
+ queueName: string;
12
+ diOptions: DependencyInjectionOptions;
13
+ };
14
+ export declare function asJobWorkerClass<T = object>(Type: Constructor<T>, workerOptions: JobWorkerModuleOptions, opts?: BuildResolverOptions<T>): BuildResolver<T> & DisposableResolver<T>;
15
+ export type JobQueueModuleOptions = {
16
+ queueName?: string;
17
+ diOptions: DependencyInjectionOptions;
18
+ };
19
+ export declare function asJobQueueClass<T = object>(Type: Constructor<T>, queueOptions: JobQueueModuleOptions, opts?: BuildResolverOptions<T>): BuildResolver<T> & DisposableResolver<T>;
@@ -0,0 +1,48 @@
1
+ import { asClass } from 'awilix';
2
+ import { isJobQueueEnabled, isJobWorkersEnabled, isMessageQueueConsumerEnabled, } from './diConfigUtils.js';
3
+ export function asSingletonClass(Type, opts) {
4
+ return asClass(Type, {
5
+ ...opts,
6
+ lifetime: 'SINGLETON',
7
+ });
8
+ }
9
+ export function asControllerClass(Type, opts) {
10
+ return asClass(Type, {
11
+ ...opts,
12
+ lifetime: 'SINGLETON',
13
+ });
14
+ }
15
+ export function asMessageQueueHandlerClass(Type, mqOptions, opts) {
16
+ return asClass(Type, {
17
+ // these follow message-queue-toolkit conventions
18
+ asyncInit: 'start',
19
+ asyncDispose: 'close',
20
+ asyncDisposePriority: 10,
21
+ enabled: isMessageQueueConsumerEnabled(mqOptions.diOptions.messageQueueConsumersEnabled, mqOptions.queueName),
22
+ lifetime: 'SINGLETON',
23
+ ...opts,
24
+ });
25
+ }
26
+ export function asJobWorkerClass(Type, workerOptions, opts) {
27
+ return asClass(Type, {
28
+ // these follow background-jobs-common conventions
29
+ asyncInit: 'start',
30
+ asyncDispose: 'dispose',
31
+ asyncDisposePriority: 10,
32
+ enabled: isJobWorkersEnabled(workerOptions.diOptions.jobWorkersEnabled, workerOptions.queueName),
33
+ lifetime: 'SINGLETON',
34
+ ...opts,
35
+ });
36
+ }
37
+ export function asJobQueueClass(Type, queueOptions, opts) {
38
+ return asClass(Type, {
39
+ // these follow background-jobs-common conventions
40
+ asyncInit: 'start',
41
+ asyncDispose: 'dispose',
42
+ asyncDisposePriority: 20,
43
+ enabled: isJobQueueEnabled(queueOptions.diOptions.jobQueuesEnabled, queueOptions.queueName),
44
+ lifetime: 'SINGLETON',
45
+ ...opts,
46
+ });
47
+ }
48
+ //# sourceMappingURL=resolverFunctions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolverFunctions.js","sourceRoot":"","sources":["../../lib/resolverFunctions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AAGhC,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,6BAA6B,GAC9B,MAAM,oBAAoB,CAAA;AAE3B,MAAM,UAAU,gBAAgB,CAC9B,IAAoB,EACpB,IAA8B;IAE9B,OAAO,OAAO,CAAC,IAAI,EAAE;QACnB,GAAG,IAAI;QACP,QAAQ,EAAE,WAAW;KACtB,CAAC,CAAA;AACJ,CAAC;AAED,MAAM,UAAU,iBAAiB,CAC/B,IAAoB,EACpB,IAA8B;IAE9B,OAAO,OAAO,CAAC,IAAI,EAAE;QACnB,GAAG,IAAI;QACP,QAAQ,EAAE,WAAW;KACtB,CAAC,CAAA;AACJ,CAAC;AAOD,MAAM,UAAU,0BAA0B,CACxC,IAAoB,EACpB,SAA4C,EAC5C,IAA8B;IAE9B,OAAO,OAAO,CAAC,IAAI,EAAE;QACnB,iDAAiD;QACjD,SAAS,EAAE,OAAO;QAClB,YAAY,EAAE,OAAO;QACrB,oBAAoB,EAAE,EAAE;QAExB,OAAO,EAAE,6BAA6B,CACpC,SAAS,CAAC,SAAS,CAAC,4BAA4B,EAChD,SAAS,CAAC,SAAS,CACpB;QACD,QAAQ,EAAE,WAAW;QACrB,GAAG,IAAI;KACR,CAAC,CAAA;AACJ,CAAC;AAOD,MAAM,UAAU,gBAAgB,CAC9B,IAAoB,EACpB,aAAqC,EACrC,IAA8B;IAE9B,OAAO,OAAO,CAAC,IAAI,EAAE;QACnB,kDAAkD;QAClD,SAAS,EAAE,OAAO;QAClB,YAAY,EAAE,SAAS;QACvB,oBAAoB,EAAE,EAAE;QAExB,OAAO,EAAE,mBAAmB,CAC1B,aAAa,CAAC,SAAS,CAAC,iBAAiB,EACzC,aAAa,CAAC,SAAS,CACxB;QACD,QAAQ,EAAE,WAAW;QACrB,GAAG,IAAI;KACR,CAAC,CAAA;AACJ,CAAC;AAOD,MAAM,UAAU,eAAe,CAC7B,IAAoB,EACpB,YAAmC,EACnC,IAA8B;IAE9B,OAAO,OAAO,CAAC,IAAI,EAAE;QACnB,kDAAkD;QAClD,SAAS,EAAE,OAAO;QAClB,YAAY,EAAE,SAAS;QACvB,oBAAoB,EAAE,EAAE;QAExB,OAAO,EAAE,iBAAiB,CAAC,YAAY,CAAC,SAAS,CAAC,gBAAgB,EAAE,YAAY,CAAC,SAAS,CAAC;QAC3F,QAAQ,EAAE,WAAW;QACrB,GAAG,IAAI;KACR,CAAC,CAAA;AACJ,CAAC"}
@@ -0,0 +1,19 @@
1
+ import type { FastifyRequest } from 'fastify';
2
+ import type { z } from 'zod';
3
+ type InferredOptionalSchema<Schema> = Schema extends z.Schema ? z.infer<Schema> : never;
4
+ /**
5
+ * Infer fastify request type from a contract
6
+ */
7
+ export type InferRequestFromContract<Contract extends {
8
+ requestHeaderSchema?: z.Schema;
9
+ requestPathParamsSchema?: z.Schema;
10
+ requestQuerySchema?: z.Schema;
11
+ successResponseBodySchema: z.Schema;
12
+ }> = FastifyRequest<{
13
+ Body: never;
14
+ Headers: InferredOptionalSchema<Contract['requestHeaderSchema']>;
15
+ Params: InferredOptionalSchema<Contract['requestPathParamsSchema']>;
16
+ Querystring: InferredOptionalSchema<Contract['requestQuerySchema']>;
17
+ Reply: InferredOptionalSchema<Contract['successResponseBodySchema']>;
18
+ }>;
19
+ export {};
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=typeUtils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"typeUtils.js","sourceRoot":"","sources":["../../lib/typeUtils.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "opinionated-machine",
3
+ "version": "1.0.0",
4
+ "description": "Very opinionated DI framework for fastify, built on top of awilix ",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "maintainers": [
9
+ {
10
+ "name": "Igor Savin",
11
+ "email": "kibertoad@gmail.com"
12
+ }
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc -p tsconfig.build.json",
16
+ "lint": "biome check . && tsc",
17
+ "lint:fix": "biome check --write .",
18
+ "test": "vitest --coverage",
19
+ "prepublishOnly": "npm run build"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/kibertoad/opinionated-machine.git"
24
+ },
25
+ "peerDependencies": {
26
+ "@lokalise/fastify-api-contracts": ">=1.4.4",
27
+ "@lokalise/universal-ts-utils": ">=4.2.0",
28
+ "awilix": ">=12.0.0",
29
+ "awilix-manager": ">=6.0.0",
30
+ "fastify": ">=5.0.0",
31
+ "fastify-type-provider-zod": ">=4.0.2",
32
+ "zod": ">=3.24.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^22.13.13",
36
+ "@biomejs/biome": "1.9.4",
37
+ "@lokalise/biome-config": "^1.6.1",
38
+ "@lokalise/fastify-api-contracts": "^1.4.4",
39
+ "@lokalise/tsconfig": "^1.1.0",
40
+ "@lokalise/universal-ts-utils": "^4.2.3",
41
+ "@vitest/coverage-v8": "^3.0.9",
42
+ "awilix": "^12.0.5",
43
+ "awilix-manager": "^6.1.0",
44
+ "fastify": "^5.2.2",
45
+ "fastify-type-provider-zod": "^4.0.2",
46
+ "vitest": "^3.0.9",
47
+ "typescript": "^5.8.2",
48
+ "zod": "^3.24.2"
49
+ },
50
+ "private": false,
51
+ "publishConfig": {
52
+ "access": "public"
53
+ },
54
+ "keywords": ["dependency", "injection", "opinionated", "awilix", "di"],
55
+ "homepage": "https://github.com/kibertoad/opinionated-machine",
56
+ "files": ["README.md", "LICENSE", "dist/*"]
57
+ }