@spytecgps/nova-orm 1.4.240 → 1.4.242

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.
@@ -0,0 +1,81 @@
1
+ import { QueryRunner } from 'typeorm';
2
+ import { QueryResultCache } from 'typeorm/cache/QueryResultCache';
3
+ import { QueryResultCacheOptions } from 'typeorm/cache/QueryResultCacheOptions';
4
+ import { Logger } from '../types/logger';
5
+ export type CacheKind = 'lru' | 'redis' | 'unknown' | (string & Record<never, never>);
6
+ export interface InstrumentedQueryResultCacheOptions {
7
+ /**
8
+ * Tag for the underlying backend. Surfaces as the `kind` attribute on every
9
+ * emitted metric so dashboards can split LRU vs Redis cost/hit behavior.
10
+ */
11
+ kind: CacheKind;
12
+ /**
13
+ * Optional logger. When provided, a summary line is logged via `flushSummary()`
14
+ * and sampled per-op trace logs are emitted if `traceSampleRate > 0`.
15
+ */
16
+ logger?: Logger;
17
+ /**
18
+ * Fraction in [0, 1] of get/store ops to log at trace level with the full
19
+ * identifier. Useful for tracking down hot keys; default 0 (off).
20
+ */
21
+ traceSampleRate?: number;
22
+ /**
23
+ * Cap on how many distinct key prefixes are tracked in the per-instance
24
+ * miss-count map. Prevents unbounded memory growth on services with
25
+ * unexpected key shapes. Default 256.
26
+ */
27
+ maxTrackedPrefixes?: number;
28
+ }
29
+ interface InstrumentationStats {
30
+ hit: number;
31
+ miss: number;
32
+ expired: number;
33
+ store: number;
34
+ remove: number;
35
+ error: number;
36
+ getLatencyMsSum: number;
37
+ getLatencyMsMax: number;
38
+ storeLatencyMsSum: number;
39
+ storeLatencyMsMax: number;
40
+ payloadBytesSum: number;
41
+ payloadBytesMax: number;
42
+ missesByPrefix: Map<string, number>;
43
+ }
44
+ /**
45
+ * Decorates any QueryResultCache (LRU, Redis-backed, or otherwise) with
46
+ * OTel metrics + a lightweight in-process counter that can be dumped as a
47
+ * single log line per Lambda invocation.
48
+ *
49
+ * Hit-rate, lookup latency, payload size, and a low-cardinality miss-by-prefix
50
+ * breakdown all become visible without per-callsite changes. Same wrapper for
51
+ * every backend, so cross-service comparisons (Redis hit% on alerts-2.0 vs
52
+ * forwarder, LRU thrash on api-auth) come for free.
53
+ */
54
+ export declare class InstrumentedQueryResultCache implements QueryResultCache {
55
+ private readonly inner;
56
+ private readonly kind;
57
+ private readonly logger?;
58
+ private readonly traceSampleRate;
59
+ private readonly maxTrackedPrefixes;
60
+ private stats;
61
+ constructor(inner: QueryResultCache, options: InstrumentedQueryResultCacheOptions);
62
+ connect(): Promise<void>;
63
+ disconnect(): Promise<void>;
64
+ synchronize(queryRunner?: QueryRunner): Promise<void>;
65
+ getFromCache(options: QueryResultCacheOptions, queryRunner?: QueryRunner): Promise<QueryResultCacheOptions | undefined>;
66
+ storeInCache(options: QueryResultCacheOptions, savedCache?: QueryResultCacheOptions, queryRunner?: QueryRunner): Promise<void>;
67
+ isExpired(saved: QueryResultCacheOptions): boolean;
68
+ remove(identifiers: string[], queryRunner?: QueryRunner): Promise<void>;
69
+ clear(): Promise<void>;
70
+ /**
71
+ * Emit one log line summarizing every op since the last call, then reset.
72
+ * Intended for per-Lambda-invocation middleware hooks; safe to call
73
+ * periodically from long-running processes.
74
+ */
75
+ flushSummary(): void;
76
+ /** Test-only / introspection accessor. Do not rely on shape across versions. */
77
+ snapshotStats(): Readonly<InstrumentationStats>;
78
+ private trackMissPrefix;
79
+ private maybeTraceLog;
80
+ }
81
+ export {};
@@ -0,0 +1 @@
1
+ "use strict";var t=require("@opentelemetry/api");const e=()=>({hit:0,miss:0,expired:0,store:0,remove:0,error:0,getLatencyMsSum:0,getLatencyMsMax:0,storeLatencyMsSum:0,storeLatencyMsMax:0,payloadBytesSum:0,payloadBytesMax:0,missesByPrefix:new Map}),s=t.metrics.getMeter("@spytecgps/nova-orm"),r=s.createCounter("nova.cache.ops",{description:"Count of nova-orm query-result cache operations"}),i=s.createHistogram("nova.cache.lookup_duration_ms",{description:"Duration of nova-orm query-result cache lookups",unit:"ms"}),o=s.createHistogram("nova.cache.store_duration_ms",{description:"Duration of nova-orm query-result cache stores",unit:"ms"}),a=s.createHistogram("nova.cache.payload_bytes",{description:"Approximate cached payload size (JSON length proxy)",unit:"By"});const n=/[0-9]/;function c(){const t=process.hrtime();return 1e3*t[0]+t[1]/1e6}function h(t){if(null==t)return 0;if("string"==typeof t)return t.length;if(Buffer.isBuffer(t))return t.length;try{return JSON.stringify(t).length}catch{return 0}}exports.InstrumentedQueryResultCache=class InstrumentedQueryResultCache{inner;kind;logger;traceSampleRate;maxTrackedPrefixes;stats=e();constructor(t,e){this.inner=t,this.kind=e.kind,this.logger=e.logger,this.traceSampleRate=e.traceSampleRate??0,this.maxTrackedPrefixes=e.maxTrackedPrefixes??256}async connect(){return this.inner.connect()}async disconnect(){return this.inner.disconnect()}async synchronize(t){return this.inner.synchronize?.(t)}async getFromCache(t,e){const s=c();try{const o=await this.inner.getFromCache(t,e),n=c()-s;let d;d=o?this.inner.isExpired(o)?"expired":"hit":"miss",this.stats[d]+=1,this.stats.getLatencyMsSum+=n,n>this.stats.getLatencyMsMax&&(this.stats.getLatencyMsMax=n);const u={kind:this.kind,op:"get",outcome:d};if(r.add(1,u),i.record(n,u),"hit"===d&&o){const t=h(o.result);t>0&&a.record(t,{kind:this.kind,op:"get"})}return"hit"!==d&&this.trackMissPrefix(t),this.maybeTraceLog("get",t,n,d),o}catch(t){this.stats.error+=1;const e=c()-s,o={kind:this.kind,op:"get",outcome:"error"};throw r.add(1,o),i.record(e,o),t}}async storeInCache(t,e,s){const i=c();try{await this.inner.storeInCache(t,e,s);const n=c()-i;this.stats.store+=1,this.stats.storeLatencyMsSum+=n,n>this.stats.storeLatencyMsMax&&(this.stats.storeLatencyMsMax=n);const d={kind:this.kind,op:"store",outcome:"ok"};r.add(1,d),o.record(n,d);const u=h(t.result);u>0&&(a.record(u,{kind:this.kind,op:"store"}),this.stats.payloadBytesSum+=u,u>this.stats.payloadBytesMax&&(this.stats.payloadBytesMax=u)),this.maybeTraceLog("store",t,n,"ok")}catch(t){throw this.stats.error+=1,r.add(1,{kind:this.kind,op:"store",outcome:"error"}),t}}isExpired(t){return this.inner.isExpired(t)}async remove(t,e){try{await this.inner.remove(t,e),this.stats.remove+=t.length,r.add(t.length,{kind:this.kind,op:"remove",outcome:"ok"})}catch(t){throw this.stats.error+=1,r.add(1,{kind:this.kind,op:"remove",outcome:"error"}),t}}async clear(){return this.inner.clear()}flushSummary(){if(!this.logger)return void(this.stats=e());const t=this.stats,s=t.hit+t.miss+t.expired,r=s>0?t.hit/s:null,i=s>0?t.getLatencyMsSum/s:0,o=t.store>0?t.storeLatencyMsSum/t.store:0,a=[...t.missesByPrefix.entries()].sort(((t,e)=>e[1]-t[1])).slice(0,10).map((([t,e])=>({prefix:t,count:e})));(s>0||t.store>0||t.remove>0)&&this.logger.info({cache:{kind:this.kind,gets:s,hits:t.hit,misses:t.miss,expired:t.expired,hitRate:r,stores:t.store,removes:t.remove,errors:t.error,avgGetMs:Number(i.toFixed(3)),maxGetMs:Number(t.getLatencyMsMax.toFixed(3)),avgStoreMs:Number(o.toFixed(3)),maxStoreMs:Number(t.storeLatencyMsMax.toFixed(3)),payloadBytesMax:t.payloadBytesMax,payloadBytesAvgStore:t.store>0?Math.round(t.payloadBytesSum/t.store):0,topMissPrefixes:a}},"nova-orm cache summary"),this.stats=e()}snapshotStats(){return this.stats}trackMissPrefix(t){const e=t.identifier??t.query;if(!e)return;const s=function(t){const e=t.split(":"),s=[];for(const t of e)if(t){if(n.test(t[0]))break;if(s.push(t),s.length>=3)break}if(0===s.length){const e=t.indexOf(" ");return e>0?t.slice(0,e):t.slice(0,32)}return s.join(":")}(e),r=this.stats.missesByPrefix,i=r.get(s);void 0!==i?r.set(s,i+1):r.size<this.maxTrackedPrefixes&&r.set(s,1)}maybeTraceLog(t,e,s,r){!this.logger||this.traceSampleRate<=0||Math.random()>=this.traceSampleRate||this.logger.trace({cache:{kind:this.kind,op:t,outcome:r,durationMs:Number(s.toFixed(3)),identifier:e.identifier}},"nova-orm cache op")}};
@@ -6,7 +6,15 @@ export interface LruQueryResultCacheConfig {
6
6
  }
7
7
  export declare class LruQueryResultCache implements QueryResultCache {
8
8
  private readonly cache;
9
+ private readonly maxSize;
9
10
  constructor(config?: LruQueryResultCacheConfig);
11
+ /**
12
+ * Emit `nova.cache.lru.size` and `nova.cache.lru.max` as observable gauges so
13
+ * dashboards can tell "is the LRU full?" directly instead of inferring from
14
+ * hit-rate decay. Filter by `ServiceName` to disambiguate when multiple
15
+ * DataSources share a process.
16
+ */
17
+ private registerSizeGauges;
10
18
  connect(): Promise<void>;
11
19
  disconnect(): Promise<void>;
12
20
  synchronize(): Promise<void>;
@@ -1 +1 @@
1
- "use strict";var e=require("lru-cache");exports.LruQueryResultCache=class LruQueryResultCache{cache;constructor(c={}){this.cache=new e.LRUCache({max:c.max??1e3,ttl:c.maxTtlMs})}async connect(){}async disconnect(){this.cache.clear()}async synchronize(){}async getFromCache(e){const c=this.keyFor(e);if(c)return this.cache.get(c)}async storeInCache(e){const c=this.keyFor(e);if(!c)return;const t={...e,time:e.time??Date.now()};this.cache.set(c,t)}isExpired(e){const c=e.time??0;return Date.now()-c>=e.duration}async remove(e){for(const c of e)this.cache.delete(c)}async clear(){this.cache.clear()}keyFor(e){return e.identifier??e.query}};
1
+ "use strict";var e=require("@opentelemetry/api"),c=require("lru-cache");exports.LruQueryResultCache=class LruQueryResultCache{cache;maxSize;constructor(e={}){this.maxSize=e.max??1e3,this.cache=new c.LRUCache({max:this.maxSize,ttl:e.maxTtlMs}),this.registerSizeGauges()}registerSizeGauges(){const c=e.metrics.getMeter("@spytecgps/nova-orm");c.createObservableGauge("nova.cache.lru.size",{description:"Current entry count in the in-process LRU query result cache"}).addCallback((e=>{e.observe(this.cache.size)})),c.createObservableGauge("nova.cache.lru.max",{description:"Configured max entry count for the in-process LRU query result cache"}).addCallback((e=>{e.observe(this.maxSize)}))}async connect(){}async disconnect(){this.cache.clear()}async synchronize(){}async getFromCache(e){const c=this.keyFor(e);if(c)return this.cache.get(c)}async storeInCache(e){const c=this.keyFor(e);if(!c)return;const t={...e,time:e.time??Date.now()};this.cache.set(c,t)}isExpired(e){const c=e.time??0;return Date.now()-c>=e.duration}async remove(e){for(const c of e)this.cache.delete(c)}async clear(){this.cache.clear()}keyFor(e){return e.identifier??e.query}};
@@ -0,0 +1,27 @@
1
+ import { Logger } from '../types/logger';
2
+ /**
3
+ * Shape of TypeORM's RedisQueryResultCache once the underlying client is
4
+ * materialized. We reach into it via duck-typing because TypeORM does not
5
+ * export the type.
6
+ */
7
+ export interface RedisQueryResultCacheLike {
8
+ client?: {
9
+ on?: (event: string, fn: (err: Error) => void) => void;
10
+ };
11
+ clientType?: string;
12
+ __novaErrorHandlerAttached?: boolean;
13
+ }
14
+ /**
15
+ * TypeORM's RedisQueryResultCache only attaches an `error` listener on the
16
+ * underlying redis client for `clientType === 'redis'`. With `'ioredis'` (what
17
+ * every hapn service uses to talk to ElastiCache Valkey) the client is an
18
+ * EventEmitter that crashes the host process on the first unhandled `error`
19
+ * event — a TLS hiccup or `MOVED` redirect during failover takes down the
20
+ * Lambda even with `ignoreErrors: true` set in TypeORM config.
21
+ *
22
+ * Idempotent: re-running on the same cache is a no-op.
23
+ *
24
+ * Returns true if a handler was attached on this call (useful for tests and
25
+ * for callers that want to observe whether anything happened).
26
+ */
27
+ export declare function attachIoredisErrorHandler(cache: RedisQueryResultCacheLike | undefined | null, logger: Logger): boolean;
@@ -0,0 +1 @@
1
+ "use strict";exports.attachIoredisErrorHandler=function(e,r){return!!e&&(!e.__novaErrorHandlerAttached&&(!!e.clientType?.startsWith("ioredis")&&("function"==typeof e.client?.on&&(e.client.on("error",(e=>{r.warn({message:e.message},"Redis cache connection problem (ignored)")})),e.__novaErrorHandlerAttached=!0,!0))))};
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Build the TypeORM `cache:` config object from REDIS_* env vars so consumers
3
+ * don't have to embed mode-switching logic in every service. SSM-driven
4
+ * `/${stage}/cache/<name>/redis/{host,port,tls,mode}` keys map 1:1 to the env
5
+ * vars and this helper returns the right TypeORM shape.
6
+ *
7
+ * Returns undefined if REDIS_HOST is absent, so it's safe to spread into a
8
+ * DataSource config that may or may not have a cache configured.
9
+ *
10
+ * Supported modes:
11
+ * - "standalone" (default): uses TypeORM's built-in RedisQueryResultCache
12
+ * with the `ioredis` client type. One node, no cluster topology.
13
+ * - "cluster": custom client passed through TypeORM's `client` field; uses
14
+ * `ioredis` Cluster mode with the ElastiCache-cluster-mode safety
15
+ * defaults baked in (enableOfflineQueue:false, checkServerIdentity bypass
16
+ * for TLS — see project_elasticache_cluster_ioredis_tls).
17
+ */
18
+ export type CacheMode = 'standalone' | 'cluster';
19
+ export interface CacheFromEnvOptions {
20
+ /** Env var name for host. Default: REDIS_HOST. Override to support multiple caches in one process. */
21
+ hostEnv?: string;
22
+ portEnv?: string;
23
+ tlsEnv?: string;
24
+ modeEnv?: string;
25
+ }
26
+ export interface BuiltCacheConfig {
27
+ type?: 'ioredis';
28
+ options?: Record<string, unknown>;
29
+ client?: unknown;
30
+ ignoreErrors?: boolean;
31
+ }
32
+ export declare function buildCacheFromEnv(options?: CacheFromEnvOptions): BuiltCacheConfig | undefined;
@@ -0,0 +1 @@
1
+ "use strict";let e;exports.buildCacheFromEnv=function(r={}){const t=r.hostEnv??"REDIS_HOST",n=r.portEnv??"REDIS_PORT",o=r.tlsEnv??"REDIS_TLS",s=r.modeEnv??"REDIS_MODE",i=process.env[t];if(!i)return;const u=Number(process.env[n]??"6379"),c="true"===process.env[o];if("cluster"===(process.env[s]??"standalone")){const r=function(){if(void 0!==e)return e;try{e=require("ioredis")}catch{e=null}return e}();if(!r)throw new Error("buildCacheFromEnv: REDIS_MODE=cluster requires `ioredis` to be installed");return{client:new r.Cluster([{host:i,port:u}],{redisOptions:c?{tls:{checkServerIdentity:()=>{}}}:{},enableOfflineQueue:!1}),ignoreErrors:!0}}return{type:"ioredis",options:{host:i,port:u,tls:c?{}:void 0},ignoreErrors:!0}};
@@ -1 +1,4 @@
1
+ export { attachIoredisErrorHandler, RedisQueryResultCacheLike } from './attachIoredisErrorHandler';
2
+ export { buildCacheFromEnv, BuiltCacheConfig, CacheFromEnvOptions, CacheMode, } from './buildCacheFromEnv';
3
+ export { CacheKind, InstrumentedQueryResultCache, InstrumentedQueryResultCacheOptions, } from './InstrumentedQueryResultCache';
1
4
  export { LruQueryResultCache, LruQueryResultCacheConfig } from './LruQueryResultCache';
@@ -1 +1 @@
1
- "use strict";var e=require("./LruQueryResultCache.js");exports.LruQueryResultCache=e.LruQueryResultCache;
1
+ "use strict";var e=require("./attachIoredisErrorHandler.js"),r=require("./buildCacheFromEnv.js"),u=require("./InstrumentedQueryResultCache.js"),t=require("./LruQueryResultCache.js");exports.attachIoredisErrorHandler=e.attachIoredisErrorHandler,exports.buildCacheFromEnv=r.buildCacheFromEnv,exports.InstrumentedQueryResultCache=u.InstrumentedQueryResultCache,exports.LruQueryResultCache=t.LruQueryResultCache;
@@ -1,13 +1,13 @@
1
1
  'use strict';
2
2
 
3
3
  var _1684483704434AddLowerCaseIndex = require('../1684483704434-addLowerCaseIndex.js');
4
- var _1684483705002UpdateSecurityUserViewsV2 = require('../1684483705002-updateSecurityUserViewsV2.js');
5
4
  var _1684483705001UpdateSecurityUserViewsV1 = require('../1684483705001-updateSecurityUserViewsV1.js');
5
+ var _1684483705002UpdateSecurityUserViewsV2 = require('../1684483705002-updateSecurityUserViewsV2.js');
6
6
  var _1746434400000AddQuickSearchFulltextIndexes = require('../1746434400000-addQuickSearchFulltextIndexes.js');
7
7
 
8
8
 
9
9
 
10
10
  exports.addLowerCaseIndex1684483704434 = _1684483704434AddLowerCaseIndex.addLowerCaseIndex1684483704434;
11
- exports.updateSecurityUserViewsV21684483705002 = _1684483705002UpdateSecurityUserViewsV2.updateSecurityUserViewsV21684483705002;
12
11
  exports.updateSecurityUserViewsV11684483705001 = _1684483705001UpdateSecurityUserViewsV1.updateSecurityUserViewsV11684483705001;
12
+ exports.updateSecurityUserViewsV21684483705002 = _1684483705002UpdateSecurityUserViewsV2.updateSecurityUserViewsV21684483705002;
13
13
  exports.addQuickSearchFulltextIndexes1746434400000 = _1746434400000AddQuickSearchFulltextIndexes.addQuickSearchFulltextIndexes1746434400000;
@@ -55,6 +55,18 @@ export declare class NovaDataSource {
55
55
  * Performs connection to the database.
56
56
  */
57
57
  connect(): Promise<void>;
58
+ /**
59
+ * Wrap whatever cache provider TypeORM materialized (Redis, our LRU, or any
60
+ * future provider) with `InstrumentedQueryResultCache` so every consumer gets
61
+ * hit-rate / latency / payload-size metrics with zero callsite changes. Idempotent.
62
+ */
63
+ private wrapQueryResultCache;
64
+ /**
65
+ * If the active cache is instrumented, emit its summary log and reset stats.
66
+ * Safe to call at any time; no-op when the cache isn't instrumented.
67
+ * Intended for per-Lambda-invocation middleware hooks.
68
+ */
69
+ flushCacheSummary(): void;
58
70
  /**
59
71
  * Closes connection with the database.
60
72
  * Once connection is closed, you cannot use repositories or perform any operations except opening connection again.
@@ -1 +1 @@
1
- "use strict";var e=require("@opentelemetry/api"),r=require("typeorm");require("lru-cache");var t=require("./entities/index.js"),a=require("./subscribers/index.js"),s=require("./utils/mysqlSpanProcessor.js"),i=require("./utils/queryLogger.js"),o=require("./utils/queryTags.js"),n=require("./utils/spanSubscriber.js");require("fs"),require("mysql2/promise"),require("path"),require("./types/enums.js"),require("./types/logger.js"),require("./entities/retailAlertExternalRecipient.js"),require("./utils/dateTool.js");exports.NovaDataSource=class NovaDataSource{dataSource;logger;constructor(e,s,o){this.logger=s;const n=[];for(const e in t){const r=t[e];n.push(r)}const c=[];for(const e in a){const r=a[e];c.push(r)}const u={host:"aws.connect.psdb.cloud",port:3306,database:"nova",ssl:{rejectUnauthorized:!0}};this.dataSource=new r.DataSource({type:"mysql",connectorPackage:"mysql2",synchronize:!1,logging:!0,logger:new i.QueryLogger(this.logger,e.logging),entities:n,subscribers:c,migrations:["node_modules/@spytecgps/nova-orm/dist/migration/*.js"],supportBigNumbers:!0,bigNumberStrings:!1,extra:{decimalNumbers:!0},...u,...e,replication:{master:{...u,...e},slaves:o?[{...u,...o}]:[{...u,...e}],defaultMode:e.replicationMode??"master"}})}async connect(){if(!this.dataSource.isInitialized){const r=e.trace.getTracer("@spytecgps/nova-orm");await r.startActiveSpan("nova-orm.connect",(async r=>{try{await this.dataSource.initialize(),r.setAttribute("db.system","mysql");const e=this.dataSource.options;e.database&&r.setAttribute("db.name",e.database),r.setAttribute("nova_orm.entity_count",this.dataSource.entityMetadatas.length)}catch(t){throw r.recordException(t),r.setStatus({code:e.SpanStatusCode.ERROR}),t}finally{r.end()}}))}if(s.isOtelAutoInstrumented())s.installMysqlSpanRenamer();else if(!this.dataSource.subscribers.some((e=>e._spanSubscriber))){const e=n.createSpanSubscriber(this.logger);e._spanSubscriber=!0,this.dataSource.subscribers.push(e)}}async disconnect(){try{this.dataSource.isInitialized&&await this.dataSource.destroy()}catch(e){this.logger.error({error:e},"Failed to close connection to database")}}getRepository(e){return this.dataSource.getRepository(e)}async query(e,r,t){const a=/^\s*(?:\/\*[\s\S]*?\*\/\s*)*(INSERT|UPDATE|DELETE|REPLACE|TRUNCATE|CREATE|ALTER|DROP|RENAME|GRANT|REVOKE|LOCK|UNLOCK|SET|CALL|START|COMMIT|ROLLBACK)\b/i.test(e);if(t?.usePrimary||a)return this.dataSource.query(e,r,null);const s=this.dataSource.createQueryRunner("slave");try{return await s.query(e,r)}finally{await s.release()}}async createQueryBuilder(e,r){return this.dataSource.createQueryBuilder(e,r,null)}createQueryRunner(e){return this.dataSource.createQueryRunner(e)}createQueryRunnerFromParams(e){const r=e?.queryOptions?.forceUseReplica?"slave":"master";return this.logger.trace({replicationMode:r},"Creating query runner with replication mode"),this.dataSource.createQueryRunner(r)}async safeQuery(e,r,t){try{await this.connect();return await e(this,t)}catch(e){throw this.logger.error({error:e},r?r+" ":"Error while executing query"),e}finally{t&&await t.release(),await this.disconnect()}}async safeQuery2(r,t,a){this.dataSource.isInitialized||(this.logger.info("nova-orm new connection"),await this.connect());return e.trace.getTracer("nova-orm").startActiveSpan(t??"safeQuery2",(async s=>{try{return await r(this,a)}catch(r){throw s.recordException(r),s.setStatus({code:e.SpanStatusCode.ERROR,message:r?.message}),this.logger.error({error:r},t?t+" ":"Error while executing query"),r}finally{s.end(),a&&await a.release()}}))}async safeQuery3(e,r,t,{replicationMode:a,tag:s}={}){return(async()=>{let i;try{await this.connect(),a?(this.logger.trace({replicationMode:a},"Creating query runner with replication mode"),i=this.dataSource.createQueryRunner(a)):this.logger.trace({replicationMode:this.dataSource.defaultReplicationModeForReads},"Creating query builder with default replication mode for reads");const n=this.dataSource.createQueryBuilder(e,r,i);s&&n.comment(o.queryTags({action:s}));return await t(n)}catch(e){throw this.logger.error({error:e},s?s+" ":"Error while executing query"),e}finally{i&&await i.release()}})()}async syncSchema(e=!0){this.logger.warn("Initiating schema sync"),await this.connect(),await this.dataSource.synchronize(e),await this.dataSource.runMigrations()}async clearCache(){this.dataSource.queryResultCache?(this.logger.info("Clearing query result cache"),await this.dataSource.queryResultCache.clear()):this.logger.info("No query result cache to clear")}async invalidateCacheKey(e){this.dataSource.queryResultCache&&(this.logger.info("Invalidating cache key",{key:e}),await this.dataSource.queryResultCache.remove([e]))}};
1
+ "use strict";var e=require("@opentelemetry/api"),r=require("typeorm"),t=require("./cache/attachIoredisErrorHandler.js"),a=require("./cache/InstrumentedQueryResultCache.js"),s=require("./cache/LruQueryResultCache.js"),i=require("./entities/index.js"),n=require("./subscribers/index.js"),o=require("./utils/mysqlSpanProcessor.js"),c=require("./utils/queryLogger.js"),u=require("./utils/queryTags.js"),l=require("./utils/spanSubscriber.js");require("fs"),require("mysql2/promise"),require("path"),require("./types/enums.js"),require("./types/logger.js"),require("./entities/retailAlertExternalRecipient.js"),require("./utils/dateTool.js");exports.NovaDataSource=class NovaDataSource{dataSource;logger;constructor(e,t,a){this.logger=t;const s=[];for(const e in i){const r=i[e];s.push(r)}const o=[];for(const e in n){const r=n[e];o.push(r)}const u={host:"aws.connect.psdb.cloud",port:3306,database:"nova",ssl:{rejectUnauthorized:!0}};this.dataSource=new r.DataSource({type:"mysql",connectorPackage:"mysql2",synchronize:!1,logging:!0,logger:new c.QueryLogger(this.logger,e.logging),entities:s,subscribers:o,migrations:["node_modules/@spytecgps/nova-orm/dist/migration/*.js"],supportBigNumbers:!0,bigNumberStrings:!1,extra:{decimalNumbers:!0},...u,...e,replication:{master:{...u,...e},slaves:a?[{...u,...a}]:[{...u,...e}],defaultMode:e.replicationMode??"master"}})}async connect(){if(!this.dataSource.isInitialized){const r=e.trace.getTracer("@spytecgps/nova-orm");await r.startActiveSpan("nova-orm.connect",(async r=>{try{await this.dataSource.initialize(),r.setAttribute("db.system","mysql");const e=this.dataSource.options;e.database&&r.setAttribute("db.name",e.database),r.setAttribute("nova_orm.entity_count",this.dataSource.entityMetadatas.length)}catch(t){throw r.recordException(t),r.setStatus({code:e.SpanStatusCode.ERROR}),t}finally{r.end()}}))}if(o.isOtelAutoInstrumented())o.installMysqlSpanRenamer();else if(!this.dataSource.subscribers.some((e=>e._spanSubscriber))){const e=l.createSpanSubscriber(this.logger);e._spanSubscriber=!0,this.dataSource.subscribers.push(e)}this.wrapQueryResultCache()}wrapQueryResultCache(){const e=this.dataSource.queryResultCache;if(!e||e.__novaInstrumented)return;let r="unknown";e instanceof s.LruQueryResultCache?r="lru":"RedisQueryResultCache"===e.constructor?.name&&(r="redis",t.attachIoredisErrorHandler(e,this.logger));const i=new a.InstrumentedQueryResultCache(e,{kind:r,logger:this.logger});i.__novaInstrumented=!0,this.dataSource.queryResultCache=i}flushCacheSummary(){const e=this.dataSource.queryResultCache;e instanceof a.InstrumentedQueryResultCache&&e.flushSummary()}async disconnect(){try{this.dataSource.isInitialized&&await this.dataSource.destroy()}catch(e){this.logger.error({error:e},"Failed to close connection to database")}}getRepository(e){return this.dataSource.getRepository(e)}async query(e,r,t){const a=/^\s*(?:\/\*[\s\S]*?\*\/\s*)*(INSERT|UPDATE|DELETE|REPLACE|TRUNCATE|CREATE|ALTER|DROP|RENAME|GRANT|REVOKE|LOCK|UNLOCK|SET|CALL|START|COMMIT|ROLLBACK)\b/i.test(e);if(t?.usePrimary||a)return this.dataSource.query(e,r,null);const s=this.dataSource.createQueryRunner("slave");try{return await s.query(e,r)}finally{await s.release()}}async createQueryBuilder(e,r){return this.dataSource.createQueryBuilder(e,r,null)}createQueryRunner(e){return this.dataSource.createQueryRunner(e)}createQueryRunnerFromParams(e){const r=e?.queryOptions?.forceUseReplica?"slave":"master";return this.logger.trace({replicationMode:r},"Creating query runner with replication mode"),this.dataSource.createQueryRunner(r)}async safeQuery(e,r,t){try{await this.connect();return await e(this,t)}catch(e){throw this.logger.error({error:e},r?r+" ":"Error while executing query"),e}finally{t&&await t.release(),await this.disconnect()}}async safeQuery2(r,t,a){this.dataSource.isInitialized||(this.logger.info("nova-orm new connection"),await this.connect());return e.trace.getTracer("nova-orm").startActiveSpan(t??"safeQuery2",(async s=>{try{return await r(this,a)}catch(r){throw s.recordException(r),s.setStatus({code:e.SpanStatusCode.ERROR,message:r?.message}),this.logger.error({error:r},t?t+" ":"Error while executing query"),r}finally{s.end(),a&&await a.release()}}))}async safeQuery3(e,r,t,{replicationMode:a,tag:s}={}){return(async()=>{let i;try{await this.connect(),a?(this.logger.trace({replicationMode:a},"Creating query runner with replication mode"),i=this.dataSource.createQueryRunner(a)):this.logger.trace({replicationMode:this.dataSource.defaultReplicationModeForReads},"Creating query builder with default replication mode for reads");const n=this.dataSource.createQueryBuilder(e,r,i);s&&n.comment(u.queryTags({action:s}));return await t(n)}catch(e){throw this.logger.error({error:e},s?s+" ":"Error while executing query"),e}finally{i&&await i.release()}})()}async syncSchema(e=!0){this.logger.warn("Initiating schema sync"),await this.connect(),await this.dataSource.synchronize(e),await this.dataSource.runMigrations()}async clearCache(){this.dataSource.queryResultCache?(this.logger.info("Clearing query result cache"),await this.dataSource.queryResultCache.clear()):this.logger.info("No query result cache to clear")}async invalidateCacheKey(e){this.dataSource.queryResultCache&&(this.logger.info("Invalidating cache key",{key:e}),await this.dataSource.queryResultCache.remove([e]))}};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spytecgps/nova-orm",
3
- "version": "1.4.240",
3
+ "version": "1.4.242",
4
4
  "description": "ORM with PlanetScale",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
package/schema.sql CHANGED
@@ -1,6 +1,6 @@
1
- -- nova-orm-schema-checksum: 974548f26b51576a57369da39c7884de
1
+ -- nova-orm-schema-checksum: bdf84e9724e3cf6c308f89f1c1871778
2
2
  -- Nova ORM Schema Dump
3
- -- Generated at: 2026-05-20T16:22:54.266Z
3
+ -- Generated at: 2026-05-24T15:55:45.644Z
4
4
 
5
5
  SET FOREIGN_KEY_CHECKS = 0;
6
6