@twasik4/pocket-service 1.0.3 → 1.0.4
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/dist/index.d.ts +34 -2
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/workers/db/clickhouse/index.ts +5 -5
- package/src/workers/db/mongo/collection.ts +18 -17
- package/src/workers/db/mongo/mongo.ts +126 -10
- package/tests/auth-strategies.spec.ts +150 -0
- package/tests/clickhouse.spec.ts +102 -0
- package/tests/express-types.spec.ts +232 -0
- package/tests/mongo-advanced.spec.ts +172 -0
- package/tests/mongo.spec.ts +49 -0
- package/tests/redis.spec.ts +1 -1
- package/tests/utils.spec.ts +30 -0
package/dist/index.d.ts
CHANGED
|
@@ -238,10 +238,12 @@ declare class CollectionWrapper<T extends Document> {
|
|
|
238
238
|
updateOne(filter: Filter<T>, update: Partial<T>): Promise<void>;
|
|
239
239
|
/**
|
|
240
240
|
* Soft delete a document by setting the `deletedAt` field to the current date.
|
|
241
|
+
* If the schema has a `deletedBy` field, it is set to the provided actor or defaults to `system`.
|
|
241
242
|
* If the schema does not have a `deletedAt` field, it will perform a hard delete.
|
|
242
243
|
* @param filter - The filter to find documents to delete.
|
|
244
|
+
* @param deletedBy - Optional actor identifier for soft deletes.
|
|
243
245
|
*/
|
|
244
|
-
delete(filter: Filter<T
|
|
246
|
+
delete(filter: Filter<T>, deletedBy?: string): Promise<void>;
|
|
245
247
|
/**
|
|
246
248
|
* Checks if the schema has a given property.
|
|
247
249
|
* @param schema - The Zod schema to check against.
|
|
@@ -279,6 +281,24 @@ type MongoIndexSpec = {
|
|
|
279
281
|
*/
|
|
280
282
|
expireAfterSeconds?: number;
|
|
281
283
|
};
|
|
284
|
+
type MongoTimeSeriesSpec = {
|
|
285
|
+
/**
|
|
286
|
+
* Name of the date field that MongoDB uses as the event time.
|
|
287
|
+
*/
|
|
288
|
+
timeField: string;
|
|
289
|
+
/**
|
|
290
|
+
* Optional metadata field used to group measurements.
|
|
291
|
+
*/
|
|
292
|
+
metaField?: string;
|
|
293
|
+
/**
|
|
294
|
+
* Optional bucket granularity hint used by MongoDB.
|
|
295
|
+
*/
|
|
296
|
+
granularity?: "seconds" | "minutes" | "hours";
|
|
297
|
+
/**
|
|
298
|
+
* Optional retention in seconds for automatic expiry.
|
|
299
|
+
*/
|
|
300
|
+
expireAfterSeconds?: number;
|
|
301
|
+
};
|
|
282
302
|
type MongoCollectionMethodContext<TDocument extends Document> = {
|
|
283
303
|
/**
|
|
284
304
|
* The MongoDB client instance
|
|
@@ -314,6 +334,10 @@ type MongoCollectionDefinition<TDocument extends Document = Document, TMethods e
|
|
|
314
334
|
* Optional array of index specifications to create on this collection. Indexes are created idempotently on service startup, so duplicate indexes are safely ignored.
|
|
315
335
|
*/
|
|
316
336
|
indexes?: MongoIndexSpec[];
|
|
337
|
+
/**
|
|
338
|
+
* Optional MongoDB time-series settings. When provided, the collection is created as a time-series collection if it does not already exist.
|
|
339
|
+
*/
|
|
340
|
+
timeSeries?: MongoTimeSeriesSpec;
|
|
317
341
|
};
|
|
318
342
|
type MongoCollectionsConfig = Record<string, MongoCollectionDefinition<any, MongoCollectionMethods>>;
|
|
319
343
|
type InferDocument<TDefinition> = TDefinition extends {
|
|
@@ -357,6 +381,10 @@ declare function defineMongoCollection<TSchema extends SchemaParser<any>>(config
|
|
|
357
381
|
* Optional array of index specifications to create on this collection.
|
|
358
382
|
*/
|
|
359
383
|
indexes?: MongoIndexSpec[];
|
|
384
|
+
/**
|
|
385
|
+
* Optional MongoDB time-series settings.
|
|
386
|
+
*/
|
|
387
|
+
timeSeries?: MongoTimeSeriesSpec;
|
|
360
388
|
}): MongoCollectionDefinition<SchemaOutput<TSchema>, {}>;
|
|
361
389
|
/**
|
|
362
390
|
* Define a MongoDB collection with an associated Zod schema for validation and optional custom methods. The returned collection definition can then be used to create a MongoDatabase instance, which will automatically wrap the collection with the provided schema validation and methods.
|
|
@@ -381,6 +409,10 @@ declare function defineMongoCollection<TSchema extends SchemaParser<any>, TMetho
|
|
|
381
409
|
* Optional array of index specifications to create on this collection.
|
|
382
410
|
*/
|
|
383
411
|
indexes?: MongoIndexSpec[];
|
|
412
|
+
/**
|
|
413
|
+
* Optional MongoDB time-series settings.
|
|
414
|
+
*/
|
|
415
|
+
timeSeries?: MongoTimeSeriesSpec;
|
|
384
416
|
}): MongoCollectionDefinition<SchemaOutput<TSchema>, TMethods>;
|
|
385
417
|
declare function createMongo<TCollections extends MongoCollectionsConfig>(dbName: string, config: TCollections, options?: {
|
|
386
418
|
uri?: string;
|
|
@@ -699,4 +731,4 @@ declare function createJoseJwtVerifier(options: CreateJoseJwtVerifierOptions): J
|
|
|
699
731
|
declare function createJwtAuthStrategy(options: CreateJwtAuthStrategyOptions): AuthStrategy;
|
|
700
732
|
declare function createMtlsAuthStrategy(options?: CreateMtlsAuthStrategyOptions): AuthStrategy;
|
|
701
733
|
|
|
702
|
-
export { type AuthResolver, type AuthStrategy, type AuthenticatedUser, type BaseExpressOptions, CollectionWrapper, type CreateJoseJwtVerifierOptions, type CreateJwtAuthStrategyOptions, type CreateMtlsAuthStrategyOptions, type CustomModuleFactory, type CustomModules, type ExpressMiddleware, type ExpressOptions, type ExpressOptionsWithCorsFn, type ExpressOptionsWithCorsWhitelist, type InferMongoCollections, type JwtStrategyClaims, type JwtTokenVerifier, type Message, type MongoBoundCollection, type MongoCollectionDefinition, type MongoCollectionMethodContext, type MongoCollectionMethods, type MongoCollectionsConfig, type MongoDatabase, type MongoIndexSpec, type RedisStreamEventHandler, type RedisStreamHandlerContext, type RouteDefinition, type RouteMeta, type SchemaParser, Service, type TypedRequest, type TypedRequestHandler, createAuthResolver, createJoseJwtVerifier, createJwtAuthStrategy, createMetaRouter, createMetaRouterWithAuth, createMongo, createMtlsAuthStrategy, defaultHeaderAuthResolver, defineMongoCollection, getRegisteredRoutes, redisStreamHandler };
|
|
734
|
+
export { type AuthResolver, type AuthStrategy, type AuthenticatedUser, type BaseExpressOptions, CollectionWrapper, type CreateJoseJwtVerifierOptions, type CreateJwtAuthStrategyOptions, type CreateMtlsAuthStrategyOptions, type CustomModuleFactory, type CustomModules, type ExpressMiddleware, type ExpressOptions, type ExpressOptionsWithCorsFn, type ExpressOptionsWithCorsWhitelist, type InferMongoCollections, type JwtStrategyClaims, type JwtTokenVerifier, type Message, type MongoBoundCollection, type MongoCollectionDefinition, type MongoCollectionMethodContext, type MongoCollectionMethods, type MongoCollectionsConfig, type MongoDatabase, type MongoIndexSpec, type MongoTimeSeriesSpec, type RedisStreamEventHandler, type RedisStreamHandlerContext, type RouteDefinition, type RouteMeta, type SchemaParser, Service, type TypedRequest, type TypedRequestHandler, createAuthResolver, createJoseJwtVerifier, createJwtAuthStrategy, createMetaRouter, createMetaRouterWithAuth, createMongo, createMtlsAuthStrategy, defaultHeaderAuthResolver, defineMongoCollection, getRegisteredRoutes, redisStreamHandler };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import O from"express";import{existsSync as J,readdirSync as _}from"fs";import b from"path";import{createClient as z}from"redis";import{Logger as V}from"winston";import{Worker as K}from"worker_threads";import{Router as N}from"express";import C from"zod";function v(n){let e=n.headers["x-user-id"],t=n.headers["x-user-meta"];if(typeof t=="string")try{let s=JSON.parse(t);if(typeof s.userId=="string")return s}catch{return null}return typeof e!="string"?null:{userId:e}}function S(n,e={}){return async(t,s)=>{for(let r of n){if(r.canHandle&&!await r.canHandle(t,s))continue;let i=await r.authenticate(t,s);if(i)return i}return e.fallbackResolver?e.fallbackResolver(t,s):null}}var H=[];function le(){return H}function de(){return w()}function w(n){let e=N(),t=[],s=n?.authResolver??(a=>v(a)),r=n?.onUnauthorized??((a,u)=>{u.status(401).json({isSuccess:!1,message:"Unauthorized"})}),i=n?.onAuthError??((a,u,d)=>{d.status(401).json({isSuccess:!1,message:"Unauthorized"})});function o(a,u){let d=[],m=a.bodyValidator,g=a.paramsValidator;t.push({method:a.method,fullPath:a.fullPath,requireAuth:a.requireAuth??!1,meta:a.meta,bodyValidator:m,paramsValidator:g}),a.requireAuth&&d.push(async(c,h,p)=>{try{let l=await s(c,a);if(!l){r(c,h);return}c.user=l,p()}catch(l){i(l,c,h)}}),m&&a.method!=="GET"&&d.push((c,h,p)=>{let l=c.body;if(typeof l=="string"){if(m instanceof C.ZodObject||m instanceof C.ZodArray||m.def.type==="object"||m.def.type==="array")try{l=JSON.parse(l)}catch(f){return h.status(400).json({isSuccess:!1,message:"Invalid JSON in request body",error:f instanceof Error?f.message:f})}else if(m instanceof C.ZodNumber||m.def.type==="number")try{l=Number(l)}catch(f){return h.status(400).json({isSuccess:!1,message:"Invalid number in request body",error:f instanceof Error?f.message:f})}}let y=m.safeParse(l);y.success?(c.body=y.data,p()):h.status(400).json({isSuccess:!1,message:"Invalid request body",error:y.error})}),g&&d.push((c,h,p)=>{let l=g.safeParse(c.params);l.success?(c.params=l.data,p()):h.status(400).json({isSuccess:!1,message:"Invalid URL parameters",error:l.error})}),e[a.method.toLowerCase()](a.fullPath,...d,u)}return{router:e,routes:t,addRoute:o}}import{createClient as W}from"@clickhouse/client";function A(n){let e=W({host:n.url,username:n.username,password:n.password});async function t(){for(let{createSQL:r}of Object.values(n.tables||{}))await e.command({query:r})}let s=Object.fromEntries(Object.entries(n.tables||{}).map(([r,i])=>[r,{select:async u=>{let d=`SELECT * FROM ${i.name}`;if(u&&Object.keys(u).length>0){let c=Object.keys(u).map(([h,p])=>typeof p=="string"?`${h} = '${p}'`:`${h} = ${p}`).join(" AND ");d+=` WHERE ${c}`}return(await(await e.query({query:d})).json()).data},insert:async u=>{await e.insert({table:i.name,values:u,format:"JSONEachRow"})}}]));return{client:e,ensureTables:t,...s}}import{MongoClient as F}from"mongodb";import{ObjectId as R}from"mongodb";var x=class{constructor(e,t,s){this.collection=e;this.schema=t;this.revision=s}collection;schema;revision;parseWrite(e){return this.schema?this.schema.parse(e):e}parseRead(e){return this.schema?{...this.schema.parse(e),_id:e._id}:e}async trackRevision(e){if(this.revision){let t={...e,_id:new R};await this.revision.insertOne(t)}}async get(e={}){try{return this.hasProperty("deletedAt")&&(e={...e,deletedAt:{$exists:!1}}),(await this.collection.find(e).toArray()).map(s=>this.parseRead(s))}catch{return[]}}async getOne(e={}){try{this.hasProperty("deletedAt")&&(e={...e,deletedAt:{$exists:!1}});let t=await this.collection.findOne(e);return t?this.parseRead(t):null}catch{return null}}async getRevisions(e={}){try{return this.revision?(await this.revision.find(e,{sort:{revision:1}}).toArray()).map(s=>this.parseRead(s)):[]}catch{return[]}}async getMostRecentRevision(){try{if(!this.revision)return null;let e=await this.revision.find({},{sort:{revision:-1}}).limit(1).toArray();return e.length>0?this.parseRead(e[0]):null}catch{return null}}async insert(e){try{let t=this.parseWrite(e);await this.collection.insertOne(t),await this.trackRevision(t)}catch(t){throw new Error(`Failed to insert document: ${t.message}`)}}async update(e,t){try{let s=await this.collection.find(e).toArray();if(!s.length)return;if(await this.collection.updateMany(e,{$set:t}),this.revision){let r=s.map(i=>({...i,...t,_id:new R}));await this.revision.insertMany(r)}}catch(s){throw new Error(`Failed to update documents: ${s.message}`)}}async updateOne(e,t){try{let r={$set:this.hasProperty("updatedAt")?{...t,updatedAt:new Date}:t,...this.revision?{$inc:{revision:1}}:{}};await this.collection.updateOne(e,r),this.revision&&await this.collection.aggregate([{$match:e},{$set:{...t,_id:new R}},{$merge:{into:{db:this.collection.dbName,col:this.revision.collectionName,on:"_id",whenNotMatched:"insert"}}}]).toArray()}catch(s){throw console.error("Failed to update document:",s),new Error(`Failed to update document: ${s.message}`)}}async delete(e){try{if(this.hasProperty("deletedAt")){let t=new Date,s={$set:{deletedAt:t}};await this.collection.updateMany(e,s),this.revision&&await this.collection.aggregate([{$match:e},{$set:{_id:new R,deletedAt:t}},{$merge:{into:{db:this.collection.dbName,col:this.revision.collectionName,on:"_id",whenNotMatched:"insert"}}}]).toArray()}else await this.collection.deleteMany(e)}catch(t){throw new Error(`Failed to delete documents: ${t.message}`)}}hasProperty(e){if(!this.schema)return!1;let t=this.schema,s=t.shape??(typeof t._def?.shape=="function"?t._def.shape():t._def?.shape);return s!==null&&typeof s=="object"&&!Array.isArray(s)?e in s:!1}};function Me(n){return{name:n.name,schema:n.schema,methods:n.methods,indexes:n.indexes,trackRevisions:!1}}function k(n,e,t={}){let s=new F(t.uri||process.env.MONGO_URI||"mongodb://mongo:27017"),r=s.db(n),i={};for(let o in e){let a=e[o],u=a.name||o,d=r.collection(u),m=a.trackRevisions?r.collection(`${u}_revisions`):void 0,g=new x(d,a.schema,m),c=a.methods?.({client:s,db:r,collection:d,base:g})||{};if(a.indexes&&a.indexes.length>0)for(let h of a.indexes){let{key:p,...l}=h;d.createIndex(p,l).catch(y=>{console.error(`Failed to create index on ${u}: ${y.message}`)})}i[o]=Object.assign(g,c)}return{client:s,db:r,collections:i,...i,disconnect:async()=>s.close(),connect:async()=>s.connect()}}import L,{format as T,transports as B}from"winston";var M=n=>L.createLogger({format:T.json({bigint:!0}),defaultMeta:{service:n},transports:[new B.Console({format:T.combine(T.colorize(),T.timestamp(),T.printf(({level:e,message:t,timestamp:s})=>`${s} [${n}] ${e}: ${t}`))})]});import*as D from"crypto";function P(n){if(n%2!==0)throw new Error("Length must be an even number");let e=n/2;return D.randomBytes(e).toString("hex")}function E(n){let e=Math.floor(n/1e3%60),t=Math.floor(n/(1e3*60)%60),s=Math.floor(n/(1e3*60*60)%24);return{days:Math.floor(n/(1e3*60*60*24)),hours:s,minutes:t,seconds:e}}import q from"cors";var $=class{name=process.env.SERVICE_NAME??P(12);url=process.env.SERVICE_URL??"http://localhost";port=process.env.SERVICE_PORT?Number(process.env.SERVICE_PORT):3100;workerCount=1;handlers={};loadedEventListeners=[];routers=[];startedAt=Date.now();dependencies=new Map;dependencyOrder=[];isWorkerRunning=!1;serviceStreamSubscriptions=[];_status="starting";HEARTBEAT_INTERVAL=1e3*5;authStrategies=[];authResolverOverride;useDefaultHeaderAuthFallback=!0;expressOptions={corsWhitelist:["*"],asJson:!0,customMiddleware:[]};withoutExpressFlag=!1;getAuthResolver(){return this.authResolverOverride?this.authResolverOverride:S(this.authStrategies,{fallbackResolver:this.useDefaultHeaderAuthFallback?e=>v(e):void 0})}get streamKey(){return this.simpleName+":events"}get simpleName(){return this.name.split("-")[0]}get fullUrl(){return`${this.url}:${this.port}`}get api(){if(this.withoutExpressFlag)throw new Error("Express server is disabled. Service built without Express.");return this.dependencies.has("expressApp")||this.dependencies.set("expressApp",O()),this.dependencies.get("expressApp")}get log(){this.dependencies.has("logger")||this.dependencies.set("logger",M(this.name));let e=this.dependencies.get("logger");if(!(e instanceof V))throw new Error("Logger is not of type Logger.");return e}get routes(){return this.routers.flatMap(e=>e.routes?.map(t=>{let s=e.base==="/"?"":e.base,r=t.fullPath==="/"?"":t.fullPath;return{path:`${s}${r}`==""?"/":`${s}${r}`,method:t.method,requireAuth:t.requireAuth??!1,meta:t.meta}})||[])}get status(){return this._status}get mongo(){if(!this.dependencies.has("mongoClient"))throw new Error("MongoDB module has not been added.");return this.dependencies.get("mongoClient")}get db(){if(!this.dependencies.has("mongoClient"))throw new Error("MongoDB module has not been added.");return this.mongo.collections}get clickhouse(){if(!this.dependencies.has("clickhouse"))throw new Error("Clickhouse module has not been added.");return this.dependencies.get("clickhouse")}get redis(){if(!this.dependencies.has("redis"))throw new Error("Redis module has not been added.");return this.dependencies.get("redis")}async register(){if(!this.dependencies.has("redis")){this.log.warn("Redis module not initialized. Call withRedis() to enable service registration.");return}let e=`services:registry:${this.name}`,t=JSON.stringify({name:this.name,streamKey:this.streamKey,url:this.fullUrl,dependencies:Object.keys(this.dependencies),routes:this.routes,lastHeartbeat:Date.now()});await this.redis.set(e,t,{EX:30})}async heartbeat(){if(!this.dependencies.has("redis")){this.log.warn("Redis module not initialized. Call withRedis() to enable heartbeat.");return}setInterval(async()=>{await this.register()},this.HEARTBEAT_INTERVAL)}build(){try{if(this._status="starting",Promise.allSettled(this.dependencyOrder.filter(e=>e!=="customDependencies").map(e=>{switch(e){case"redis":{this.dependencies.has("workers")?(this.loadHandlers(),this.runWorkers()):this.log.warn("Worker module not initialized. Call withWorkers() to enable worker threads and dynamically load handlers."),this.redis.connect().then(async()=>{this.log.info("Redis module loaded"),this.register(),this.log.info("Service registered in Redis"),this.heartbeat(),this.log.info("Heartbeat started"),process.on("SIGINT",()=>{this.log.info("SIGINT received. Shutting down gracefully..."),this.shutdown()})}).catch(t=>{this.log.error(`Failed to load Redis module: ${t}`)});break}case"mongoClient":{this.log.info("Connecting to MongoDB..."),this.mongo.connect().then(()=>{this.log.info("Mongo module loaded")}).catch(t=>{throw this.log.error(`Failed to connect to MongoDB: ${t}`),t});break}case"clickhouse":{this.log.info("Connecting to ClickHouse..."),this.clickhouse.ensureTables().then(()=>{this.log.info("ClickHouse module loaded")}).catch(t=>{this.log.error(`Failed to load ClickHouse module: ${t}`)});break}default:this.log.info(`Unknown module ${e} during initialization.`)}})).then(e=>{if(e.forEach((t,s)=>{let r=this.dependencyOrder[s];t.status==="fulfilled"?this.log.info(`Module "${r}" initialized successfully.`):this.log.error(`Failed to initialize module "${r}": ${t.reason}`)}),this.dependencies.has("customDependencies")){let t=this.dependencies.get("customDependencies");for(let s of t)try{s.init(this.log)}catch(r){this.log.error(`Failed to initialize custom module "${s.name}": ${r}`)}}}),this.withoutExpressFlag)this.log.warn("Express server is disabled. Service built without Express.");else{this.expressOptions.asJson&&(this.log.info("Configuring Express to parse JSON bodies"),this.api.use(O.json()));for(let e of this.expressOptions.customMiddleware||[])this.log.info("Adding custom middleware to Express"),this.api.use(e);"corsWhitelist"in this.expressOptions?(this.log.info("Configuring CORS with whitelist: "+this.expressOptions.corsWhitelist),this.api.use(q({origin:this.expressOptions.corsWhitelist,credentials:this.expressOptions.credentials}))):"corsFn"in this.expressOptions&&(this.log.info("Configuring CORS with function"),this.api.use(q({origin:this.expressOptions.corsFn,credentials:this.expressOptions.credentials}))),this.expressOptions.omitDefaultRoutes||this.registerDefaultRoutes(),this.log.info(`Registering ${this.routers.length} routers`),this.routers.forEach(({base:e,router:t})=>{this.api.use(e,t)}),this.log.info("Routers registered:"+this.routers.map(e=>{let t=e.routes?Math.max(...e.routes?.map(i=>i.method.length)):0,s=e.routes?Math.max(...e.routes?.map(i=>i.fullPath.length)):0,r=e.routes?Math.max(...e.routes?.map(i=>i.requireAuth?6:8)):0;return`
|
|
1
|
+
import $ from"express";import{existsSync as z,readdirSync as V}from"fs";import S from"path";import{createClient as K}from"redis";import{Logger as Z}from"winston";import{Worker as G}from"worker_threads";import{Router as H}from"express";import C from"zod";function v(r){let e=r.headers["x-user-id"],t=r.headers["x-user-meta"];if(typeof t=="string")try{let s=JSON.parse(t);if(typeof s.userId=="string")return s}catch{return null}return typeof e!="string"?null:{userId:e}}function A(r,e={}){return async(t,s)=>{for(let n of r){if(n.canHandle&&!await n.canHandle(t,s))continue;let i=await n.authenticate(t,s);if(i)return i}return e.fallbackResolver?e.fallbackResolver(t,s):null}}var W=[];function ce(){return W}function he(){return R()}function R(r){let e=H(),t=[],s=r?.authResolver??(l=>v(l)),n=r?.onUnauthorized??((l,u)=>{u.status(401).json({isSuccess:!1,message:"Unauthorized"})}),i=r?.onAuthError??((l,u,d)=>{d.status(401).json({isSuccess:!1,message:"Unauthorized"})});function o(l,u){let d=[],a=l.bodyValidator,m=l.paramsValidator;t.push({method:l.method,fullPath:l.fullPath,requireAuth:l.requireAuth??!1,meta:l.meta,bodyValidator:a,paramsValidator:m}),l.requireAuth&&d.push(async(h,p,g)=>{try{let c=await s(h,l);if(!c){n(h,p);return}h.user=c,g()}catch(c){i(c,h,p)}}),a&&l.method!=="GET"&&d.push((h,p,g)=>{let c=h.body;if(typeof c=="string"){if(a instanceof C.ZodObject||a instanceof C.ZodArray||a.def.type==="object"||a.def.type==="array")try{c=JSON.parse(c)}catch(y){return p.status(400).json({isSuccess:!1,message:"Invalid JSON in request body",error:y instanceof Error?y.message:y})}else if(a instanceof C.ZodNumber||a.def.type==="number")try{c=Number(c)}catch(y){return p.status(400).json({isSuccess:!1,message:"Invalid number in request body",error:y instanceof Error?y.message:y})}}let f=a.safeParse(c);f.success?(h.body=f.data,g()):p.status(400).json({isSuccess:!1,message:"Invalid request body",error:f.error})}),m&&d.push((h,p,g)=>{let c=m.safeParse(h.params);c.success?(h.params=c.data,g()):p.status(400).json({isSuccess:!1,message:"Invalid URL parameters",error:c.error})}),e[l.method.toLowerCase()](l.fullPath,...d,u)}return{router:e,routes:t,addRoute:o}}import{createClient as B}from"@clickhouse/client";function k(r){let e=B({host:r.url,username:r.username,password:r.password});async function t(){for(let{createSQL:n}of Object.values(r.tables||{}))await e.command({query:n})}let s=Object.fromEntries(Object.entries(r.tables||{}).map(([n,i])=>[n,{select:async u=>{let d=`SELECT * FROM ${i.name}`;if(u&&Object.keys(u).length>0){let h=Object.entries(u).map(([p,g])=>typeof g=="string"?`${p} = '${g}'`:`${p} = ${g}`).join(" AND ");d+=` WHERE ${h}`}return(await(await e.query({query:d})).json()).data},insert:async u=>{await e.insert({table:i.name,values:u,format:"JSONEachRow"})}}]));return{client:e,ensureTables:t,...s}}import{MongoClient as L}from"mongodb";import{ObjectId as x}from"mongodb";var b=class{constructor(e,t,s){this.collection=e;this.schema=t;this.revision=s}collection;schema;revision;parseWrite(e){return this.schema?this.schema.parse(e):e}parseRead(e){return this.schema?{...this.schema.parse(e),_id:e._id}:e}async trackRevision(e){if(this.revision){let t={...e,_id:new x};await this.revision.insertOne(t)}}async get(e={}){try{return this.hasProperty("deletedAt")&&(e={...e,deletedAt:{$exists:!1}}),(await this.collection.find(e).toArray()).map(s=>this.parseRead(s))}catch{return[]}}async getOne(e={}){try{this.hasProperty("deletedAt")&&(e={...e,deletedAt:{$exists:!1}});let t=await this.collection.findOne(e);return t?this.parseRead(t):null}catch{return null}}async getRevisions(e={}){try{return this.revision?(await this.revision.find(e,{sort:{revision:1}}).toArray()).map(s=>this.parseRead(s)):[]}catch{return[]}}async getMostRecentRevision(){try{if(!this.revision)return null;let e=await this.revision.find({},{sort:{revision:-1}}).limit(1).toArray();return e.length>0?this.parseRead(e[0]):null}catch{return null}}async insert(e){try{let t=this.parseWrite(e);await this.collection.insertOne(t),await this.trackRevision(t)}catch(t){throw new Error(`Failed to insert document: ${t.message}`)}}async update(e,t){try{let s=await this.collection.find(e).toArray();if(!s.length)return;if(await this.collection.updateMany(e,{$set:t}),this.revision){let n=s.map(i=>({...i,...t,_id:new x}));await this.revision.insertMany(n)}}catch(s){throw new Error(`Failed to update documents: ${s.message}`)}}async updateOne(e,t){try{let n={$set:this.hasProperty("updatedAt")?{...t,updatedAt:new Date}:t,...this.revision?{$inc:{revision:1}}:{}};await this.collection.updateOne(e,n),this.revision&&await this.collection.aggregate([{$match:e},{$set:{...t,_id:new x}},{$merge:{into:this.revision.collectionName,on:"_id",whenNotMatched:"insert"}}]).toArray()}catch(s){throw console.error("Failed to update document:",s),new Error(`Failed to update document: ${s.message}`)}}async delete(e,t){try{if(this.hasProperty("deletedAt")){let s=new Date,n=t??"system",i={deletedAt:s,...this.hasProperty("deletedBy")?{deletedBy:n}:{}},o={$set:i};await this.collection.updateMany(e,o),this.revision&&await this.collection.aggregate([{$match:e},{$set:{_id:new x,...i}},{$merge:{into:this.revision.collectionName,on:"_id",whenNotMatched:"insert"}}]).toArray()}else await this.collection.deleteMany(e)}catch(s){throw new Error(`Failed to delete documents: ${s.message}`)}}hasProperty(e){if(!this.schema)return!1;let t=this.schema,s=t.shape??(typeof t._def?.shape=="function"?t._def.shape():t._def?.shape);return s!==null&&typeof s=="object"&&!Array.isArray(s)?e in s:!1}};function Pe(r){return{name:r.name,schema:r.schema,methods:r.methods,indexes:r.indexes,timeSeries:r.timeSeries,trackRevisions:!1}}function M(r,e,t={}){let s=new L(t.uri||process.env.MONGO_URI||"mongodb://mongo:27017"),n=s.db(r),i={},o=[];for(let d in e){let a=e[d],m=a.name||d,h=n.collection(m),p=a.trackRevisions?n.collection(`${m}_revisions`):void 0,g=new b(h,a.schema,p),c=a.methods?.({client:s,db:n,collection:h,base:g})||{};o.push(async()=>{if(a.timeSeries){let f=await n.listCollections({name:m},{nameOnly:!1}).next();if(!f)await n.createCollection(m,{timeseries:{timeField:a.timeSeries.timeField,...a.timeSeries.metaField?{metaField:a.timeSeries.metaField}:{},...a.timeSeries.granularity?{granularity:a.timeSeries.granularity}:{}},...typeof a.timeSeries.expireAfterSeconds=="number"?{expireAfterSeconds:a.timeSeries.expireAfterSeconds}:{}});else{if(!(f.type==="timeseries"||!!f.options?.timeseries))throw new Error(`Collection ${m} already exists and is not a time-series collection. Migration is required before enabling timeSeries.`);let T=f.options?.timeseries;if(T?.timeField&&T.timeField!==a.timeSeries.timeField)throw new Error(`Collection ${m} has timeField '${T.timeField}' but config expects '${a.timeSeries.timeField}'.`);if(a.timeSeries.metaField&&T?.metaField&&T.metaField!==a.timeSeries.metaField)throw new Error(`Collection ${m} has metaField '${T.metaField}' but config expects '${a.timeSeries.metaField}'.`)}}if(a.indexes&&a.indexes.length>0)for(let f of a.indexes){let{key:y,...T}=f;await h.createIndex(y,T).catch(N=>{console.error(`Failed to create index on ${m}: ${N.message}`)})}}),i[d]=Object.assign(g,c)}let l=null,u=async()=>(l||(l=(async()=>{for(let d of o)await d()})()),l);return{client:s,db:n,collections:i,...i,disconnect:async()=>s.close(),connect:async()=>(await s.connect(),await u(),s)}}import J,{format as w,transports as _}from"winston";var D=r=>J.createLogger({format:w.json({bigint:!0}),defaultMeta:{service:r},transports:[new _.Console({format:w.combine(w.colorize(),w.timestamp(),w.printf(({level:e,message:t,timestamp:s})=>`${s} [${r}] ${e}: ${t}`))})]});import*as P from"crypto";function E(r){if(r%2!==0)throw new Error("Length must be an even number");let e=r/2;return P.randomBytes(e).toString("hex")}function O(r){let e=Math.floor(r/1e3%60),t=Math.floor(r/(1e3*60)%60),s=Math.floor(r/(1e3*60*60)%24);return{days:Math.floor(r/(1e3*60*60*24)),hours:s,minutes:t,seconds:e}}import q from"cors";var F=class{name=process.env.SERVICE_NAME??E(12);url=process.env.SERVICE_URL??"http://localhost";port=process.env.SERVICE_PORT?Number(process.env.SERVICE_PORT):3100;workerCount=1;handlers={};loadedEventListeners=[];routers=[];startedAt=Date.now();dependencies=new Map;dependencyOrder=[];isWorkerRunning=!1;serviceStreamSubscriptions=[];_status="starting";HEARTBEAT_INTERVAL=1e3*5;authStrategies=[];authResolverOverride;useDefaultHeaderAuthFallback=!0;expressOptions={corsWhitelist:["*"],asJson:!0,customMiddleware:[]};withoutExpressFlag=!1;getAuthResolver(){return this.authResolverOverride?this.authResolverOverride:A(this.authStrategies,{fallbackResolver:this.useDefaultHeaderAuthFallback?e=>v(e):void 0})}get streamKey(){return this.simpleName+":events"}get simpleName(){return this.name.split("-")[0]}get fullUrl(){return`${this.url}:${this.port}`}get api(){if(this.withoutExpressFlag)throw new Error("Express server is disabled. Service built without Express.");return this.dependencies.has("expressApp")||this.dependencies.set("expressApp",$()),this.dependencies.get("expressApp")}get log(){this.dependencies.has("logger")||this.dependencies.set("logger",D(this.name));let e=this.dependencies.get("logger");if(!(e instanceof Z))throw new Error("Logger is not of type Logger.");return e}get routes(){return this.routers.flatMap(e=>e.routes?.map(t=>{let s=e.base==="/"?"":e.base,n=t.fullPath==="/"?"":t.fullPath;return{path:`${s}${n}`==""?"/":`${s}${n}`,method:t.method,requireAuth:t.requireAuth??!1,meta:t.meta}})||[])}get status(){return this._status}get mongo(){if(!this.dependencies.has("mongoClient"))throw new Error("MongoDB module has not been added.");return this.dependencies.get("mongoClient")}get db(){if(!this.dependencies.has("mongoClient"))throw new Error("MongoDB module has not been added.");return this.mongo.collections}get clickhouse(){if(!this.dependencies.has("clickhouse"))throw new Error("Clickhouse module has not been added.");return this.dependencies.get("clickhouse")}get redis(){if(!this.dependencies.has("redis"))throw new Error("Redis module has not been added.");return this.dependencies.get("redis")}async register(){if(!this.dependencies.has("redis")){this.log.warn("Redis module not initialized. Call withRedis() to enable service registration.");return}let e=`services:registry:${this.name}`,t=JSON.stringify({name:this.name,streamKey:this.streamKey,url:this.fullUrl,dependencies:Object.keys(this.dependencies),routes:this.routes,lastHeartbeat:Date.now()});await this.redis.set(e,t,{EX:30})}async heartbeat(){if(!this.dependencies.has("redis")){this.log.warn("Redis module not initialized. Call withRedis() to enable heartbeat.");return}setInterval(async()=>{await this.register()},this.HEARTBEAT_INTERVAL)}build(){try{if(this._status="starting",Promise.allSettled(this.dependencyOrder.filter(e=>e!=="customDependencies").map(e=>{switch(e){case"redis":{this.dependencies.has("workers")?(this.loadHandlers(),this.runWorkers()):this.log.warn("Worker module not initialized. Call withWorkers() to enable worker threads and dynamically load handlers."),this.redis.connect().then(async()=>{this.log.info("Redis module loaded"),this.register(),this.log.info("Service registered in Redis"),this.heartbeat(),this.log.info("Heartbeat started"),process.on("SIGINT",()=>{this.log.info("SIGINT received. Shutting down gracefully..."),this.shutdown()})}).catch(t=>{this.log.error(`Failed to load Redis module: ${t}`)});break}case"mongoClient":{this.log.info("Connecting to MongoDB..."),this.mongo.connect().then(()=>{this.log.info("Mongo module loaded")}).catch(t=>{throw this.log.error(`Failed to connect to MongoDB: ${t}`),t});break}case"clickhouse":{this.log.info("Connecting to ClickHouse..."),this.clickhouse.ensureTables().then(()=>{this.log.info("ClickHouse module loaded")}).catch(t=>{this.log.error(`Failed to load ClickHouse module: ${t}`)});break}default:this.log.info(`Unknown module ${e} during initialization.`)}})).then(e=>{if(e.forEach((t,s)=>{let n=this.dependencyOrder[s];t.status==="fulfilled"?this.log.info(`Module "${n}" initialized successfully.`):this.log.error(`Failed to initialize module "${n}": ${t.reason}`)}),this.dependencies.has("customDependencies")){let t=this.dependencies.get("customDependencies");for(let s of t)try{s.init(this.log)}catch(n){this.log.error(`Failed to initialize custom module "${s.name}": ${n}`)}}}),this.withoutExpressFlag)this.log.warn("Express server is disabled. Service built without Express.");else{this.expressOptions.asJson&&(this.log.info("Configuring Express to parse JSON bodies"),this.api.use($.json()));for(let e of this.expressOptions.customMiddleware||[])this.log.info("Adding custom middleware to Express"),this.api.use(e);"corsWhitelist"in this.expressOptions?(this.log.info("Configuring CORS with whitelist: "+this.expressOptions.corsWhitelist),this.api.use(q({origin:this.expressOptions.corsWhitelist,credentials:this.expressOptions.credentials}))):"corsFn"in this.expressOptions&&(this.log.info("Configuring CORS with function"),this.api.use(q({origin:this.expressOptions.corsFn,credentials:this.expressOptions.credentials}))),this.expressOptions.omitDefaultRoutes||this.registerDefaultRoutes(),this.log.info(`Registering ${this.routers.length} routers`),this.routers.forEach(({base:e,router:t})=>{this.api.use(e,t)}),this.log.info("Routers registered:"+this.routers.map(e=>{let t=e.routes?Math.max(...e.routes?.map(i=>i.method.length)):0,s=e.routes?Math.max(...e.routes?.map(i=>i.fullPath.length)):0,n=e.routes?Math.max(...e.routes?.map(i=>i.requireAuth?6:8)):0;return`
|
|
2
2
|
Router ${e.base}: ${e.routes?.length||0} routes registered.
|
|
3
|
-
${e.routes?.map(i=>` ${("["+i.method+"]").padEnd(t+2)} ${(i.requireAuth?"[Auth]":"[Public]").padEnd(
|
|
3
|
+
${e.routes?.map(i=>` ${("["+i.method+"]").padEnd(t+2)} ${(i.requireAuth?"[Auth]":"[Public]").padEnd(n)} ${i.fullPath.padEnd(s)} | ${i.meta?.description||""}`).join(`
|
|
4
4
|
`)||""}`}).join(`
|
|
5
|
-
`)),this.api.listen(this.port,()=>{this.log.info(`API listening at ${this.fullUrl}`)})}return this._status="running",this}catch(e){throw this.log.error(`Failed to build service: ${e}`),e}}loadHandlers(){if(!this.dependencies.has("redis")){this.log.warn("Redis module not initialized. Skipping handler loading.");return}let e=b.resolve(process.cwd(),"src/handlers");if(!J(e)){this.log.warn("Handlers directory does not exist.");return}let t=_(e).filter(s=>s.endsWith(".ts")||s.endsWith(".js"));for(let s of t){let r=b.join(e,s);this.handlers[b.basename(s,b.extname(s))]=r}this.log.info(`Loaded ${Object.keys(this.handlers).length} handlers:`);for(let s of Object.keys(this.handlers))this.log.info(`- ${s}`)}registerDefaultRoutes(){this.initBaseRoutes(),this.dependencies.has("redis")&&this.dependencies.has("workers")&&(this.log.info("Redis and handler modules detected. Registering worker routes."),this.initWorkerRoutes()),this.log.info("Default routes registered.")}initBaseRoutes(){this.log.info("Registering base routes");let{router:e,routes:t,addRoute:s}=w({authResolver:this.getAuthResolver()});s({method:"GET",fullPath:"/",requireAuth:!1,meta:{description:"Base route"}},(r,i)=>{i.send('OK from "'+this.name+'" service')}),s({method:"GET",fullPath:"/health",requireAuth:!1,meta:{description:"Health check route that returns service status and uptime"}},(r,i)=>{i.status(200).json({service:this.name,status:this.status,startedAt:new Date(this.startedAt),uptime:E(Math.floor(process.uptime()*1e3)),workerRunning:this.isWorkerRunning,modulesLoaded:Array.from(this.dependencies.keys()).filter(o=>o!=="customDependencies").concat(Array.from(this.dependencies.has("customDependencies")?this.dependencies.get("customDependencies").map(o=>`custom:${o.name}`):[])),routesLoaded:this.routes,handlersLoaded:this.loadedEventListeners})}),this.internalAddRouter("/",e,t)}initWorkerRoutes(){let{router:e,routes:t,addRoute:s}=w({authResolver:this.getAuthResolver()});s({method:"GET",fullPath:"/",requireAuth:!0,meta:{description:"Get worker status"}},(r,i)=>{i.json({status:this.isWorkerRunning?"Running":"Stopped",handlersLoaded:Object.keys(this.handlers)})}),s({method:"GET",fullPath:"/:stream",requireAuth:!0,meta:{description:"Get stream messages"}},async(r,i)=>{if(!this.redis)return i.status(500).json({error:"Redis client not connected"});let o=await this.redis.xRange(this.streamKey,"-","+",{COUNT:10});i.json({status:"Stream",messages:o})}),s({method:"POST",fullPath:"/:stream/:id/ack",requireAuth:!0,meta:{description:"Acknowledge a message in the stream"}},async(r,i)=>{let{stream:o,id:a}=r.params;if(!this.redis)return i.status(500).json({error:"Redis client not connected"});let u=await this.redis.xAck(o,`${this.simpleName}:consumer-group`,a);i.json({status:"Message acknowledged",result:u})}),s({method:"GET",fullPath:"/:stream/dlq",requireAuth:!0,meta:{description:"Get DLQ stream messages"}},async(r,i)=>{if(!this.redis)return i.status(500).json({error:"Redis client not connected"});let o=await this.redis.xRange(this.streamKey,"-","+",{COUNT:10});i.json({status:"DLQ Stream",messages:o})}),s({method:"POST",fullPath:"/:stream/dlq/:id/retry",requireAuth:!0,meta:{description:"Retry a message in the DLQ"}},async(r,i)=>{let{stream:o,id:a}=r.params;if(!this.redis)return i.status(500).json({error:"Redis client not connected"});let u=await this.redis.xRange(o,a,a);if(u.length===0||u[0].id!==a)return i.status(404).json({error:"Message not found in DLQ"});let d=await this.redis.xAdd(o,`${this.simpleName}:consumer-group`,u[0].message);i.json({status:"Message retried",result:d})}),this.internalAddRouter("/workers",e,t)}runWorkers({workerCount:e}={workerCount:1}){if(!this.name){this.log.error("Service name is not set. Cannot start workers.");return}if(!this.dependencies.has("redis")){this.log.warn("Redis module not initialized. Skipping worker setup.");return}if(isNaN(e)||e<1)throw new Error("Worker count must be a number greater than 0.");this.workerCount=e<1?1:e;for(let t=0;t<e;t++){let s=new K(new URL("./worker-thread.js",import.meta.url),{workerData:{name:this.name,redisUrl:this.redis.options?.url,handlers:this.handlers,index:t,subscriptions:this.serviceStreamSubscriptions??[`${this.name}:events`]}});this.isWorkerRunning=!0,s.on("error",r=>{this.log.error(`Worker ${t} error: ${r.message}`)}),s.on("message",r=>{switch(r.level){case"info":this.log.info(r.message);break;case"warn":this.log.warn(r.message);break;case"error":this.log.error(r.message);break;case"create":this.loadedEventListeners=r.message;break;default:this.log.info(r.message)}}),s.on("exit",r=>{r!==0?this.log.error(`Worker ${t} stopped with exit code ${r}`):this.log.info(`Worker ${t} exited gracefully.`),this.isWorkerRunning=!1}),this.log.info(`Worker ${t} started for service ${this.name}`)}}withMongo(e){if(!this.dependencies.has("mongoClient")){let t=k(e.dbName,e.collections,{uri:e.uri});this.dependencies.set("mongoClient",t),this.dependencyOrder.push("mongoClient")}return this}withClickhouse(e){return this.dependencies.has("clickhouse")||(this.dependencies.set("clickhouse",A(e)),this.dependencyOrder.push("clickhouse")),this}withPort(e=Number(process.env.PORT)||3100){return this.port=e,this}withUrl(e){return this.url=e,this}withName(e){return this.name=e,this}withRedis(e="redis://redis:6379"){return this.dependencies.has("redis")||(this.dependencies.set("redis",z({url:e})),this.dependencyOrder.push("redis")),this}withWorkers(e,t=[]){return this.dependencies.has("redis")?(!this.dependencies.has("workers")&&t.length>0&&(this.dependencies.set("workers",!0),this.serviceStreamSubscriptions=t,this.workerCount=e<1?1:e,this.dependencyOrder.push("workers")),this):(this.log.warn("Redis module not initialized. Call withRedis() before setting worker subscriptions."),this)}withModules(e){return this.dependencies.has("customDependencies")||(this.dependencies.set("customDependencies",e.map(t=>({name:t.name,init:t.init,shutdown:t.shutdown}))),this.dependencyOrder.push("customDependencies")),this}withoutExpress(){return this.withoutExpressFlag=!0,this}withExpressOptions(e){return this.withoutExpressFlag?(this.log.warn("Express server is disabled. Express options will not be applied."),this):(this.expressOptions={...this.expressOptions,...e},this)}withAuthStrategy(e){return this.withoutExpressFlag?(this.log.warn("Express server is disabled. Auth strategies will not be applied."),this):(this.authStrategies.push(e),this)}withAuthStrategies(e){return this.withoutExpressFlag?(this.log.warn("Express server is disabled. Auth strategies will not be applied."),this):(this.authStrategies.push(...e),this)}withAuthResolver(e){return this.withoutExpressFlag?(this.log.warn("Express server is disabled. Auth resolver will not be applied."),this):(this.authResolverOverride=e,this)}withoutDefaultHeaderAuthFallback(){return this.useDefaultHeaderAuthFallback=!1,this}internalAddRouter(e,t,s=[]){return this.routers.push({base:e,router:t,routes:s}),this}addRouter(e,t,s=[]){return this.withoutExpressFlag?(this.log.warn("Express server is disabled. Routers will not be registered."),this):e==="/"?(this.log.warn("Base path '/' is not allowed. Skipping."),this):(this.internalAddRouter(e,t,s),this)}async shutdown(e={}){if(await e.beforeShutdown?.(),this._status="stopping",this.dependencies.has("redis")&&await this.redis.quit(),this.dependencies.has("mongoClient")&&await this.mongo.disconnect(),this.dependencies.has("clickhouse")&&await this.clickhouse.client.close(),this.dependencies.has("customDependencies")){for(let t of this.dependencies.get("customDependencies"))if(t.shutdown)try{let s=await t.shutdown(this.log);this.log.info(`Custom dependency "${t.name}" shutdown with result: ${s}`)}catch(s){this.log.error(`Failed to shutdown custom dependency "${t.name}": ${s}`)}}this.log.info(`Service ${this.name} shutting down.`),this._status="stopped",await e.afterShutdown?.(),process.exit(0)}[Symbol.dispose](){this.shutdown()}*[Symbol.iterator](){for(let e of Object.values(this.dependencies))yield e}};function nt(n,e){return{type:n,execute:e}}import{createRemoteJWKSet as Z,jwtVerify as U}from"jose";function G(n,e,t){let s=n.headers[e.toLowerCase()];if(typeof s!="string")return null;let r=s.trim();if(!r)return null;if(!t)return r;let i=`${t} `;return r.startsWith(i)&&r.slice(i.length).trim()||null}function Q(n){return typeof n=="string"||typeof n=="number"||typeof n=="boolean"||Array.isArray(n)&&n.every(e=>typeof e=="string")}function X(n){if(typeof n.sub!="string"||n.sub.length===0)return null;let e={userId:n.sub};for(let[t,s]of Object.entries(n))t!=="sub"&&Q(s)&&(e[t]=s);return e}function j(n){let e={};for(let[t,s]of Object.entries(n))e[t]=s;return typeof n.sub=="string"&&(e.sub=n.sub),e}function I(n,e){return e.every(t=>n[t]!==void 0)}function at(n){let e=n.requiredClaims??["sub"],t={issuer:n.issuer,audience:n.audience,algorithms:n.algorithms,clockTolerance:n.clockTolerance};if(n.jwksUri){let r=Z(new URL(n.jwksUri));return async i=>{try{let{payload:o}=await U(i,r,t);return I(o,e)?j(o):null}catch{return null}}}let s=typeof n.secret=="string"?new TextEncoder().encode(n.secret):n.secret;if(!(s instanceof Uint8Array))throw new Error("createJoseJwtVerifier requires either jwksUri or secret.");return async r=>{try{let{payload:i}=await U(r,s,t);return I(i,e)?j(i):null}catch{return null}}}function Y(n){return n?n.split(",").map(e=>e.trim()).filter(Boolean):[]}function ee(n){let e=Y(n.subjectaltname),t=e.find(i=>i.startsWith("URI:"));if(t)return t.slice(4).trim()||null;let s=e.find(i=>i.startsWith("DNS:"));if(s)return s.slice(4).trim()||null;let r=n.subject?.CN;if(typeof r=="string"&&r.length>0)return r;if(Array.isArray(r)&&r.length>0){let i=r[0];if(typeof i=="string"&&i.length>0)return i}return n.fingerprint256?n.fingerprint256:null}function ut(n){let e=n.headerName??"authorization",t=n.tokenPrefix??"Bearer";return{name:n.name??"jwt",canHandle:n.canHandle,authenticate:async(s,r)=>{let i=G(s,e,t);if(!i)return null;let o=await n.verifyToken(i,s,r);return o?n.mapPayloadToUser?n.mapPayloadToUser(o,s,r):X(o):null}}}function lt(n={}){let e=n.requireAuthorized??!0;return{name:n.name??"mtls",canHandle:n.canHandle,authenticate:async(t,s)=>{let r=t.socket;if(typeof r.getPeerCertificate!="function"||e&&r.authorized!==!0)return null;let i=r.getPeerCertificate(!0);if(!i||Object.keys(i).length===0)return null;if(n.mapCertificateToUser)return n.mapCertificateToUser(i,t,s);let o=ee(i);return o?{userId:o,certSubjectCn:i.subject?.CN??"",certIssuerCn:i.issuer?.CN??"",certFingerprint256:i.fingerprint256??"",certSerialNumber:i.serialNumber??"",mtlsAuthorized:r.authorized}:null}}}export{x as CollectionWrapper,$ as Service,S as createAuthResolver,at as createJoseJwtVerifier,ut as createJwtAuthStrategy,de as createMetaRouter,w as createMetaRouterWithAuth,k as createMongo,lt as createMtlsAuthStrategy,v as defaultHeaderAuthResolver,Me as defineMongoCollection,le as getRegisteredRoutes,nt as redisStreamHandler};
|
|
5
|
+
`)),this.api.listen(this.port,()=>{this.log.info(`API listening at ${this.fullUrl}`)})}return this._status="running",this}catch(e){throw this.log.error(`Failed to build service: ${e}`),e}}loadHandlers(){if(!this.dependencies.has("redis")){this.log.warn("Redis module not initialized. Skipping handler loading.");return}let e=S.resolve(process.cwd(),"src/handlers");if(!z(e)){this.log.warn("Handlers directory does not exist.");return}let t=V(e).filter(s=>s.endsWith(".ts")||s.endsWith(".js"));for(let s of t){let n=S.join(e,s);this.handlers[S.basename(s,S.extname(s))]=n}this.log.info(`Loaded ${Object.keys(this.handlers).length} handlers:`);for(let s of Object.keys(this.handlers))this.log.info(`- ${s}`)}registerDefaultRoutes(){this.initBaseRoutes(),this.dependencies.has("redis")&&this.dependencies.has("workers")&&(this.log.info("Redis and handler modules detected. Registering worker routes."),this.initWorkerRoutes()),this.log.info("Default routes registered.")}initBaseRoutes(){this.log.info("Registering base routes");let{router:e,routes:t,addRoute:s}=R({authResolver:this.getAuthResolver()});s({method:"GET",fullPath:"/",requireAuth:!1,meta:{description:"Base route"}},(n,i)=>{i.send('OK from "'+this.name+'" service')}),s({method:"GET",fullPath:"/health",requireAuth:!1,meta:{description:"Health check route that returns service status and uptime"}},(n,i)=>{i.status(200).json({service:this.name,status:this.status,startedAt:new Date(this.startedAt),uptime:O(Math.floor(process.uptime()*1e3)),workerRunning:this.isWorkerRunning,modulesLoaded:Array.from(this.dependencies.keys()).filter(o=>o!=="customDependencies").concat(Array.from(this.dependencies.has("customDependencies")?this.dependencies.get("customDependencies").map(o=>`custom:${o.name}`):[])),routesLoaded:this.routes,handlersLoaded:this.loadedEventListeners})}),this.internalAddRouter("/",e,t)}initWorkerRoutes(){let{router:e,routes:t,addRoute:s}=R({authResolver:this.getAuthResolver()});s({method:"GET",fullPath:"/",requireAuth:!0,meta:{description:"Get worker status"}},(n,i)=>{i.json({status:this.isWorkerRunning?"Running":"Stopped",handlersLoaded:Object.keys(this.handlers)})}),s({method:"GET",fullPath:"/:stream",requireAuth:!0,meta:{description:"Get stream messages"}},async(n,i)=>{if(!this.redis)return i.status(500).json({error:"Redis client not connected"});let o=await this.redis.xRange(this.streamKey,"-","+",{COUNT:10});i.json({status:"Stream",messages:o})}),s({method:"POST",fullPath:"/:stream/:id/ack",requireAuth:!0,meta:{description:"Acknowledge a message in the stream"}},async(n,i)=>{let{stream:o,id:l}=n.params;if(!this.redis)return i.status(500).json({error:"Redis client not connected"});let u=await this.redis.xAck(o,`${this.simpleName}:consumer-group`,l);i.json({status:"Message acknowledged",result:u})}),s({method:"GET",fullPath:"/:stream/dlq",requireAuth:!0,meta:{description:"Get DLQ stream messages"}},async(n,i)=>{if(!this.redis)return i.status(500).json({error:"Redis client not connected"});let o=await this.redis.xRange(this.streamKey,"-","+",{COUNT:10});i.json({status:"DLQ Stream",messages:o})}),s({method:"POST",fullPath:"/:stream/dlq/:id/retry",requireAuth:!0,meta:{description:"Retry a message in the DLQ"}},async(n,i)=>{let{stream:o,id:l}=n.params;if(!this.redis)return i.status(500).json({error:"Redis client not connected"});let u=await this.redis.xRange(o,l,l);if(u.length===0||u[0].id!==l)return i.status(404).json({error:"Message not found in DLQ"});let d=await this.redis.xAdd(o,`${this.simpleName}:consumer-group`,u[0].message);i.json({status:"Message retried",result:d})}),this.internalAddRouter("/workers",e,t)}runWorkers({workerCount:e}={workerCount:1}){if(!this.name){this.log.error("Service name is not set. Cannot start workers.");return}if(!this.dependencies.has("redis")){this.log.warn("Redis module not initialized. Skipping worker setup.");return}if(isNaN(e)||e<1)throw new Error("Worker count must be a number greater than 0.");this.workerCount=e<1?1:e;for(let t=0;t<e;t++){let s=new G(new URL("./worker-thread.js",import.meta.url),{workerData:{name:this.name,redisUrl:this.redis.options?.url,handlers:this.handlers,index:t,subscriptions:this.serviceStreamSubscriptions??[`${this.name}:events`]}});this.isWorkerRunning=!0,s.on("error",n=>{this.log.error(`Worker ${t} error: ${n.message}`)}),s.on("message",n=>{switch(n.level){case"info":this.log.info(n.message);break;case"warn":this.log.warn(n.message);break;case"error":this.log.error(n.message);break;case"create":this.loadedEventListeners=n.message;break;default:this.log.info(n.message)}}),s.on("exit",n=>{n!==0?this.log.error(`Worker ${t} stopped with exit code ${n}`):this.log.info(`Worker ${t} exited gracefully.`),this.isWorkerRunning=!1}),this.log.info(`Worker ${t} started for service ${this.name}`)}}withMongo(e){if(!this.dependencies.has("mongoClient")){let t=M(e.dbName,e.collections,{uri:e.uri});this.dependencies.set("mongoClient",t),this.dependencyOrder.push("mongoClient")}return this}withClickhouse(e){return this.dependencies.has("clickhouse")||(this.dependencies.set("clickhouse",k(e)),this.dependencyOrder.push("clickhouse")),this}withPort(e=Number(process.env.PORT)||3100){return this.port=e,this}withUrl(e){return this.url=e,this}withName(e){return this.name=e,this}withRedis(e="redis://redis:6379"){return this.dependencies.has("redis")||(this.dependencies.set("redis",K({url:e})),this.dependencyOrder.push("redis")),this}withWorkers(e,t=[]){return this.dependencies.has("redis")?(!this.dependencies.has("workers")&&t.length>0&&(this.dependencies.set("workers",!0),this.serviceStreamSubscriptions=t,this.workerCount=e<1?1:e,this.dependencyOrder.push("workers")),this):(this.log.warn("Redis module not initialized. Call withRedis() before setting worker subscriptions."),this)}withModules(e){return this.dependencies.has("customDependencies")||(this.dependencies.set("customDependencies",e.map(t=>({name:t.name,init:t.init,shutdown:t.shutdown}))),this.dependencyOrder.push("customDependencies")),this}withoutExpress(){return this.withoutExpressFlag=!0,this}withExpressOptions(e){return this.withoutExpressFlag?(this.log.warn("Express server is disabled. Express options will not be applied."),this):(this.expressOptions={...this.expressOptions,...e},this)}withAuthStrategy(e){return this.withoutExpressFlag?(this.log.warn("Express server is disabled. Auth strategies will not be applied."),this):(this.authStrategies.push(e),this)}withAuthStrategies(e){return this.withoutExpressFlag?(this.log.warn("Express server is disabled. Auth strategies will not be applied."),this):(this.authStrategies.push(...e),this)}withAuthResolver(e){return this.withoutExpressFlag?(this.log.warn("Express server is disabled. Auth resolver will not be applied."),this):(this.authResolverOverride=e,this)}withoutDefaultHeaderAuthFallback(){return this.useDefaultHeaderAuthFallback=!1,this}internalAddRouter(e,t,s=[]){return this.routers.push({base:e,router:t,routes:s}),this}addRouter(e,t,s=[]){return this.withoutExpressFlag?(this.log.warn("Express server is disabled. Routers will not be registered."),this):e==="/"?(this.log.warn("Base path '/' is not allowed. Skipping."),this):(this.internalAddRouter(e,t,s),this)}async shutdown(e={}){if(await e.beforeShutdown?.(),this._status="stopping",this.dependencies.has("redis")&&await this.redis.quit(),this.dependencies.has("mongoClient")&&await this.mongo.disconnect(),this.dependencies.has("clickhouse")&&await this.clickhouse.client.close(),this.dependencies.has("customDependencies")){for(let t of this.dependencies.get("customDependencies"))if(t.shutdown)try{let s=await t.shutdown(this.log);this.log.info(`Custom dependency "${t.name}" shutdown with result: ${s}`)}catch(s){this.log.error(`Failed to shutdown custom dependency "${t.name}": ${s}`)}}this.log.info(`Service ${this.name} shutting down.`),this._status="stopped",await e.afterShutdown?.(),process.exit(0)}[Symbol.dispose](){this.shutdown()}*[Symbol.iterator](){for(let e of Object.values(this.dependencies))yield e}};function it(r,e){return{type:r,execute:e}}import{createRemoteJWKSet as Q,jwtVerify as U}from"jose";function X(r,e,t){let s=r.headers[e.toLowerCase()];if(typeof s!="string")return null;let n=s.trim();if(!n)return null;if(!t)return n;let i=`${t} `;return n.startsWith(i)&&n.slice(i.length).trim()||null}function Y(r){return typeof r=="string"||typeof r=="number"||typeof r=="boolean"||Array.isArray(r)&&r.every(e=>typeof e=="string")}function ee(r){if(typeof r.sub!="string"||r.sub.length===0)return null;let e={userId:r.sub};for(let[t,s]of Object.entries(r))t!=="sub"&&Y(s)&&(e[t]=s);return e}function j(r){let e={};for(let[t,s]of Object.entries(r))e[t]=s;return typeof r.sub=="string"&&(e.sub=r.sub),e}function I(r,e){return e.every(t=>r[t]!==void 0)}function ut(r){let e=r.requiredClaims??["sub"],t={issuer:r.issuer,audience:r.audience,algorithms:r.algorithms,clockTolerance:r.clockTolerance};if(r.jwksUri){let n=Q(new URL(r.jwksUri));return async i=>{try{let{payload:o}=await U(i,n,t);return I(o,e)?j(o):null}catch{return null}}}let s=typeof r.secret=="string"?new TextEncoder().encode(r.secret):r.secret;if(!(s instanceof Uint8Array))throw new Error("createJoseJwtVerifier requires either jwksUri or secret.");return async n=>{try{let{payload:i}=await U(n,s,t);return I(i,e)?j(i):null}catch{return null}}}function te(r){return r?r.split(",").map(e=>e.trim()).filter(Boolean):[]}function se(r){let e=te(r.subjectaltname),t=e.find(i=>i.startsWith("URI:"));if(t)return t.slice(4).trim()||null;let s=e.find(i=>i.startsWith("DNS:"));if(s)return s.slice(4).trim()||null;let n=r.subject?.CN;if(typeof n=="string"&&n.length>0)return n;if(Array.isArray(n)&&n.length>0){let i=n[0];if(typeof i=="string"&&i.length>0)return i}return r.fingerprint256?r.fingerprint256:null}function dt(r){let e=r.headerName??"authorization",t=r.tokenPrefix??"Bearer";return{name:r.name??"jwt",canHandle:r.canHandle,authenticate:async(s,n)=>{let i=X(s,e,t);if(!i)return null;let o=await r.verifyToken(i,s,n);return o?r.mapPayloadToUser?r.mapPayloadToUser(o,s,n):ee(o):null}}}function ct(r={}){let e=r.requireAuthorized??!0;return{name:r.name??"mtls",canHandle:r.canHandle,authenticate:async(t,s)=>{let n=t.socket;if(typeof n.getPeerCertificate!="function"||e&&n.authorized!==!0)return null;let i=n.getPeerCertificate(!0);if(!i||Object.keys(i).length===0)return null;if(r.mapCertificateToUser)return r.mapCertificateToUser(i,t,s);let o=se(i);return o?{userId:o,certSubjectCn:i.subject?.CN??"",certIssuerCn:i.issuer?.CN??"",certFingerprint256:i.fingerprint256??"",certSerialNumber:i.serialNumber??"",mtlsAuthorized:n.authorized}:null}}}export{b as CollectionWrapper,F as Service,A as createAuthResolver,ut as createJoseJwtVerifier,dt as createJwtAuthStrategy,he as createMetaRouter,R as createMetaRouterWithAuth,M as createMongo,ct as createMtlsAuthStrategy,v as defaultHeaderAuthResolver,Pe as defineMongoCollection,ce as getRegisteredRoutes,it as redisStreamHandler};
|
|
6
6
|
//# sourceMappingURL=index.js.map
|