Skip to content

Commit 6f678d7

Browse files
committed
feat(@sirutils/wizard): middleware
1 parent 8bdf5de commit 6f678d7

File tree

5 files changed

+204
-9
lines changed

5 files changed

+204
-9
lines changed

packages/wizard/src/definitions/wizard.ts

+53-5
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ declare global {
2626
}
2727

2828
interface WizardServices {}
29+
interface WizardMiddlewares {}
2930

3031
namespace Wizard {
3132
interface Options {
@@ -51,6 +52,7 @@ declare global {
5152
interface BaseApi {
5253
broker: ServiceBroker
5354
gateway: MoleculerService
55+
middleware: MoleculerService
5456
}
5557

5658
interface ServiceOptions<
@@ -67,7 +69,7 @@ declare global {
6769

6870
actions?: R
6971

70-
created?: <B, P, Q>(ctx: Sirutils.Wizard.ActionContext<B, P, Q>) => BlobType
72+
created?: <B, P, Q, S>(ctx: Sirutils.Wizard.ActionContext<B, P, Q, S>) => BlobType
7173
}
7274

7375
type ActionNames = {
@@ -97,6 +99,15 @@ declare global {
9799
: never
98100
: never
99101

102+
// Extract share props from global middleware or MiddlewareSchema
103+
type ExtractShareContent<M> = M extends keyof Sirutils.WizardMiddlewares
104+
? Sirutils.WizardMiddlewares[M] extends Sirutils.Wizard.MiddlewareSchema<infer S, BlobType>
105+
? S
106+
: never
107+
: M extends Sirutils.Wizard.MiddlewareSchema<infer S, BlobType>
108+
? S
109+
: never
110+
100111
interface ServiceApi {
101112
service: <
102113
const T extends string,
@@ -136,7 +147,7 @@ declare global {
136147
},
137148
]
138149

139-
interface ActionContext<B, P, Q> {
150+
interface ActionContext<B, P, Q, S> {
140151
logger: Moleculer.LoggerInstance
141152
body: Simplify<
142153
(B extends Sirutils.Schema.ValidationSchema<BlobType>
@@ -154,23 +165,39 @@ declare global {
154165
res?: ServerResponse
155166
streams?: Sirutils.Wizard.StreamData[]
156167
raw?: MoleculerContext
168+
share?: Partial<Pick<ContextShare, S extends keyof ContextShare ? S : never>>
169+
}
170+
// Only difference between ActionContext and MiddlewareContext is
171+
// MiddlewareContext's share property is not optional
172+
interface MiddlewareContext<B, P, Q, S> extends Omit<ActionContext<B, P, Q, S>, "share"> {
173+
share: Pick<ContextShare, S extends keyof ContextShare ? S : never>
157174
}
158175

176+
interface ContextShare {}
177+
159178
interface ActionSchema<B, P, Q, R> extends MoleculerActionSchema {}
179+
interface MiddlewareSchema<S extends keyof ContextShare, R> {
180+
logger: unknown,
181+
share: S[],
182+
handler: Sirutils.Wizard.MiddlewareHandler<S, R>
183+
}
160184

161185
type ActionList = Record<
162186
string,
163187
Sirutils.Wizard.ActionSchema<BlobType, BlobType, BlobType, BlobType>
164188
>
165189

166-
type ActionHandler<B, P, Q, R> = (ctx: Sirutils.Wizard.ActionContext<B, P, Q>) => R
190+
type ActionHandler<B, P, Q, S, R> = (ctx: Sirutils.Wizard.ActionContext<B, P, Q, S>) => R
191+
type MiddlewareHandler<S, R> = (ctx: Sirutils.Wizard.MiddlewareContext<BlobType, BlobType, BlobType, S>, next: unknown) => R
167192

168193
interface ActionApi {
169194
createAction: <
170195
const B extends Sirutils.Schema.ValidationSchema<BlobType>,
171196
const P extends Sirutils.Schema.ValidationSchema<BlobType>,
172197
const Q extends Sirutils.Schema.ValidationSchema<BlobType>,
173198
Hr,
199+
const M extends keyof Sirutils.WizardMiddlewares
200+
| Sirutils.Wizard.MiddlewareSchema<keyof Sirutils.Wizard.ContextShare, Hr> = never,
174201
>(
175202
meta: {
176203
body?: B
@@ -179,9 +206,13 @@ declare global {
179206
rest?: true | string | string[]
180207
cache?: boolean | CacherOptions
181208
stream?: boolean
209+
middlewares?: M[]
182210
multipart?: formidable.Options | boolean
183211
},
184-
handler: Sirutils.Wizard.ActionHandler<NoInfer<B>, NoInfer<P>, NoInfer<Q>, Hr>
212+
handler: Sirutils.Wizard.ActionHandler<
213+
NoInfer<B>, NoInfer<P>, NoInfer<Q>, ExtractShareContent<M>,
214+
Hr
215+
>
185216
) => (
186217
serviceOptions: Sirutils.Wizard.ServiceOptions<BlobType, BlobType, BlobType>,
187218
actionName: string
@@ -199,9 +230,26 @@ declare global {
199230
>
200231
}
201232

233+
interface MiddlewareApi {
234+
createMiddleware: <
235+
Hr,
236+
const S extends keyof ContextShare = never,
237+
>(
238+
meta: {
239+
name?: keyof WizardMiddlewares
240+
share?: S[]
241+
},
242+
handler: Sirutils.Wizard.MiddlewareHandler<S, Hr>
243+
) => Sirutils.Wizard.MiddlewareSchema<S, Hr>
244+
processMiddlewares: (
245+
ctx: Sirutils.Wizard.ActionContext<BlobType, BlobType, BlobType, BlobType>,
246+
middlewares: (keyof WizardMiddlewares | Sirutils.Wizard.MiddlewareSchema<keyof ContextShare, BlobType>)[]
247+
) => Promise<{ continue: true } | { continue: false, returnedData: BlobType}>
248+
}
249+
202250
type Context = Sirutils.PluginSystem.Context<
203251
Sirutils.Wizard.Options,
204-
Sirutils.Wizard.BaseApi & Sirutils.Wizard.ServiceApi & Sirutils.Wizard.ActionApi
252+
Sirutils.Wizard.BaseApi & Sirutils.Wizard.ServiceApi & Sirutils.Wizard.ActionApi & Sirutils.Wizard.MiddlewareApi
205253
>
206254
}
207255
}

packages/wizard/src/tag.ts

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const wizardTags = {
88
plugin: createTag('plugin'),
99
service: createTag('service'),
1010
action: createTag('action'),
11+
middleware: createTag('middleware'),
1112
httpMixin: createTag('http-mixin'),
1213
getDetails: createTag('get-details'),
1314

packages/wizard/src/utils/create.ts

+6
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { actionActions } from './internals/action'
1010
import { WizardRegenerator } from './internals/error'
1111
import { WizardLogger } from './internals/logger'
1212
import { serviceActions } from './internals/service'
13+
import { middlewareActions } from './internals/middleware'
1314

1415
export const createWizard = createPlugin<Sirutils.Wizard.Options, Sirutils.Wizard.BaseApi>(
1516
{
@@ -149,6 +150,10 @@ export const createWizard = createPlugin<Sirutils.Wizard.Options, Sirutils.Wizar
149150
},
150151
},
151152
}),
153+
// Middleware service for global middlewares
154+
middleware: broker.createService({
155+
name: 'middleware',
156+
}),
152157
methods: {
153158
reformatError(err: BlobType) {
154159
return err
@@ -160,4 +165,5 @@ export const createWizard = createPlugin<Sirutils.Wizard.Options, Sirutils.Wizar
160165
)
161166
.register(serviceActions)
162167
.register(actionActions)
168+
.register(middlewareActions)
163169
.lock()

packages/wizard/src/utils/internals/action.ts

+10-4
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export const actionActions = createActions(
5252
ctx.params.req.method === 'DELETE'
5353

5454
// @ts-ignore
55-
const subctx: Sirutils.Wizard.ActionContext<BlobType, BlobType, BlobType> = {
55+
const subctx: Sirutils.Wizard.ActionContext<BlobType, BlobType, BlobType, BlobType> = {
5656
body: ctx.params.req.body ?? ({} as BlobType),
5757
req: ctx.params.req,
5858
res: ctx.params.res,
@@ -150,7 +150,10 @@ export const actionActions = createActions(
150150
unwrap(await bodySchema(subctx.body as BlobType), wizardTags.invalidBody)
151151
}
152152

153-
return rawHandler(subctx)
153+
// Process through the middlewares if there are
154+
// And continue or return data according to the result
155+
const middlewaresResult = await context.api.processMiddlewares(subctx, meta.middlewares ?? [])
156+
return middlewaresResult.continue ? rawHandler(subctx) : middlewaresResult.returnedData
154157
}
155158

156159
if (meta.rest) {
@@ -176,7 +179,7 @@ export const actionActions = createActions(
176179
)
177180
}
178181

179-
const subctx: Sirutils.Wizard.ActionContext<BlobType, BlobType, BlobType> = {
182+
const subctx: Sirutils.Wizard.ActionContext<BlobType, BlobType, BlobType, BlobType> = {
180183
body: isParamsStream ? ctx.meta : ctx.params,
181184
logger: serviceLogger,
182185
raw: ctx,
@@ -191,7 +194,10 @@ export const actionActions = createActions(
191194
: [ctx.params, getDetails(ctx.params, ctx.meta.$params)]
192195
}
193196

194-
return rawHandler(subctx)
197+
// Process through the middlewares if there are
198+
// And continue or return data according to the result
199+
const middlewaresResult = await context.api.processMiddlewares(subctx, meta.middlewares ?? [])
200+
return middlewaresResult.continue ? rawHandler(subctx) : middlewaresResult.returnedData
195201
},
196202
`${wizardTags.action}#createAction.handler.${serviceOptions.name}@${serviceOptions.version}#${actionName}` as Sirutils.ErrorValues,
197203
context.$cause
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// import fs from 'node:fs'
2+
import { type BlobType, capsule, createActions } from '@sirutils/core'
3+
4+
import { logger } from '../../internal/logger'
5+
import { createTag } from '../../internal/tag'
6+
import { wizardTags } from '../../tag'
7+
8+
import type { LoggerInstance, ServiceAction } from 'moleculer'
9+
10+
11+
export const middlewareActions = createActions(
12+
(context: Sirutils.Wizard.Context): Sirutils.Wizard.MiddlewareApi => ({
13+
/**
14+
* Creates middleware and returns its schema
15+
*
16+
* If middleware is created with a name, it becomes middleware servive's action
17+
* For access it from anywhere with just its name
18+
*
19+
* Nameless middlewares can be used for generators
20+
*/
21+
createMiddleware(meta, rawHandler) {
22+
// Logger for use it in the handler
23+
const middlewareLogger = logger.create({
24+
defaults: {
25+
tag: createTag(`middleware.${meta.name}`),
26+
},
27+
})
28+
29+
// Capsule the handler for better error handling
30+
const handler = capsule(
31+
rawHandler,
32+
`${wizardTags.middleware}#createMiddleware.handler.${meta.name}` as Sirutils.ErrorValues,
33+
context.$cause
34+
)
35+
36+
const middlewareSchema: Sirutils.Wizard.MiddlewareSchema<keyof Sirutils.Wizard.ContextShare, BlobType> = {
37+
logger: middlewareLogger,
38+
share: Array.from(new Set(meta.share ? meta.share : [])),
39+
handler
40+
}
41+
42+
const settings = context.api.middleware.settings
43+
44+
// If middleware has a name, make it a action of middleware service
45+
if (meta.name) {
46+
if (settings.middlewareSchemas === undefined) {
47+
settings.middlewareSchemas = {}
48+
}
49+
50+
settings.middlewareSchemas[meta.name] = middlewareSchema
51+
52+
context.api.middleware.actions[meta.name] = (handler) as ServiceAction
53+
}
54+
55+
return middlewareSchema
56+
},
57+
/**
58+
* Processes middlewares that given its name or schema in order
59+
* And returns that whether endpoint processes must continue after middlewares
60+
*
61+
* If there is a returned data from any middleware, returns it in the case continue is false
62+
*/
63+
async processMiddlewares(actionCtx, middlewares) {
64+
if(middlewares.length === 0) {
65+
return { continue: true }
66+
}
67+
68+
let willContinue = true
69+
let returnedData: BlobType
70+
71+
// Data structure that has shared usage between middlewares
72+
// Its properties are defined in ContextShare interface
73+
const share: Record<string, BlobType> = {}
74+
75+
// TODO: Sometimes interCtx is causing error about
76+
// Excessively deep and possibly infinite instantiation ts(2589)
77+
const interCtx = {
78+
...actionCtx, share
79+
}
80+
81+
// Symbol for determining to pass the next middleware
82+
const nextSymbol = Symbol('next-middleware')
83+
const settings = context.api.middleware.settings
84+
85+
const shareKeys: string[] = middlewares.flatMap(middleware => {
86+
if (typeof middleware === 'string') {
87+
return settings.middlewareSchemas[middleware].share
88+
}
89+
return middleware.share
90+
})
91+
92+
// Grap given share props in schemas and add these into share object as undefined
93+
//biome-ignore lint/complexity/noForEach: <explanation>
94+
shareKeys.forEach(key => {
95+
if (!Object.hasOwn(share, key)) {
96+
Object.defineProperty(share, key, { writable: true })
97+
}
98+
});
99+
100+
// Process middleware and everytime passing next middleware, change the logger
101+
for (const middleware of middlewares) {
102+
if (typeof middleware === 'string') {
103+
const middlewareSchema = settings.middlewareSchemas[middleware] as
104+
Sirutils.Wizard.MiddlewareSchema<keyof Sirutils.Wizard.ContextShare, BlobType>
105+
106+
interCtx.logger = middlewareSchema.logger as LoggerInstance
107+
returnedData = await middlewareSchema.handler(interCtx, nextSymbol)
108+
} else {
109+
interCtx.logger = middleware.logger as LoggerInstance
110+
returnedData = await middleware.handler(interCtx, nextSymbol)
111+
}
112+
113+
if (returnedData !== nextSymbol) {
114+
willContinue = false
115+
break
116+
}
117+
}
118+
119+
// After processes remove props that have undefined value
120+
// For avoiding confusion on share object keys
121+
actionCtx.share = shareKeys.reduce((acc, key) => {
122+
if(share[key] !== undefined) {
123+
acc[key] = share[key]
124+
}
125+
return acc;
126+
}, {} as Record<string, BlobType>);
127+
128+
return willContinue
129+
? { continue: willContinue }
130+
: { continue: willContinue, returnedData }
131+
}
132+
}),
133+
wizardTags.middleware
134+
)

0 commit comments

Comments
 (0)