@gnar-engine/core 1.0.6 → 1.0.7

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.
Files changed (2) hide show
  1. package/dist/core.js +1 -1
  2. package/package.json +1 -1
package/dist/core.js CHANGED
@@ -1 +1 @@
1
- import oe from"fastify";import se from"@fastify/cors";import ne from"@fastify/rate-limit";import"pino";var n={cloudProjectId:"",serviceName:"",logs:[],transports:[],flushIntervalMs:5e3,timer:null,init:({cloudProjectId:e,serviceName:t,transports:o,flushIntervalMs:r})=>{if(n.cloudProjectId=e||"",!t)throw new Error("Service name is required for logger initialization");n.serviceName=t,n.transports=o||[],n.flushIntervalMs=r||5e3,n.startFlushTimer()},info:(e,t,o,r)=>{n.addLog({args1:e,args2:t,args3:o,args4:r,level:"info"})},warning:(e,t,o,r)=>{n.addLog({args1:e,args2:t,args3:o,args4:r,level:"warning"})},error:(e,t,o,r)=>{n.addLog({args1:e,args2:t,args3:o,args4:r,level:"error"})},table:(e,t,o,r)=>{n.addLog({args1:e,args2:t,args3:o,args4:r,level:"table"})},testResult:(e,t,o)=>{n.addLog({args1:o,level:"test_result",testResult:t,test:e})},addTransport:e=>{},addLog:({args1:e,args2:t,args3:o,args4:r,level:s,testResult:a,test:i})=>{let u=[e,t,o,r].filter(p=>p!==void 0).map(p=>{if(typeof p=="object")try{return JSON.stringify(p)}catch{return"[Object]"}else return String(p)}).join(" "),m={cloudProjectId:n.cloudProjectId||null,service:n.serviceName,timestamp:Date.now(),level:s||"info",message:u,testResult:a||null,test:i||null};if(n.transports.length==0)if(m.level==="table"){try{let p=JSON.parse(m.message);console.table(p)}catch{console.log("[table] unable to parse table data:",m.message)}return}else console.log(n.formatForConsole(m));else n.logs.push(m)},startFlushTimer(){n.timer&&clearInterval(n.timer),n.timer=setInterval(()=>n.flush(),n.flushIntervalMs)},flush:()=>{n.transports.forEach(e=>{}),n.logs=[]},formatForConsole:e=>{let o={error:"\x1B[31m",warning:"\x1B[33m",reset:"\x1B[0m"},r=new Date(e.timestamp).toISOString(),a=(e.level||"info").toUpperCase().padEnd(12),i=`${r} ${a} \u2014 ${e.message}`,l=o[e.level]||"",u=l?o.reset:"";return`${l}${i}${u}`}};var v=null,H=3e3,W={init:e=>{v=oe({}),H=e.port||3e3,v.register(se,{origin:e.allowedOrigins||[],methods:e.allowedMethods||["GET","POST","PUT","DELETE"],allowedHeaders:e.allowedHeaders||["Content-Type","Authorization"],preflight:!0,optionsSuccessStatus:204,credentials:!0}),e?.rateLimiting?.max&&e?.rateLimiting?.timeWindow&&v.register(ne,e.rateLimiting)},registerRoutes:({controllers:e})=>{e.forEach(t=>{Object.values(t).forEach(o=>{v.route(o)})}),n.info("Control service registered http routes "+v.printRoutes())},addHook:(e,t)=>{v.addHook(e,t)},start:async()=>{try{await v.listen({port:H,host:"0.0.0.0"}),n.info(`HTTP server started on port ${H}`)}catch(e){n.error("Error starting HTTP server: "+e),process.exit(1)}},setErrorHandler:e=>{v.setErrorHandler(e)}};import ae from"amqplib";var I="",R=null,$=null,U={arguments:{"x-queue-type":"quorum"}},P=e=>{I=e},T=async()=>{if(R)return R;try{console.log("Connecting to RabbitMQ:",I),R=await ae.connect(I)}catch(e){return console.log("Error connecting to RabbitMQ:",e),R=null,await new Promise(t=>setTimeout(t,3e3)),T({rabbitConnectionUrl:I})}return R},E=async()=>{if($)return $;R||await T();try{$=await R.createChannel()}catch(e){return console.error("Error creating RabbitMQ channel:",e),$=null,await new Promise(t=>setTimeout(t,3e3)),E()}return $},K=async(e,t,o)=>{let r=await E();return await r.assertQueue(e,U),await r.prefetch(t),(await r.consume(e,async a=>{a&&(async()=>{try{await o(a,r)}catch(i){console.error("Error handling message:",i),r.nack(a,!1,!1)}})()})).consumerTag},ie=async(e,t,o,r)=>{await T(),await E();let s=await E();return await s.assertQueue(e,U),await s.prefetch(t),r&&await s.cancel(r),K(e,t,o)},Q=async(e,t,o)=>{await T();let r=await E();await r.assertQueue(e,U),await r.prefetch(t);let s=await K(e,t,o);return R.on("close",async()=>{console.log("RabbitMQ connection closed, attempting to reconnect..."),await z(e,t,o,s)}),console.log("RabbitMQ connection established and consumer registered:",s),s},z=async(e,t,o,r)=>{console.log("Reconnecting to RabbitMQ...");let s=ie(e,t,o,r);return console.log("RabbitMQ connection re-established and consumer registered:",s),s},J={getChannel:E,getConnection:T,setConnectionUrl:P,initialize:Q,reinitialize:z};var G=async(e,t)=>{let o=`${e}Queue`,r;try{r=await E(),await r.assertQueue(o,{arguments:{"x-queue-type":"quorum"}});let s=ce(),a=s,i=await r.assertQueue("",{exclusive:!0});return new Promise((l,u)=>{let m=setTimeout(()=>{p(),u(new Error(`No response from ${e} service`))},5e3),p=()=>{r.cancel(a).catch(()=>{}),r.deleteQueue(i.queue).catch(()=>{}),clearTimeout(m)};r.consume(i.queue,A=>{A.properties.correlationId===s&&(p(),l(JSON.parse(A.content.toString())),r.ack(A))},{noAck:!1,consumerTag:a}).catch(u),r.sendToQueue(o,Buffer.from(JSON.stringify(t)),{replyTo:i.queue,correlationId:s})})}catch(s){if(r)try{await r.close()}catch(a){console.error("Failed to close channel:",a)}throw new Error(`Failed to send message to ${e}: ${s.message}`)}},O=async(e,t)=>{let o=`${e}Queue`,r;try{r=await E(),await r.assertQueue(o,{arguments:{"x-queue-type":"quorum"}}),r.sendToQueue(o,Buffer.from(JSON.stringify(t)))}catch(s){if(r)try{await r.close()}catch(a){console.error("Failed to close channel:",a)}throw new Error(`Failed to send message to ${e}: ${s.message}`)}},ce=()=>Math.random().toString(36).substring(7);import M,{WebSocketServer as le}from"ws";import{v4 as de}from"uuid";var N={wsMap:new Map,pendingCalls:[],async init(e,t){e.serviceName=t,this.startServer();let o=1;for(;;)try{e.serviceName!=="controlService"&&(n.info("serviceName "+JSON.stringify(e)),await this.connect("controlService",e));break}catch{o>=e.maxInitialConnectionAttempts?(n.error(`Initial WS connection to control service failed after ${o} attempts. Exiting.`),process.exit(1)):(n.error(`Initial WS connection to control service failed (attempt ${o}). Retrying in 3s...`),o++,await new Promise(s=>setTimeout(s,3e3)))}setInterval(async()=>{let r=[];try{r=await b.execute("controlService.getServices",{})}catch(a){n.error("Failed to get service registry. "+a);return}let s=r.map(a=>a.name===e.serviceName||this.wsMap[a.name]&&this.wsMap[a.name].readyState===M.OPEN?Promise.resolve():this.connect(a.name,e));try{await Promise.all(s)}catch{}},e.reconnectInterval||1e4)},startServer(e=5e3){new le({port:e}).on("connection",(o,r)=>{let s=this.identifyPeer(r);this.wsMap.set(s,o),o.on("message",a=>{this.handleMessage(s,a)}),o.on("close",()=>{n.info(`Peer disconnected: ${s}`),this.wsMap.delete(s)})})},async connect(e,t){let r=`ws://${e.replace("Service","-service").toLowerCase()}:5000`;return this.wsMap.has(e)&&this.wsMap.get(e).readyState===M.OPEN?this.wsMap.get(e):new Promise((s,a)=>{let i=new M(r,{headers:{"x-api-key":"my-secret-key","x-service-name":t.serviceName}});i.on("open",()=>{n.info(`Connected to peer ${e} at ${r}`),this.wsMap.set(e,i),i.on("message",l=>{this.handleMessage(e,l)}),i.on("close",()=>{n.info(`Peer disconnected: ${e}`),this.wsMap.delete(e)}),s(i)}),i.on("error",l=>{a(l)})})},async handleMessage(e,t){let o;try{o=JSON.parse(t)}catch{n.error("Invalid JSON from peer:",t);return}if(o.type==="request"&&o.commandName)try{let r=await b.execute(o.commandName,o.payload);this.sendResponse(e,o.messageId,r)}catch(r){this.sendResponse(e,o.messageId,null,r.message)}else if(o.type==="response"){let r=this.pendingCalls.findIndex(a=>a.messageId===o.messageId);if(r===-1){n.error(`No pending call for messageId ${o.messageId}`);return}let s=this.pendingCalls.splice(r,1)[0];clearTimeout(s.timeout),o.error?s.reject(new Error(o.error)):s.resolve(o.response)}else n.error("Unknown message type",o)},async send(e,t,o,r=1e4){let s=this.wsMap.get(e);if((!s||s.readyState!==M.OPEN)&&(s=await this.waitForWsReady(e)),!s||s.readyState!==M.OPEN)throw new Error(`WebSocket not connected to ${e}`);let a=de(),i={type:"request",messageId:a,commandName:t,payload:o};return s.send(JSON.stringify(i)),new Promise((l,u)=>{let m=setTimeout(()=>{this.pendingCalls=this.pendingCalls.filter(p=>p.messageId!==a),u(new Error(`Timeout waiting for response from ${e}`)),n.error(`Timeout waiting for response on call: ${JSON.stringify(i)}`)},r);this.pendingCalls.push({messageId:a,resolve:l,reject:u,timeout:m})})},sendResponse(e,t,o=null,r=null){let s=this.wsMap.get(e);!s||s.readyState!==M.OPEN||s.send(JSON.stringify({type:"response",messageId:t,response:o,error:r}))},identifyPeer(e){return e.headers["x-service-name"]||`unknown-${Date.now()}`},async waitForWsReady(e,t=50,o=5e3){let r=Date.now();for(;;){let s=this.wsMap.get(e);if(s&&s.readyState===M.OPEN)return s;if(Date.now()-r>o)throw new Error(`WebSocket not connected to ${e} after ${o}ms`);await new Promise(a=>setTimeout(a,t))}}};var w={manifest:{commandList:[],commandImplementations:{},schemas:{}},addCommand({commandName:e,handlerFunction:t}){w.manifest.commandImplementations[e]={function:t.toString()},w.manifest.commandList.push(e)},addSchema({schemaName:e,schema:t}){w.manifest.schemas[e]=t}};var b={config:{},handlers:new Map,init(e){this.config=e},register(e,t){this.handlers.set(e,t),w.addCommand({commandName:e,handlerFunction:t})},async execute(e,t,o={}){let{fireAndForget:r=!1}=o;if(this.config.architecture=="modular-monolith"){let s=this.handlers.get(e);if(!s)throw new Error(`Command "${e}" not registered`);return await s(t)}else{let[s,a]=e.split(".");if(!s||!a){let l=this.config.serviceName+"."+e,u=this.handlers.get(l);if(!u)throw new Error(`Command "${l}" not registered`);return await u(t)}if(s===this.config.serviceName){let l=this.handlers.get(e);if(!l)throw new Error(`Command "${e}" not registered`);return await l(t)}if(r){O(s,{method:e,data:t});return}return await N.send(s,e,t)}}};var V={routeHandlers:{},defaultHandlers:{runMigrations:async e=>{let t=e.data?.migration;await b.execute("runMigrations",{migration:t})},runSeeders:async e=>{let t=e.data?.seeder;await b.execute("runSeeders",{seeder:t})},healthCheck:async()=>({status:"ok"})},async init({config:e,handlers:t={}}){if(!e?.queueName)throw new Error("Queue name is required for message controller initialization");this.routeHandlers={...this.defaultHandlers,...t},await Q(e.queueName,e.prefetch||20,this.handleMessage.bind(this))},async handleMessage(e,t){if(!e)return;let o;try{o=JSON.parse(e.content.toString())}catch(i){return n.error("Invalid JSON message received:",i),t.ack(e)}let r=o.method;r.includes(".")&&(r=r.split(".").slice(1).join("."));let s={correlationId:e.properties.correlationId},a=this.routeHandlers[r];if(!a)return t.sendToQueue(e.properties.replyTo,Buffer.from(JSON.stringify({error:"Method not found"})),s),t.ack(e);try{let i=await a(o,e,t);e.properties.replyTo&&t.sendToQueue(e.properties.replyTo,Buffer.from(JSON.stringify(i??{ok:!0})),s)}catch(i){e.properties.replyTo&&t.sendToQueue(e.properties.replyTo,Buffer.from(JSON.stringify({error:i.message})),s)}finally{t.ack(e)}}};import{MongoClient as me}from"mongodb";import Y from"mysql2/promise";var d=null,j="",ue=5e3,B=async e=>{if(d)return d;j=e.type;try{switch(e.type){case"mongodb":return await pe({host:e.host,user:e.user,password:e.password,database:e.database,port:e.port,connectionArgs:e.connectionArgs});case"mysql":return await fe({host:e.host,user:e.user,password:e.password,database:e.database,connectionLimit:e.connectionLimit,queueLimit:e.queueLimit});default:throw new Error("Unsupported database type: "+e.type)}}catch(t){throw n.error("Error initializing database connection: "+t.message),t}},pe=async({host:e,user:t,password:o,database:r,port:s=27017,connectionArgs:a={}})=>{try{n.info("Connecting to mongo..");let i=`mongodb://${t}:${encodeURIComponent(o)}@${e}:${s}/${r}`;return n.info(`MongoDB connection URL: ${i}`),d=(await me.connect(i,a)).db(),n.info("MongoDB connected successfully"),d}catch(i){throw n.error("MongoDB connection error: "+i),i}},fe=async({host:e,user:t,password:o,database:r,connectionLimit:s=10,queueLimit:a=20,maxRetries:i=5})=>{if(d)return d;let l=0;for(;l<i;)try{return await he({host:e,user:t,password:o,database:r}),n.info("Establishing new MySQL pool..."),d=await Y.createPool({host:e,user:t,password:o,database:r,waitForConnections:!0,connectionLimit:s,queueLimit:a}),n.info("MySQL pool established successfully"),d}catch(u){if(n.error(`MySQL connection attempt ${l+1} failed: ${u.message}`),l++,l>=i)throw n.error("Max retries reached. Could not connect to MySQL."),new Error("Could not connect to the database after multiple attempts");await new Promise(m=>setTimeout(m,ue))}},he=async({host:e,user:t,password:o,database:r})=>{if(n.info(`[assertDbExists] Attempting to connect to MySQL on host ${e} as ${t}`),n.info(`[assertDbExists] Target DB: ${r}`),!r||!e||!t||!o)throw n.error("[assertDbExists] Missing required variables: database, host, user, or password"),new Error("Missing required variables for DB connection");try{let s=await Y.createConnection({host:e,user:t,password:o});n.info(`[assertDbExists] Connected to MySQL, asserting database '${r}'`);let[a]=await s.query(`CREATE DATABASE IF NOT EXISTS \`${r}\``);await s.end()}catch(s){throw n.error("[assertDbExists] \u274C Error asserting database existence"),n.error(s.stack||s.message||s),s}},X=async()=>{if(!d)throw new Error("Database connection not initialized");try{return!0}catch(e){throw e}},Z=async()=>{if(!d)throw new Error("Database connection not initialized");try{j==="mongodb"?await d.dropDatabase():j==="mysql"&&await _()}catch(e){console.error("Error dropping database data: "+e.message)}};var _=async()=>{if(!d)throw new Error("Database connection not initialized");if(F.environment!=="test"&&F.environment!=="development")throw new Error("Cannot reset mysql database outside of test or development mode!");try{let[e]=await d.query("SHOW TABLES");await d.query("SET FOREIGN_KEY_CHECKS = 0");for(let t of e){let o=Object.values(t)[0];await d.query(`DROP TABLE \`${o}\``)}await d.query("SET FOREIGN_KEY_CHECKS = 1"),console.log("MySQL database reset successfully")}catch(e){throw console.error("Error resetting MySQL database: "+e.message),e}};import ge from"fs";import we from"path";var S={config:null,runSeeders:async({config:e,seeder:t})=>{try{if(e.environment=="test"&&!t)return;n.info("Running seeders"),S.config=e;let o,r;try{if(t?o=process.env.GLOBAL_SERVICE_BASE_DIR+"/db/seeders/"+t:o=process.env.GLOBAL_SERVICE_BASE_DIR+"/db/seeders/"+e.environment,r=(await ge.promises.readdir(o)).sort(),r.length==0){n.info("No seeders found for environment: "+e.environment);return}}catch{n.info("No seeders found for environment: "+e.environment);return}for(let s of r){let i=await import(we.join(o,s));if(await S.checkSeederAlreadyRun(s))n.info("Seeder already run: "+s);else try{n.info("Running seeder: "+s),await i.up(),await S.markSeederAsRun(s)}catch(l){n.error("Error running seeder "+s+": "+l)}}n.info("Seeders completed successfully")}catch(o){throw n.error("Error running seeders: "+o),o}},checkSeederAlreadyRun:async e=>{let t=[];if(S.config.db.type=="mysql"){let[o]=await d.query('SHOW TABLES LIKE "seeders"');if(o.length==0)return!1;let[r]=await d.query("SELECT * FROM seeders WHERE name = ?",[e]);t=r}else t=await d.collection("seeders").find({name:e}).toArray();return t.length!=0},markSeederAsRun:async e=>{S.config.db.type=="mysql"?await d.execute("INSERT INTO seeders (name) VALUES (?)",[e]):await d.collection("seeders").insertOne({name:e,runAt:new Date})}};var ee=async()=>{try{await B()}catch(e){n.error("[Internal health check] Failed - Exiting. Error connecting to MongoDB: "+e),process.exit(1)}};process.env.NODE_ENV!=="development"&&(process.on("uncaughtException",e=>{n.error("Uncaught Exception: "+e),process.exit(1)}),process.on("unhandledRejection",(e,t)=>{n.error("Unhandled Rejection at: "+t+". Reason: "+e),process.exit(1)}));var te=e=>{e.setErrorHandler((t,o,r)=>{if(t.validation){if(r&&typeof r.send=="function")return r.code(400).send({statusCode:400,error:"Bad Request",message:t.message});throw new Error("Bad Request: "+t.message)}if(t instanceof k){if(r&&typeof r.send=="function")return r.code(404).send({statusCode:404,error:"Not Found",message:t.message});throw new Error("Not found: "+t.message)}if(t instanceof L){if(r&&typeof r.code=="function"&&typeof r.send=="function")return r.code(400).send({statusCode:400,error:"Bad Request",message:t.message});throw new Error("Bad Request: "+t.message)}if(t instanceof q){if(r)return r.code(401).send({statusCode:401,error:"Unauthorized",message:t.message});throw new Error("Unauthorized: "+t.message)}if(t.statusCode===429){if(r)return r.code(429).send({statusCode:429,error:"Too Many Requests",message:"You have exceeded the request limit."});throw new Error("Too Many Requests: You have exceeded the request limit.")}if(t instanceof D){if(r)throw new Error("Failed Health Check: "+t.message);return r.code(500).send({statusCode:500,error:"Failed Health Check",message:t.message})}if(n.error(t),r)return r.code(500).send({statusCode:500,error:"Internal Server Error",message:"Something went wrong",details:process.env.NODE_ENV==="development"?t.stack:""});throw new Error("Internal Server Error: "+t.message)})},k=class extends Error{constructor(t="Resource not found"){super(t),this.name="NotFoundError",this.statusCode=404}},L=class extends Error{constructor(t="Bad request"){super(t),this.name="BadRequestError",this.statusCode=400}},q=class extends Error{constructor(t="Unauthorised"){super(t),this.name="UnauthorisedError",this.statusCode=401}},D=class extends Error{constructor(t="Failed health check"){super(t),this.name="FailedHealthCheckError",this.statusCode=500}};var y={db:{},init(e){y.db=e},async syncManyToMany({table:e,parentId:t,childIds:o,parentColumn:r,childColumn:s}){try{o=Array.from(new Set(o||[])).map(String);let[a]=await y.db.query(`SELECT ${s} FROM ${e} WHERE ${r} = ?`,[t]),i=a.map(m=>{let p=m[s];return Buffer.isBuffer(p)?p.toString("hex"):String(p)}),l=o.filter(m=>!i.includes(m)),u=i.filter(m=>!o.includes(m));if(u.length&&await y.db.query(`DELETE FROM ${e} WHERE ${r} = ? AND ${s} IN (?)`,[t,u]),l.length){let m=l.map(p=>[t,p]);await y.db.query(`INSERT IGNORE INTO ${e} (${r}, ${s}) VALUES ?`,[m])}return{added:l,removed:u}}catch(a){throw n.error("Error syncing many-to-many relationship: "+a.message),a}},async syncOneToMany({table:e,parentId:t,childIds:o,parentColumn:r,childIdColumn:s="id"}){try{Array.isArray(o)||(o=[]),o=Array.from(new Set(o)).map(String);let[a]=await y.db.query(`SELECT ${s} FROM ${e} WHERE ${r} = ?`,[t]),i=a.map(m=>{let p=m[s];return Buffer.isBuffer(p)?p.toString("hex"):String(p)});console.log("currentIds:",i),console.log("childIds:",o);let l=o.filter(m=>!i.includes(m)),u=i.filter(m=>!o.includes(m));return u.length&&await y.db.query(`UPDATE ${e} SET ${r} = NULL WHERE ${s} IN (?)`,[u]),l.length&&await y.db.query(`UPDATE ${e} SET ${r} = ? WHERE ${s} IN (?)`,[t,l]),{linked:l,unlinked:u}}catch(a){throw n.error("Error syncing one-to-many relationship: "+a.message),a}},toSnake(e){return e.replace(/[A-Z]/g,t=>`_${t.toLowerCase()}`)},objectToCamelCase(e){let t={};for(let o in e){let r=o.replace(/_([a-z])/g,s=>s[1].toUpperCase());t[r]=e[o]}return t}};import ye from"fs";import Ee from"path";var C={config:null,runMigrations:async({config:e})=>{try{if(e.db.type!=="mysql"){n.info("Migrations only supported for MySQL");return}(e.resetDatabase&&e.environment=="development"||e.environment=="test")&&(n.info("Resetting database..."),await _()),n.info("Running migrations"),C.config=e;let t=process.env.GLOBAL_SERVICE_BASE_DIR+"/db/migrations",o=(await ye.promises.readdir(t)).sort();for(let r of o){let a=await import(Ee.join(t,r));await C.checkMigrationAlreadyRun(r)?n.info("Migration already run: "+r):(n.info("Running migration: "+r),await a.up(),await C.markMigrationAsRun(r))}n.info("Migrations completed successfully")}catch(t){throw n.error("Error running migrations: "+t),t}},checkMigrationAlreadyRun:async e=>{let[t]=await d.query('SHOW TABLES LIKE "migrations"');if(t.length==0)return!1;if(e=="01-init.js")return!0;let[o]=await d.query("SELECT * FROM migrations WHERE name = ?",[e]);return o.length!=0},markMigrationAsRun:async e=>{await d.execute("INSERT INTO migrations (name) VALUES (?)",[e])}};import be from"ajv";import Se from"ajv-formats";var x=new be({allErrors:!0,useDefaults:!0});Se(x);x.addFormat("mysql-date",/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/);var ve={compile:e=>{w.addSchema(e);let t=x.compile(e.schema);return o=>{if(!t(o)){let s=[];return t.errors.map(a=>{s.push(a.instancePath+" "+a.message)}),{errors:s.join(", ")}}return!1}},addFormat:(e,t)=>{x.addFormat(e,t)},addKeyword:(e,t)=>{x.addKeyword(e,t)},addSchema:e=>{w.addSchema(e),x.addSchema(e.schema,e.$id)}},re=ve;import Re from"assert";import Me from"fs";import Ce from"path";var f={testsDirectory:`${process.env.GLOBAL_SERVICE_BASE_DIR}/tests/commands`,failed:0,beforeEachFns:[],afterEachFns:[],tests:[],testResults:[],beforeEach:e=>{f.beforeEachFns.push(e)},run:(e,t)=>{f.tests.push({name:e,fn:t})},afterEach:e=>{f.afterEachFns.push(e)},assert:Re,runCommandTests:async()=>{console.log("=============================="),console.log("Running command tests...");let e=Me.readdirSync(f.testsDirectory).filter(t=>t.endsWith(".js")).sort();for(let t of e){f.prepFns=[],f.tests=[];let o=Ce.join(f.testsDirectory,t);try{await import(o)}catch(s){f.failed++,console.error(`\u274C Failed to load ${t}: ${s.message}`);continue}let r=0;for(let s of f.tests){r++;for(let a of f.beforeEachFns)try{await a()}catch(i){f.failed++,console.error(`\u274C Test preparation failed - ${i.message}`);break}try{await s.fn(),f.testResults.push({name:s.name,error:null})}catch(a){f.failed++,f.testResults.push({name:s.name,error:a})}for(let a of f.afterEachFns)try{await a()}catch(i){f.failed++,console.error(`\u274C Test tidy up failed - ${i.message}`);break}}}console.table(f.testResults.map(t=>({Test:t.name,Result:t.error?`\u274C Failed - ${t.error.message}`:"\u2705 Passed"}))),f.failed>0?console.error(`\u274C Some Integration tests failed: ${f.failed} error(s)`):console.log("\u2705 All integration tests passed!")}};var h={client:null,bucket:"",uploadsUrl:"",init:async e=>{if(!e.bucket)throw new Error("S3 storage driver requires a bucket name.");if(!e.region)throw new Error("S3 storage driver requires a region.");if(!e.uploadsUrl)throw new Error("S3 storage driver requires an uploadsUrl.");let{S3Client:t,PutObjectCommand:o,GetObjectCommand:r,DeleteObjectCommand:s}=await import("@aws-sdk/client-s3");h.S3Client=t,h.PutObjectCommand=o,h.GetObjectCommand=r,h.DeleteObjectCommand=s,h.bucket=e.bucket,h.uploadsUrl=e.uploadsUrl,h.client=new t({region:e.region,credentials:{accessKeyId:e.accessKeyId,secretAccessKey:e.secretAccessKey}})},upload:async({file:e,key:t,contentType:o})=>{let r=new h.PutObjectCommand({Bucket:h.bucket,Key:t,Body:e,ContentType:o});return await h.client.send(r),t},download:async({key:e,stream:t})=>{let o=new h.GetObjectCommand({Bucket:h.bucket,Key:e}),r=await h.client.send(o);if(t)return r.Body;{let s=[];for await(let a of r.Body)s.push(a);return Buffer.concat(s)}},delete:async({key:e})=>{let t=new h.DeleteObjectCommand({Bucket:h.bucket,Key:e});await h.client.send(t)},getUrl:async({key:e})=>`${h.uploadsUrl}/${e}`};var g={driverName:"",driver:null,uploadsUrl:"",init:async e=>{try{if(!e.driver){n.info("No storage driver specified, skipping storage initialization.");return}switch(e.driver){case"s3":g.driverName="s3",g.driver=h;break;default:throw new Error(`Unsupported storage type: ${e.driver}`)}await g.driver.init(e),g.uploadsUrl=e.uploadsUrl||""}catch(t){throw n.error(`Storage initialization error: ${t.message}`),t}},upload:async({file:e,key:t,contentType:o,metadata:r})=>{try{let s=await g.driver.upload({file:e,key:t,contentType:o,metadata:r});return`${g.uploadsUrl}/${s}`}catch(s){throw n.error(`Storage upload error: ${s.message}`),s}},download:async({key:e,stream:t})=>{try{return await g.driver.download({key:e,stream:t})}catch(o){throw n.error(`Storage download error: ${o.message}`),o}},delete:async({key:e})=>{try{return await g.driver.delete({key:e})}catch(t){throw n.error(`Storage delete error: ${t.message}`),t}},getUrl:async({key:e,expiresIn:t})=>{try{return await g.driver.getUrl({key:e,expiresIn:t})}catch(o){throw n.error(`Storage getUrl error: ${o.message}`),o}}};import{v4 as xe}from"uuid";import{v5 as $e}from"uuid";var Te=await import(process.env.GLOBAL_SERVICE_BASE_DIR+"config.js"),F=Te.config,c={init:async e=>{if(c.config=e,c.http=W,e.http&&await c.http.init(e.http),c.commands=b,c.commands.init(e),e.db&&e.db.type)try{c.db=await B(e.db)}catch(t){n.error("Error connecting to database: "+t),process.exit(1)}if(c.db||(c.db={}),c.db.checkConnection=X,c.db.migrations=C,c.db.seeders=S,y.init(c.db),c.db.sql={},c.db.sql.helpers=y,c.http.addHook("onReady",async()=>{setInterval(()=>{b.execute("internalHealthCheck",{})},6e4)}),te(c.http),P(e.message?.url||""),c.message=V,c.message.sendAwaitResponse=G,c.message.sendAndForget=O,c.webSockets=N,c.commands.register(`${e.serviceName}.runMigrations`,async()=>await C.runMigrations({config:e})),c.commands.register(`${e.serviceName}.runSeeders`,async({seeder:t})=>await S.runSeeders({config:e,seeder:t})),c.commands.register(`${e.serviceName}.internalHealthCheck`,ee),c.commands.register(`${e.serviceName}.dropDatabaseData`,Z),c.schema=re,c.logger=n,c.logger.init({cloudProjectId:e.cloud?.projectId||"",serviceName:e.serviceName,flushIntervalMs:e.cloud?.logger.flushIntervalMs||5e3,transports:e.cloud?.logger||[]}),c.error={notFound:k,badRequest:L,unauthorised:q,failedHealthCheck:D},c.utils={uuid:()=>xe(),hash:(t,o)=>$e(t,o)},c.http.addHook("onRequest",async(t,o)=>{let{url:r,method:s}=t;if(!r.endsWith("/")&&!r.includes(".")&&r!=="/"){let l=new URL(t.raw.url,`http://${t.headers.host}`);l.pathname+="/",t.raw.url=l.pathname+(l.search||"")}let a=t.raw.headers.authorization||"",i=a?a.split(" ")[1]:"";if(i){let l=await c.commands.execute("userService.getAuthenticatedUser",{token:i});l&&(t.user=l)}}),c.registerService=async()=>{if(e.serviceName!=="controlService")try{await c.commands.execute("controlService.registerService",{service:{name:e.serviceName,manifest:w.manifest}}),c.logger.info(`Service ${e.serviceName} registered with control service.`)}catch(t){c.logger.error(`Failed to register service ${e.serviceName} with control service: ${t.message}`)}},c.test=f,e.storage&&e.storage.driver)g.init(e.storage),c.storage=g;else{let t=()=>{throw new Error("Storage service not configured - please configure storage in config.js")};c.storage={upload:t,download:t,getUrl:t}}c.rabbit=J}};await c.init(F);var mr=c,{commands:ur,http:pr,message:fr,db:hr,schema:gr,logger:wr,error:yr,utils:Er,registerService:br,webSockets:Sr,test:vr,storage:Rr,rabbit:Mr}=c;export{ur as commands,F as config,hr as db,mr as default,yr as error,pr as http,wr as logger,fr as message,Mr as rabbit,br as registerService,gr as schema,Rr as storage,vr as test,Er as utils,Sr as webSockets};
1
+ import oe from"fastify";import se from"@fastify/cors";import ne from"@fastify/rate-limit";import"pino";var n={cloudProjectId:"",serviceName:"",logs:[],transports:[],flushIntervalMs:5e3,timer:null,init:({cloudProjectId:e,serviceName:t,transports:o,flushIntervalMs:r})=>{if(n.cloudProjectId=e||"",!t)throw new Error("Service name is required for logger initialization");n.serviceName=t,n.transports=o||[],n.flushIntervalMs=r||5e3,n.startFlushTimer()},info:(e,t,o,r)=>{n.addLog({args1:e,args2:t,args3:o,args4:r,level:"info"})},warning:(e,t,o,r)=>{n.addLog({args1:e,args2:t,args3:o,args4:r,level:"warning"})},error:(e,t,o,r)=>{n.addLog({args1:e,args2:t,args3:o,args4:r,level:"error"})},table:(e,t,o,r)=>{n.addLog({args1:e,args2:t,args3:o,args4:r,level:"table"})},testResult:(e,t,o)=>{n.addLog({args1:o,level:"test_result",testResult:t,test:e})},addTransport:e=>{},addLog:({args1:e,args2:t,args3:o,args4:r,level:s,testResult:a,test:i})=>{let u=[e,t,o,r].filter(p=>p!==void 0).map(p=>{if(typeof p=="object")try{return JSON.stringify(p)}catch{return"[Object]"}else return String(p)}).join(" "),m={cloudProjectId:n.cloudProjectId||null,service:n.serviceName,timestamp:Date.now(),level:s||"info",message:u,testResult:a||null,test:i||null};if(n.transports.length==0)if(m.level==="table"){try{let p=JSON.parse(m.message);console.table(p)}catch{console.log("[table] unable to parse table data:",m.message)}return}else console.log(n.formatForConsole(m));else n.logs.push(m)},startFlushTimer(){n.timer&&clearInterval(n.timer),n.timer=setInterval(()=>n.flush(),n.flushIntervalMs)},flush:()=>{n.transports.forEach(e=>{}),n.logs=[]},formatForConsole:e=>{let o={error:"\x1B[31m",warning:"\x1B[33m",reset:"\x1B[0m"},r=new Date(e.timestamp).toISOString(),a=(e.level||"info").toUpperCase().padEnd(12),i=`${r} ${a} \u2014 ${e.message}`,l=o[e.level]||"",u=l?o.reset:"";return`${l}${i}${u}`}};var v=null,H=3e3,W={init:e=>{v=oe({}),H=e.port||3e3,v.register(se,{origin:e.allowedOrigins||[],methods:e.allowedMethods||["GET","POST","PUT","DELETE"],allowedHeaders:e.allowedHeaders||["Content-Type","Authorization"],preflight:!0,optionsSuccessStatus:204,credentials:!0}),e?.rateLimiting?.max&&e?.rateLimiting?.timeWindow&&v.register(ne,e.rateLimiting)},registerRoutes:({controllers:e})=>{e.forEach(t=>{Object.values(t).forEach(o=>{v.route(o)})}),n.info("Control service registered http routes "+v.printRoutes())},addHook:(e,t)=>{v.addHook(e,t)},start:async()=>{try{await v.listen({port:H,host:"0.0.0.0"}),n.info(`HTTP server started on port ${H}`)}catch(e){n.error("Error starting HTTP server: "+e),process.exit(1)}},setErrorHandler:e=>{v.setErrorHandler(e)}};import ae from"amqplib";var I="",R=null,$=null,U={arguments:{"x-queue-type":"quorum"}},P=e=>{I=e},T=async()=>{if(R)return R;try{console.log("Connecting to RabbitMQ:",I),R=await ae.connect(I)}catch(e){return console.log("Error connecting to RabbitMQ:",e),R=null,await new Promise(t=>setTimeout(t,3e3)),T({rabbitConnectionUrl:I})}return R},E=async()=>{if($)return $;R||await T();try{$=await R.createChannel()}catch(e){return console.error("Error creating RabbitMQ channel:",e),$=null,await new Promise(t=>setTimeout(t,3e3)),E()}return $},K=async(e,t,o)=>{let r=await E();return await r.assertQueue(e,U),await r.prefetch(t),(await r.consume(e,async a=>{a&&(async()=>{try{await o(a,r)}catch(i){console.error("Error handling message:",i),r.nack(a,!1,!1)}})()})).consumerTag},ie=async(e,t,o,r)=>{await T(),await E();let s=await E();return await s.assertQueue(e,U),await s.prefetch(t),r&&await s.cancel(r),K(e,t,o)},Q=async(e,t,o)=>{await T();let r=await E();await r.assertQueue(e,U),await r.prefetch(t);let s=await K(e,t,o);return R.on("close",async()=>{console.log("RabbitMQ connection closed, attempting to reconnect..."),await z(e,t,o,s)}),console.log("RabbitMQ connection established and consumer registered:",s),s},z=async(e,t,o,r)=>{console.log("Reconnecting to RabbitMQ...");let s=ie(e,t,o,r);return console.log("RabbitMQ connection re-established and consumer registered:",s),s},J={getChannel:E,getConnection:T,setConnectionUrl:P,initialize:Q,reinitialize:z};var G=async(e,t)=>{let o=`${e}Queue`,r;try{r=await E(),await r.assertQueue(o,{arguments:{"x-queue-type":"quorum"}});let s=ce(),a=s,i=await r.assertQueue("",{exclusive:!0});return new Promise((l,u)=>{let m=setTimeout(()=>{p(),u(new Error(`No response from ${e} service`))},5e3),p=()=>{r.cancel(a).catch(()=>{}),r.deleteQueue(i.queue).catch(()=>{}),clearTimeout(m)};r.consume(i.queue,A=>{A.properties.correlationId===s&&(p(),l(JSON.parse(A.content.toString())),r.ack(A))},{noAck:!1,consumerTag:a}).catch(u),r.sendToQueue(o,Buffer.from(JSON.stringify(t)),{replyTo:i.queue,correlationId:s})})}catch(s){if(r)try{await r.close()}catch(a){console.error("Failed to close channel:",a)}throw new Error(`Failed to send message to ${e}: ${s.message}`)}},O=async(e,t)=>{let o=`${e}Queue`,r;try{r=await E(),await r.assertQueue(o,{arguments:{"x-queue-type":"quorum"}}),r.sendToQueue(o,Buffer.from(JSON.stringify(t)))}catch(s){if(r)try{await r.close()}catch(a){console.error("Failed to close channel:",a)}throw new Error(`Failed to send message to ${e}: ${s.message}`)}},ce=()=>Math.random().toString(36).substring(7);import M,{WebSocketServer as le}from"ws";import{v4 as de}from"uuid";var N={wsMap:new Map,pendingCalls:[],async init(e,t){e.serviceName=t,this.startServer();let o=1;for(;;)try{e.serviceName!=="controlService"&&(n.info("serviceName "+JSON.stringify(e)),await this.connect("controlService",e));break}catch{o>=e.maxInitialConnectionAttempts?(n.error(`Initial WS connection to control service failed after ${o} attempts. Exiting.`),process.exit(1)):(n.error(`Initial WS connection to control service failed (attempt ${o}). Retrying in 3s...`),o++,await new Promise(s=>setTimeout(s,3e3)))}setInterval(async()=>{let r=[];try{r=await b.execute("controlService.getServices",{})}catch(a){n.error("Failed to get service registry. "+a);return}let s=r.map(a=>a.name===e.serviceName||this.wsMap[a.name]&&this.wsMap[a.name].readyState===M.OPEN?Promise.resolve():this.connect(a.name,e));try{await Promise.all(s)}catch{}},e.reconnectInterval||1e4)},startServer(e=5e3){new le({port:e}).on("connection",(o,r)=>{let s=this.identifyPeer(r);this.wsMap.set(s,o),o.on("message",a=>{this.handleMessage(s,a)}),o.on("close",()=>{n.info(`Peer disconnected: ${s}`),this.wsMap.delete(s)})})},async connect(e,t){let r=`ws://${e.replace("Service","-service").toLowerCase()}:5000`;return this.wsMap.has(e)&&this.wsMap.get(e).readyState===M.OPEN?this.wsMap.get(e):new Promise((s,a)=>{let i=new M(r,{headers:{"x-api-key":"my-secret-key","x-service-name":t.serviceName}});i.on("open",()=>{n.info(`Connected to peer ${e} at ${r}`),this.wsMap.set(e,i),i.on("message",l=>{this.handleMessage(e,l)}),i.on("close",()=>{n.info(`Peer disconnected: ${e}`),this.wsMap.delete(e)}),s(i)}),i.on("error",l=>{a(l)})})},async handleMessage(e,t){let o;try{o=JSON.parse(t)}catch{n.error("Invalid JSON from peer:",t);return}if(o.type==="request"&&o.commandName)try{let r=await b.execute(o.commandName,o.payload);this.sendResponse(e,o.messageId,r)}catch(r){this.sendResponse(e,o.messageId,null,r.message)}else if(o.type==="response"){let r=this.pendingCalls.findIndex(a=>a.messageId===o.messageId);if(r===-1){n.error(`No pending call for messageId ${o.messageId}`);return}let s=this.pendingCalls.splice(r,1)[0];clearTimeout(s.timeout),o.error?s.reject(new Error(o.error)):s.resolve(o.response)}else n.error("Unknown message type",o)},async send(e,t,o,r=1e4){let s=this.wsMap.get(e);if((!s||s.readyState!==M.OPEN)&&(s=await this.waitForWsReady(e)),!s||s.readyState!==M.OPEN)throw new Error(`WebSocket not connected to ${e}`);let a=de(),i={type:"request",messageId:a,commandName:t,payload:o};return s.send(JSON.stringify(i)),new Promise((l,u)=>{let m=setTimeout(()=>{this.pendingCalls=this.pendingCalls.filter(p=>p.messageId!==a),u(new Error(`Timeout waiting for response from ${e}`)),n.error(`Timeout waiting for response on call: ${JSON.stringify(i)}`)},r);this.pendingCalls.push({messageId:a,resolve:l,reject:u,timeout:m})})},sendResponse(e,t,o=null,r=null){let s=this.wsMap.get(e);!s||s.readyState!==M.OPEN||s.send(JSON.stringify({type:"response",messageId:t,response:o,error:r}))},identifyPeer(e){return e.headers["x-service-name"]||`unknown-${Date.now()}`},async waitForWsReady(e,t=50,o=5e3){let r=Date.now();for(;;){let s=this.wsMap.get(e);if(s&&s.readyState===M.OPEN)return s;if(Date.now()-r>o)throw new Error(`WebSocket not connected to ${e} after ${o}ms`);await new Promise(a=>setTimeout(a,t))}}};var w={manifest:{commandList:[],commandImplementations:{},schemas:{}},addCommand({commandName:e,handlerFunction:t}){w.manifest.commandImplementations[e]={function:t.toString()},w.manifest.commandList.push(e)},addSchema({schemaName:e,schema:t}){w.manifest.schemas[e]=t}};var b={config:{},handlers:new Map,init(e){this.config=e},register(e,t){this.handlers.set(e,t),w.addCommand({commandName:e,handlerFunction:t})},async execute(e,t,o={}){let{fireAndForget:r=!1}=o;if(this.config.architecture=="modular-monolith"){let s=this.handlers.get(e);if(!s)throw new Error(`Command "${e}" not registered`);return await s(t)}else{let[s,a]=e.split(".");if(!s||!a){let l=this.config.serviceName+"."+e,u=this.handlers.get(l);if(!u)throw new Error(`Command "${l}" not registered`);return await u(t)}if(s===this.config.serviceName){let l=this.handlers.get(e);if(!l)throw new Error(`Command "${e}" not registered`);return await l(t)}if(r){O(s,{method:e,data:t});return}return await N.send(s,e,t)}}};var V={routeHandlers:{},defaultHandlers:{runMigrations:async e=>{let t=e.data?.migration;await b.execute("runMigrations",{migration:t})},runSeeders:async e=>{let t=e.data?.seeder;await b.execute("runSeeders",{seeder:t})},healthCheck:async()=>({status:"ok"})},async init({config:e,handlers:t={}}){if(!e?.queueName)throw new Error("Queue name is required for message controller initialization");this.routeHandlers={...this.defaultHandlers,...t},await Q(e.queueName,e.prefetch||20,this.handleMessage.bind(this))},async handleMessage(e,t){if(!e)return;let o;try{o=JSON.parse(e.content.toString())}catch(i){return n.error("Invalid JSON message received:",i),t.ack(e)}let r=o.method;r.includes(".")&&(r=r.split(".").slice(1).join("."));let s={correlationId:e.properties.correlationId},a=this.routeHandlers[r];if(!a)return t.sendToQueue(e.properties.replyTo,Buffer.from(JSON.stringify({error:"Method not found"})),s),t.ack(e);try{let i=await a(o,e,t);e.properties.replyTo&&t.sendToQueue(e.properties.replyTo,Buffer.from(JSON.stringify(i??{ok:!0})),s)}catch(i){e.properties.replyTo&&t.sendToQueue(e.properties.replyTo,Buffer.from(JSON.stringify({error:i.message})),s)}finally{t.ack(e)}}};import{MongoClient as me}from"mongodb";import Y from"mysql2/promise";var d=null,j="",ue=5e3,B=async e=>{if(d)return d;j=e.type;try{switch(e.type){case"mongodb":return await pe({host:e.host,user:e.user,password:e.password,database:e.database,port:e.port,connectionArgs:e.connectionArgs});case"mysql":return await fe({host:e.host,user:e.user,password:e.password,database:e.database,connectionLimit:e.connectionLimit,queueLimit:e.queueLimit});default:throw new Error("Unsupported database type: "+e.type)}}catch(t){throw n.error("Error initializing database connection: "+t.message),t}},pe=async({host:e,user:t,password:o,database:r,port:s=27017,connectionArgs:a={}})=>{try{n.info("Connecting to mongo..");let i=`mongodb://${t}:${encodeURIComponent(o)}@${e}:${s}/${r}`;return n.info(`MongoDB connection URL: ${i}`),d=(await me.connect(i,a)).db(),n.info("MongoDB connected successfully"),d}catch(i){throw n.error("MongoDB connection error: "+i),i}},fe=async({host:e,user:t,password:o,database:r,connectionLimit:s=10,queueLimit:a=20,maxRetries:i=5})=>{if(d)return d;let l=0;for(;l<i;)try{return await he({host:e,user:t,password:o,database:r}),n.info("Establishing new MySQL pool..."),d=await Y.createPool({host:e,user:t,password:o,database:r,waitForConnections:!0,connectionLimit:s,queueLimit:a}),n.info("MySQL pool established successfully"),d}catch(u){if(n.error(`MySQL connection attempt ${l+1} failed: ${u.message}`),l++,l>=i)throw n.error("Max retries reached. Could not connect to MySQL."),new Error("Could not connect to the database after multiple attempts");await new Promise(m=>setTimeout(m,ue))}},he=async({host:e,user:t,password:o,database:r})=>{if(n.info(`[assertDbExists] Attempting to connect to MySQL on host ${e} as ${t}`),n.info(`[assertDbExists] Target DB: ${r}`),!r||!e||!t||!o)throw n.error("[assertDbExists] Missing required variables: database, host, user, or password"),new Error("Missing required variables for DB connection");try{let s=await Y.createConnection({host:e,user:t,password:o});n.info(`[assertDbExists] Connected to MySQL, asserting database '${r}'`);let[a]=await s.query(`CREATE DATABASE IF NOT EXISTS \`${r}\``);await s.end()}catch(s){throw n.error("[assertDbExists] \u274C Error asserting database existence"),n.error(s.stack||s.message||s),s}},X=async()=>{if(!d)throw new Error("Database connection not initialized");try{return!0}catch(e){throw e}},Z=async()=>{if(!d)throw new Error("Database connection not initialized");try{j==="mongodb"?await d.dropDatabase():j==="mysql"&&await _()}catch(e){console.error("Error dropping database data: "+e.message)}};var _=async()=>{if(!d)throw new Error("Database connection not initialized");if(F.environment!=="test"&&F.environment!=="development")throw new Error("Cannot reset mysql database outside of test or development mode!");try{let[e]=await d.query("SHOW TABLES");await d.query("SET FOREIGN_KEY_CHECKS = 0");for(let t of e){let o=Object.values(t)[0];await d.query(`DROP TABLE \`${o}\``)}await d.query("SET FOREIGN_KEY_CHECKS = 1"),console.log("MySQL database reset successfully")}catch(e){throw console.error("Error resetting MySQL database: "+e.message),e}};import ge from"fs";import we from"path";var S={config:null,runSeeders:async({config:e,seeder:t})=>{try{if(e.environment=="test"&&!t)return;n.info("Running seeders"),S.config=e;let o,r;try{if(t?o=process.env.GLOBAL_SERVICE_BASE_DIR+"/db/seeders/"+t:o=process.env.GLOBAL_SERVICE_BASE_DIR+"/db/seeders/"+e.environment,r=(await ge.promises.readdir(o)).sort(),r.length==0){n.info("No seeders found for environment: "+e.environment);return}}catch{n.info("No seeders found for environment: "+e.environment);return}for(let s of r){let i=await import(we.join(o,s));if(await S.checkSeederAlreadyRun(s))n.info("Seeder already run: "+s);else try{n.info("Running seeder: "+s),await i.up(),await S.markSeederAsRun(s)}catch(l){n.error("Error running seeder "+s+": "+l)}}n.info("Seeders completed successfully")}catch(o){throw n.error("Error running seeders: "+o),o}},checkSeederAlreadyRun:async e=>{let t=[];if(S.config.db.type=="mysql"){let[o]=await d.query('SHOW TABLES LIKE "seeders"');if(o.length==0)return!1;let[r]=await d.query("SELECT * FROM seeders WHERE name = ?",[e]);t=r}else t=await d.collection("seeders").find({name:e}).toArray();return t.length!=0},markSeederAsRun:async e=>{S.config.db.type=="mysql"?await d.execute("INSERT INTO seeders (name) VALUES (?)",[e]):await d.collection("seeders").insertOne({name:e,runAt:new Date})}};var ee=async()=>{try{await B()}catch(e){n.error("[Internal health check] Failed - Exiting. Error connecting to MongoDB: "+e),process.exit(1)}};process.env.NODE_ENV!=="development"&&(process.on("uncaughtException",e=>{n.error("Uncaught Exception: "+e),process.exit(1)}),process.on("unhandledRejection",(e,t)=>{n.error("Unhandled Rejection at: "+t+". Reason: "+e),process.exit(1)}));var te=e=>{e.setErrorHandler((t,o,r)=>{if(t.validation){if(r&&typeof r.send=="function")return r.code(400).send({statusCode:400,error:"Bad Request",message:t.message});throw new Error("Bad Request: "+t.message)}if(t instanceof k){if(r&&typeof r.send=="function")return r.code(404).send({statusCode:404,error:"Not Found",message:t.message});throw new Error("Not found: "+t.message)}if(t instanceof L){if(r&&typeof r.code=="function"&&typeof r.send=="function")return r.code(400).send({statusCode:400,error:"Bad Request",message:t.message});throw new Error("Bad Request: "+t.message)}if(t instanceof q){if(r)return r.code(401).send({statusCode:401,error:"Unauthorized",message:t.message});throw new Error("Unauthorized: "+t.message)}if(t.statusCode===429){if(r)return r.code(429).send({statusCode:429,error:"Too Many Requests",message:"You have exceeded the request limit."});throw new Error("Too Many Requests: You have exceeded the request limit.")}if(t instanceof D){if(r)throw new Error("Failed Health Check: "+t.message);return r.code(500).send({statusCode:500,error:"Failed Health Check",message:t.message})}if(n.error(t),r)return r.code(500).send({statusCode:500,error:"Internal Server Error",message:"Something went wrong",details:process.env.NODE_ENV==="development"?t.stack:""});throw new Error("Internal Server Error: "+t.message)})},k=class extends Error{constructor(t="Resource not found"){super(t),this.name="NotFoundError",this.statusCode=404}},L=class extends Error{constructor(t="Bad request"){super(t),this.name="BadRequestError",this.statusCode=400}},q=class extends Error{constructor(t="Unauthorised"){super(t),this.name="UnauthorisedError",this.statusCode=401}},D=class extends Error{constructor(t="Failed health check"){super(t),this.name="FailedHealthCheckError",this.statusCode=500}};var y={db:{},init(e){y.db=e},async syncManyToMany({table:e,parentId:t,childIds:o,parentColumn:r,childColumn:s}){try{o=Array.from(new Set(o||[])).map(String);let[a]=await y.db.query(`SELECT ${s} FROM ${e} WHERE ${r} = ?`,[t]),i=a.map(m=>{let p=m[s];return Buffer.isBuffer(p)?p.toString("hex"):String(p)}),l=o.filter(m=>!i.includes(m)),u=i.filter(m=>!o.includes(m));if(u.length&&await y.db.query(`DELETE FROM ${e} WHERE ${r} = ? AND ${s} IN (?)`,[t,u]),l.length){let m=l.map(p=>[t,p]);await y.db.query(`INSERT IGNORE INTO ${e} (${r}, ${s}) VALUES ?`,[m])}return{added:l,removed:u}}catch(a){throw n.error("Error syncing many-to-many relationship: "+a.message),a}},async syncOneToMany({table:e,parentId:t,childIds:o,parentColumn:r,childIdColumn:s="id"}){try{Array.isArray(o)||(o=[]),o=Array.from(new Set(o)).map(String);let[a]=await y.db.query(`SELECT ${s} FROM ${e} WHERE ${r} = ?`,[t]),i=a.map(m=>{let p=m[s];return Buffer.isBuffer(p)?p.toString("hex"):String(p)});console.log("currentIds:",i),console.log("childIds:",o);let l=o.filter(m=>!i.includes(m)),u=i.filter(m=>!o.includes(m));return u.length&&await y.db.query(`UPDATE ${e} SET ${r} = NULL WHERE ${s} IN (?)`,[u]),l.length&&await y.db.query(`UPDATE ${e} SET ${r} = ? WHERE ${s} IN (?)`,[t,l]),{linked:l,unlinked:u}}catch(a){throw n.error("Error syncing one-to-many relationship: "+a.message),a}},toSnake(e){return e.replace(/[A-Z]/g,t=>`_${t.toLowerCase()}`)},objectToCamelCase(e){let t={};for(let o in e){let r=o.replace(/_([a-z])/g,s=>s[1].toUpperCase());t[r]=e[o]}return t}};import ye from"fs";import Ee from"path";var C={config:null,runMigrations:async({config:e})=>{try{if(e.db.type!=="mysql"){n.info("Migrations only supported for MySQL");return}(e.resetDatabase&&e.environment=="development"||e.environment=="test")&&(n.info("Resetting database..."),await _()),n.info("Running migrations"),C.config=e;let t=process.env.GLOBAL_SERVICE_BASE_DIR+"/db/migrations",o=(await ye.promises.readdir(t)).sort();for(let r of o){let a=await import(Ee.join(t,r));await C.checkMigrationAlreadyRun(r)?n.info("Migration already run: "+r):(n.info("Running migration: "+r),await a.up(),await C.markMigrationAsRun(r))}n.info("Migrations completed successfully")}catch(t){throw n.error("Error running migrations: "+t),t}},checkMigrationAlreadyRun:async e=>{let[t]=await d.query('SHOW TABLES LIKE "migrations"');if(t.length==0)return!1;if(e=="01-init.js")return!0;let[o]=await d.query("SELECT * FROM migrations WHERE name = ?",[e]);return o.length!=0},markMigrationAsRun:async e=>{await d.execute("INSERT INTO migrations (name) VALUES (?)",[e])}};import be from"ajv";import Se from"ajv-formats";var x=new be({allErrors:!0,useDefaults:!0});Se(x);x.addFormat("mysql-date",/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/);var ve={compile:e=>{w.addSchema(e);let t=x.compile(e.schema);return o=>{if(!t(o)){let s=[];return t.errors.map(a=>{s.push(a.instancePath+" "+a.message)}),{errors:s.join(", ")}}return!1}},addFormat:(e,t)=>{x.addFormat(e,t)},addKeyword:(e,t)=>{x.addKeyword(e,t)},addSchema:e=>{w.addSchema(e),x.addSchema(e.schema,e.$id)}},re=ve;import Re from"assert";import Me from"fs";import Ce from"path";var f={testsDirectory:`${process.env.GLOBAL_SERVICE_BASE_DIR}/tests/commands`,failed:0,beforeEachFns:[],afterEachFns:[],tests:[],testResults:[],beforeEach:e=>{f.beforeEachFns.push(e)},run:(e,t)=>{f.tests.push({name:e,fn:t})},afterEach:e=>{f.afterEachFns.push(e)},assert:Re,runCommandTests:async()=>{console.log("=============================="),console.log("Running command tests...");let e=Me.readdirSync(f.testsDirectory).filter(t=>t.endsWith(".js")).sort();for(let t of e){f.prepFns=[],f.tests=[];let o=Ce.join(f.testsDirectory,t);try{await import(o)}catch(s){f.failed++,console.error(`\u274C Failed to load ${t}: ${s.message}`);continue}let r=0;for(let s of f.tests){r++;for(let a of f.beforeEachFns)try{await a()}catch(i){f.failed++,console.error(`\u274C Test preparation failed - ${i.message}`);break}try{await s.fn(),f.testResults.push({name:s.name,error:null})}catch(a){f.failed++,f.testResults.push({name:s.name,error:a})}for(let a of f.afterEachFns)try{await a()}catch(i){f.failed++,console.error(`\u274C Test tidy up failed - ${i.message}`);break}}}console.table(f.testResults.map(t=>({Test:t.name,Result:t.error?`\u274C Failed - ${t.error.message}`:"\u2705 Passed"}))),f.failed>0?console.error(`\u274C Some Integration tests failed: ${f.failed} error(s)`):console.log("\u2705 All integration tests passed!")}};var h={client:null,bucket:"",uploadsUrl:"",init:async e=>{if(!e.bucket)throw new Error("S3 storage driver requires a bucket name.");if(!e.region)throw new Error("S3 storage driver requires a region.");if(!e.uploadsUrl)throw new Error("S3 storage driver requires an uploadsUrl.");let{S3Client:t,PutObjectCommand:o,GetObjectCommand:r,DeleteObjectCommand:s}=await import("@aws-sdk/client-s3");h.S3Client=t,h.PutObjectCommand=o,h.GetObjectCommand=r,h.DeleteObjectCommand=s,h.bucket=e.bucket,h.uploadsUrl=e.uploadsUrl,h.client=new t({region:e.region,credentials:{accessKeyId:e.accessKeyId,secretAccessKey:e.secretAccessKey}})},upload:async({file:e,key:t,contentType:o})=>{let r=new h.PutObjectCommand({Bucket:h.bucket,Key:t,Body:e,ContentType:o});return await h.client.send(r),t},download:async({key:e,stream:t})=>{let o=new h.GetObjectCommand({Bucket:h.bucket,Key:e}),r=await h.client.send(o);if(t)return r.Body;{let s=[];for await(let a of r.Body)s.push(a);return Buffer.concat(s)}},delete:async({key:e})=>{let t=new h.DeleteObjectCommand({Bucket:h.bucket,Key:e});await h.client.send(t)},getUrl:async({key:e})=>`${h.uploadsUrl}/${e}`};var g={driverName:"",driver:null,uploadsUrl:"",init:async e=>{try{if(!e.driver){n.info("No storage driver specified, skipping storage initialization.");return}switch(e.driver){case"s3":g.driverName="s3",g.driver=h;break;default:throw new Error(`Unsupported storage type: ${e.driver}`)}await g.driver.init(e),g.uploadsUrl=e.uploadsUrl||""}catch(t){throw n.error(`Storage initialization error: ${t.message}`),t}},upload:async({file:e,key:t,contentType:o,metadata:r})=>{try{let s=await g.driver.upload({file:e,key:t,contentType:o,metadata:r});return`${g.uploadsUrl}/${s}`}catch(s){throw n.error(`Storage upload error: ${s.message}`),s}},download:async({key:e,stream:t})=>{try{return await g.driver.download({key:e,stream:t})}catch(o){throw n.error(`Storage download error: ${o.message}`),o}},delete:async({key:e})=>{try{return await g.driver.delete({key:e})}catch(t){throw n.error(`Storage delete error: ${t.message}`),t}},getUrl:async({key:e,expiresIn:t})=>{try{return await g.driver.getUrl({key:e,expiresIn:t})}catch(o){throw n.error(`Storage getUrl error: ${o.message}`),o}}};import{v4 as xe}from"uuid";import{v5 as $e}from"uuid";var Te=await import(process.env.GLOBAL_SERVICE_BASE_DIR+"config.js"),F=Te.config,c={init:async e=>{if(c.config=e,c.http=W,e.http&&await c.http.init(e.http),c.commands=b,c.commands.init(e),e.db&&e.db.type)try{c.db=await B(e.db)}catch(t){n.error("Error connecting to database: "+t),process.exit(1)}if(c.db||(c.db={}),c.db.checkConnection=X,c.db.migrations=C,c.db.seeders=S,y.init(c.db),c.db.sql={},c.db.sql.helpers=y,c.http&&c.http.addHook("onReady",async()=>{setInterval(()=>{b.execute("internalHealthCheck",{})},6e4)}),c.http&&te(c.http),P(e.message?.url||""),c.message=V,c.message.sendAwaitResponse=G,c.message.sendAndForget=O,c.webSockets=N,c.commands.register(`${e.serviceName}.runMigrations`,async()=>await C.runMigrations({config:e})),c.commands.register(`${e.serviceName}.runSeeders`,async({seeder:t})=>await S.runSeeders({config:e,seeder:t})),c.commands.register(`${e.serviceName}.internalHealthCheck`,ee),c.commands.register(`${e.serviceName}.dropDatabaseData`,Z),c.schema=re,c.logger=n,c.logger.init({cloudProjectId:e.cloud?.projectId||"",serviceName:e.serviceName,flushIntervalMs:e.cloud?.logger.flushIntervalMs||5e3,transports:e.cloud?.logger||[]}),c.error={notFound:k,badRequest:L,unauthorised:q,failedHealthCheck:D},c.utils={uuid:()=>xe(),hash:(t,o)=>$e(t,o)},c.http&&c.http.addHook("onRequest",async(t,o)=>{let{url:r,method:s}=t;if(!r.endsWith("/")&&!r.includes(".")&&r!=="/"){let l=new URL(t.raw.url,`http://${t.headers.host}`);l.pathname+="/",t.raw.url=l.pathname+(l.search||"")}let a=t.raw.headers.authorization||"",i=a?a.split(" ")[1]:"";if(i){let l=await c.commands.execute("userService.getAuthenticatedUser",{token:i});l&&(t.user=l)}}),c.registerService=async()=>{if(e.serviceName!=="controlService")try{await c.commands.execute("controlService.registerService",{service:{name:e.serviceName,manifest:w.manifest}}),c.logger.info(`Service ${e.serviceName} registered with control service.`)}catch(t){c.logger.error(`Failed to register service ${e.serviceName} with control service: ${t.message}`)}},c.test=f,e.storage&&e.storage.driver)g.init(e.storage),c.storage=g;else{let t=()=>{throw new Error("Storage service not configured - please configure storage in config.js")};c.storage={upload:t,download:t,getUrl:t}}c.rabbit=J,c.http||(c.http={})}};await c.init(F);var mr=c,{commands:ur,http:pr,message:fr,db:hr,schema:gr,logger:wr,error:yr,utils:Er,registerService:br,webSockets:Sr,test:vr,storage:Rr,rabbit:Mr}=c;export{ur as commands,F as config,hr as db,mr as default,yr as error,pr as http,wr as logger,fr as message,Mr as rabbit,br as registerService,gr as schema,Rr as storage,vr as test,Er as utils,Sr as webSockets};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gnar-engine/core",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "Gnar Engine - Service Core",
5
5
  "type": "module",
6
6
  "main": "dist/core.js",