@fintekkers/ledger-models 0.1.131 → 0.1.133

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 (151) hide show
  1. package/node/fintekkers/models/portfolio/portfolio_pb.js +19 -13
  2. package/node/fintekkers/models/position/field_pb.js +7 -1
  3. package/node/fintekkers/models/position/measure_pb.js +7 -1
  4. package/node/fintekkers/models/position/position_filter_pb.js +13 -7
  5. package/node/fintekkers/models/position/position_pb.js +17 -11
  6. package/node/fintekkers/models/position/position_status_pb.js +7 -1
  7. package/node/fintekkers/models/position/position_util_pb.js +17 -11
  8. package/node/fintekkers/models/price/price_pb.js +20 -14
  9. package/node/fintekkers/models/price/price_type_pb.js +7 -1
  10. package/node/fintekkers/models/security/bond/auction_type_pb.js +7 -1
  11. package/node/fintekkers/models/security/bond/issuance_pb.js +23 -17
  12. package/node/fintekkers/models/security/coupon_frequency_pb.js +7 -1
  13. package/node/fintekkers/models/security/coupon_type_pb.js +7 -1
  14. package/node/fintekkers/models/security/identifier/identifier_pb.js +15 -9
  15. package/node/fintekkers/models/security/identifier/identifier_type_pb.js +7 -1
  16. package/node/fintekkers/models/security/index/index_type_pb.js +7 -1
  17. package/node/fintekkers/models/security/index_composition_grpc_pb.js +1 -0
  18. package/node/fintekkers/models/security/index_composition_pb.js +29 -23
  19. package/node/fintekkers/models/security/security_pb.d.ts +6 -0
  20. package/node/fintekkers/models/security/security_pb.js +147 -90
  21. package/node/fintekkers/models/security/security_quantity_type_pb.js +7 -1
  22. package/node/fintekkers/models/security/security_type_pb.js +7 -1
  23. package/node/fintekkers/models/security/tenor_pb.js +15 -9
  24. package/node/fintekkers/models/security/tenor_type_pb.js +7 -1
  25. package/node/fintekkers/models/strategy/strategy_allocation_pb.js +19 -13
  26. package/node/fintekkers/models/strategy/strategy_pb.js +20 -14
  27. package/node/fintekkers/models/transaction/transaction_pb.js +30 -24
  28. package/node/fintekkers/models/transaction/transaction_type_pb.js +7 -1
  29. package/node/fintekkers/models/util/api/api_key_pb.js +16 -10
  30. package/node/fintekkers/models/util/currency_grpc_pb.js +1 -0
  31. package/node/fintekkers/models/util/currency_pb.js +10 -4
  32. package/node/fintekkers/models/util/date_range_pb.js +14 -8
  33. package/node/fintekkers/models/util/decimal_value_pb.js +10 -4
  34. package/node/fintekkers/models/util/endpoint_pb.js +13 -7
  35. package/node/fintekkers/models/util/local_date_pb.js +11 -5
  36. package/node/fintekkers/models/util/local_timestamp_pb.js +11 -5
  37. package/node/fintekkers/models/util/lock/node_partition_pb.js +15 -9
  38. package/node/fintekkers/models/util/lock/node_state_pb.js +16 -10
  39. package/node/fintekkers/models/util/uuid_pb.js +9 -3
  40. package/node/fintekkers/models/valuation/cashflow_grpc_pb.js +1 -0
  41. package/node/fintekkers/models/valuation/cashflow_pb.js +13 -7
  42. package/node/fintekkers/requests/index_composition/create_index_composition_request_grpc_pb.js +1 -0
  43. package/node/fintekkers/requests/index_composition/create_index_composition_request_pb.js +22 -16
  44. package/node/fintekkers/requests/index_composition/get_index_composition_request_grpc_pb.js +1 -0
  45. package/node/fintekkers/requests/index_composition/get_index_composition_request_pb.js +22 -16
  46. package/node/fintekkers/requests/portfolio/create_portfolio_request_pb.js +13 -7
  47. package/node/fintekkers/requests/portfolio/create_portfolio_response_pb.js +14 -8
  48. package/node/fintekkers/requests/portfolio/query_portfolio_request_pb.js +17 -11
  49. package/node/fintekkers/requests/portfolio/query_portfolio_response_pb.js +14 -8
  50. package/node/fintekkers/requests/position/query_position_request_pb.js +27 -15
  51. package/node/fintekkers/requests/position/query_position_response_pb.js +16 -10
  52. package/node/fintekkers/requests/price/create_price_request_pb.js +13 -7
  53. package/node/fintekkers/requests/price/create_price_response_pb.js +14 -8
  54. package/node/fintekkers/requests/price/query_price_request_pb.d.ts +3 -0
  55. package/node/fintekkers/requests/price/query_price_request_pb.js +48 -12
  56. package/node/fintekkers/requests/price/query_price_response_pb.d.ts +7 -0
  57. package/node/fintekkers/requests/price/query_price_response_pb.js +68 -9
  58. package/node/fintekkers/requests/security/create_security_request_pb.js +13 -7
  59. package/node/fintekkers/requests/security/create_security_response_pb.js +15 -9
  60. package/node/fintekkers/requests/security/get_field_values_request_pb.js +13 -7
  61. package/node/fintekkers/requests/security/get_field_values_response_pb.js +13 -7
  62. package/node/fintekkers/requests/security/get_fields_response_pb.js +17 -8
  63. package/node/fintekkers/requests/security/query_security_request_pb.js +17 -11
  64. package/node/fintekkers/requests/security/query_security_response_pb.js +15 -9
  65. package/node/fintekkers/requests/transaction/create_transaction_request_pb.js +13 -7
  66. package/node/fintekkers/requests/transaction/create_transaction_response_pb.js +14 -8
  67. package/node/fintekkers/requests/transaction/query_transaction_request_pb.js +16 -10
  68. package/node/fintekkers/requests/transaction/query_transaction_response_pb.js +15 -9
  69. package/node/fintekkers/requests/util/delete_request_grpc_pb.js +1 -0
  70. package/node/fintekkers/requests/util/delete_request_pb.js +34 -28
  71. package/node/fintekkers/requests/util/errors/error_pb.js +13 -7
  72. package/node/fintekkers/requests/util/errors/message_pb.js +12 -6
  73. package/node/fintekkers/requests/util/errors/summary_pb.js +10 -4
  74. package/node/fintekkers/requests/util/lock/lock_request_pb.js +14 -8
  75. package/node/fintekkers/requests/util/lock/lock_response_pb.js +15 -9
  76. package/node/fintekkers/requests/util/operation_pb.js +7 -1
  77. package/node/fintekkers/requests/valuation/curve_request_grpc_pb.js +1 -0
  78. package/node/fintekkers/requests/valuation/curve_request_pb.d.ts +12 -0
  79. package/node/fintekkers/requests/valuation/curve_request_pb.js +125 -14
  80. package/node/fintekkers/requests/valuation/curve_response_grpc_pb.js +1 -0
  81. package/node/fintekkers/requests/valuation/curve_response_pb.js +21 -15
  82. package/node/fintekkers/requests/valuation/product_inputs.test.d.ts +6 -0
  83. package/node/fintekkers/requests/valuation/product_inputs.test.js +146 -0
  84. package/node/fintekkers/requests/valuation/product_inputs.test.js.map +1 -0
  85. package/node/fintekkers/requests/valuation/product_inputs_grpc_pb.js +1 -0
  86. package/node/fintekkers/requests/valuation/product_inputs_pb.d.ts +42 -0
  87. package/node/fintekkers/requests/valuation/product_inputs_pb.js +360 -27
  88. package/node/fintekkers/requests/valuation/valuation_request_pb.js +25 -16
  89. package/node/fintekkers/requests/valuation/valuation_response_pb.js +16 -10
  90. package/node/fintekkers/services/index-composition-service/index_composition_service_grpc_pb.js +14 -14
  91. package/node/fintekkers/services/index-composition-service/index_composition_service_pb.js +7 -1
  92. package/node/fintekkers/services/lock-service/lock_service_grpc_pb.js +23 -23
  93. package/node/fintekkers/services/lock-service/lock_service_pb.js +21 -15
  94. package/node/fintekkers/services/portfolio-service/portfolio_service_grpc_pb.js +8 -8
  95. package/node/fintekkers/services/portfolio-service/portfolio_service_pb.js +7 -1
  96. package/node/fintekkers/services/position-service/position_service_grpc_pb.js +10 -10
  97. package/node/fintekkers/services/position-service/position_service_pb.js +7 -1
  98. package/node/fintekkers/services/price-service/price_service_grpc_pb.js +6 -6
  99. package/node/fintekkers/services/price-service/price_service_pb.js +7 -1
  100. package/node/fintekkers/services/security-service/security_service_grpc_pb.js +12 -12
  101. package/node/fintekkers/services/security-service/security_service_pb.js +7 -1
  102. package/node/fintekkers/services/transaction-service/transaction_service_grpc_pb.js +11 -11
  103. package/node/fintekkers/services/transaction-service/transaction_service_pb.js +7 -1
  104. package/node/fintekkers/services/valuation-service/valuation_service_grpc_pb.js +5 -5
  105. package/node/fintekkers/services/valuation-service/valuation_service_pb.js +7 -1
  106. package/node/wrappers/models/price/Price.d.ts +5 -0
  107. package/node/wrappers/models/price/Price.js +7 -0
  108. package/node/wrappers/models/price/Price.js.map +1 -1
  109. package/node/wrappers/models/price/Price.ts +8 -0
  110. package/node/wrappers/models/security/BondSecurity.d.ts +8 -0
  111. package/node/wrappers/models/security/BondSecurity.js +13 -0
  112. package/node/wrappers/models/security/BondSecurity.js.map +1 -1
  113. package/node/wrappers/models/security/BondSecurity.ts +13 -0
  114. package/node/wrappers/models/security/identifier.d.ts +26 -0
  115. package/node/wrappers/models/security/identifier.js +39 -0
  116. package/node/wrappers/models/security/identifier.js.map +1 -1
  117. package/node/wrappers/models/security/identifier.test.js +62 -0
  118. package/node/wrappers/models/security/identifier.test.js.map +1 -1
  119. package/node/wrappers/models/security/identifier.test.ts +70 -0
  120. package/node/wrappers/models/security/identifier.ts +44 -0
  121. package/node/wrappers/models/security/security.d.ts +42 -1
  122. package/node/wrappers/models/security/security.js +53 -2
  123. package/node/wrappers/models/security/security.js.map +1 -1
  124. package/node/wrappers/models/security/security.test.js +72 -0
  125. package/node/wrappers/models/security/security.test.js.map +1 -1
  126. package/node/wrappers/models/security/security.test.ts +80 -0
  127. package/node/wrappers/models/security/security.ts +56 -3
  128. package/node/wrappers/services/price-service/PriceService.d.ts +19 -0
  129. package/node/wrappers/services/price-service/PriceService.js +26 -0
  130. package/node/wrappers/services/price-service/PriceService.js.map +1 -1
  131. package/node/wrappers/services/price-service/PriceService.ts +29 -0
  132. package/node/wrappers/services/searchWithSecurities.test.js +125 -0
  133. package/node/wrappers/services/searchWithSecurities.test.js.map +1 -0
  134. package/node/wrappers/services/searchWithSecurities.test.ts +103 -0
  135. package/node/wrappers/services/transaction-service/TransactionService.d.ts +14 -0
  136. package/node/wrappers/services/transaction-service/TransactionService.js +25 -0
  137. package/node/wrappers/services/transaction-service/TransactionService.js.map +1 -1
  138. package/node/wrappers/services/transaction-service/TransactionService.ts +29 -0
  139. package/node/wrappers/util/link-resolver.d.ts +127 -0
  140. package/node/wrappers/util/link-resolver.js +378 -0
  141. package/node/wrappers/util/link-resolver.js.map +1 -0
  142. package/node/wrappers/util/link-resolver.test.d.ts +1 -0
  143. package/node/wrappers/util/link-resolver.test.js +349 -0
  144. package/node/wrappers/util/link-resolver.test.js.map +1 -0
  145. package/node/wrappers/util/link-resolver.test.ts +402 -0
  146. package/node/wrappers/util/link-resolver.ts +448 -0
  147. package/package.json +1 -1
  148. package/node/wrappers/services/security-service/SecurityService.searchByUuid.test.js +0 -38
  149. package/node/wrappers/services/security-service/SecurityService.searchByUuid.test.js.map +0 -1
  150. package/node/wrappers/services/security-service/SecurityService.searchByUuid.test.ts +0 -32
  151. /package/node/wrappers/services/{security-service/SecurityService.searchByUuid.test.d.ts → searchWithSecurities.test.d.ts} +0 -0
@@ -0,0 +1,448 @@
1
+ import { promisify } from 'util';
2
+
3
+ import { SecurityProto } from '../../fintekkers/models/security/security_pb';
4
+ import { PortfolioProto } from '../../fintekkers/models/portfolio/portfolio_pb';
5
+ import { UUIDProto } from '../../fintekkers/models/util/uuid_pb';
6
+ import { LocalTimestampProto } from '../../fintekkers/models/util/local_timestamp_pb';
7
+
8
+ import { SecurityClient } from '../../fintekkers/services/security-service/security_service_grpc_pb';
9
+ import { PortfolioClient } from '../../fintekkers/services/portfolio-service/portfolio_service_grpc_pb';
10
+ import { QuerySecurityRequestProto } from '../../fintekkers/requests/security/query_security_request_pb';
11
+ import { QuerySecurityResponseProto } from '../../fintekkers/requests/security/query_security_response_pb';
12
+ import { QueryPortfolioRequestProto } from '../../fintekkers/requests/portfolio/query_portfolio_request_pb';
13
+ import { QueryPortfolioResponseProto } from '../../fintekkers/requests/portfolio/query_portfolio_response_pb';
14
+
15
+ import Security from '../models/security/security';
16
+ import Portfolio from '../models/portfolio/portfolio';
17
+ import { UUID } from '../models/utils/uuid';
18
+ import EnvConfig from '../models/utils/requestcontext';
19
+
20
+ /**
21
+ * LinkResolver — bulk hydration of `is_link=true` entity references into
22
+ * full entities. Implements the consumer side of the `is_link` pattern
23
+ * documented in `docs/adr/is_link_pattern.md`.
24
+ *
25
+ * Two surface methods:
26
+ * - getSecurity(uuid) / getPortfolio(uuid): single-UUID resolution. Cached
27
+ * and concurrent-deduped.
28
+ * - resolveSecurities(items) / resolvePortfolios(items): bulk in-place
29
+ * mutation across a heterogeneous list of items that each have a
30
+ * proto-style getter+setter for the embedded entity. Collects unique
31
+ * link UUIDs, fires one batched GetByIds RPC, mutates each item's proto
32
+ * to swap the link sub-message for the resolved full entity (with
33
+ * is_link=false on the embedded copy).
34
+ *
35
+ * Caching:
36
+ * - Process-level LRU keyed on UUID string. Default 1000 entries, no TTL
37
+ * (entries live until evicted by LRU). Long-running services that need
38
+ * freshness should pass `{ ttlMs: <ms> }`. Tests can disable with
39
+ * `{ cacheSize: 0 }`.
40
+ * - Concurrent same-UUID requests are deduped via an in-flight promise
41
+ * map — N parallel callers for the same UUID share one RPC.
42
+ *
43
+ * RPC choice: uses `GetByIds` (unary, UUID-keyed bulk) per the ADR. The
44
+ * existing `SecurityService.search` (streaming) would also work but
45
+ * requires more wrapper plumbing for batched-by-UUID semantics.
46
+ *
47
+ * Mutation semantic: when bulk-resolving, the embedded sub-message is
48
+ * replaced (not the outer entity). Outer Price.proto.is_link is unchanged;
49
+ * only the inner SecurityProto is swapped from link-stub to full entity.
50
+ * Wrapper objects that read through the proto (`price.getSecurity()`)
51
+ * automatically see the resolved data.
52
+ *
53
+ * Time-travel (`as_of`) semantic: per is_link_pattern.md addendum, when
54
+ * a link sub-message has only `uuid` set the resolver fetches the latest
55
+ * version. When the link sub-message ALSO has `as_of` set, the resolver
56
+ * fetches the version of the entity as of that timestamp. The cache is
57
+ * keyed on (uuid, as_of) so the same UUID at different timestamps does
58
+ * not collide. Bulk lookups group by `as_of` (one GetByIds RPC per unique
59
+ * timestamp bucket, since the request proto carries a single as_of).
60
+ */
61
+ export interface LinkResolverOptions {
62
+ /** Optional API key. If omitted, EnvConfig.apiCredentials is used. */
63
+ apiKey?: string;
64
+ /** LRU max entries. Default 1000. Set to 0 to disable caching. */
65
+ cacheSize?: number;
66
+ /** Per-entry TTL in ms. Default undefined (no expiry). */
67
+ ttlMs?: number;
68
+ /**
69
+ * Test injection: clients to use instead of constructing real ones.
70
+ * Production callers should not set these.
71
+ */
72
+ securityClient?: SecurityClient;
73
+ portfolioClient?: PortfolioClient;
74
+ }
75
+
76
+ interface CacheEntry<V> {
77
+ value: V;
78
+ insertedAt: number;
79
+ }
80
+
81
+ /**
82
+ * Tiny LRU. Map keeps insertion order; on get-hit we delete + re-insert
83
+ * to bump to the end (most recently used). On overflow we drop the
84
+ * oldest entry (first key in the Map). Avoids pulling in lru-cache as a
85
+ * dependency for ~30 lines of logic.
86
+ */
87
+ class TinyLRU<V> {
88
+ private map = new Map<string, CacheEntry<V>>();
89
+ constructor(private maxSize: number, private ttlMs?: number) {}
90
+
91
+ get(key: string): V | undefined {
92
+ if (this.maxSize === 0) return undefined;
93
+ const entry = this.map.get(key);
94
+ if (!entry) return undefined;
95
+ if (this.ttlMs !== undefined && Date.now() - entry.insertedAt > this.ttlMs) {
96
+ this.map.delete(key);
97
+ return undefined;
98
+ }
99
+ // Bump to most-recently-used.
100
+ this.map.delete(key);
101
+ this.map.set(key, entry);
102
+ return entry.value;
103
+ }
104
+
105
+ set(key: string, value: V): void {
106
+ if (this.maxSize === 0) return;
107
+ if (this.map.has(key)) this.map.delete(key);
108
+ this.map.set(key, { value, insertedAt: Date.now() });
109
+ while (this.map.size > this.maxSize) {
110
+ const oldest = this.map.keys().next().value;
111
+ if (oldest === undefined) break;
112
+ this.map.delete(oldest);
113
+ }
114
+ }
115
+
116
+ size(): number {
117
+ return this.map.size;
118
+ }
119
+
120
+ clear(): void {
121
+ this.map.clear();
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Stable serialization of a LocalTimestampProto for use in cache keys
127
+ * and as_of-bucket grouping. Uses the proto's binary form (Uint8Array
128
+ * → base64). Returns the literal "latest" when as_of is undefined so
129
+ * unset and explicit-undefined collapse to the same bucket.
130
+ *
131
+ * Two LocalTimestampProto instances representing the same moment will
132
+ * produce the same key as long as the underlying nanos/seconds match —
133
+ * proto3 binary encoding is canonical for unset fields.
134
+ */
135
+ function asOfKey(asOf: LocalTimestampProto | undefined): string {
136
+ if (!asOf) return 'latest';
137
+ // serializeBinary returns Uint8Array.
138
+ const bytes = asOf.serializeBinary();
139
+ return Buffer.from(bytes).toString('base64');
140
+ }
141
+
142
+ class LinkResolver {
143
+ private securityClient: SecurityClient;
144
+ private portfolioClient: PortfolioClient;
145
+
146
+ private securityCache: TinyLRU<SecurityProto>;
147
+ private portfolioCache: TinyLRU<PortfolioProto>;
148
+
149
+ // Concurrent-call dedupe: a UUID currently being fetched maps to the
150
+ // promise the *first* caller is awaiting. Subsequent callers for the
151
+ // same UUID receive that same promise.
152
+ private securityInFlight = new Map<string, Promise<SecurityProto>>();
153
+ private portfolioInFlight = new Map<string, Promise<PortfolioProto>>();
154
+
155
+ constructor(opts: LinkResolverOptions = {}) {
156
+ const cacheSize = opts.cacheSize ?? 1000;
157
+ const ttlMs = opts.ttlMs;
158
+
159
+ this.securityCache = new TinyLRU(cacheSize, ttlMs);
160
+ this.portfolioCache = new TinyLRU(cacheSize, ttlMs);
161
+
162
+ if (opts.securityClient) {
163
+ this.securityClient = opts.securityClient;
164
+ } else if (opts.apiKey) {
165
+ const { credentials, interceptors } = EnvConfig.getAuthenticatedClientOptions(opts.apiKey);
166
+ this.securityClient = new SecurityClient(EnvConfig.apiURL, credentials, { interceptors });
167
+ } else {
168
+ this.securityClient = new SecurityClient(EnvConfig.apiURL, EnvConfig.apiCredentials);
169
+ }
170
+
171
+ if (opts.portfolioClient) {
172
+ this.portfolioClient = opts.portfolioClient;
173
+ } else if (opts.apiKey) {
174
+ const { credentials, interceptors } = EnvConfig.getAuthenticatedClientOptions(opts.apiKey);
175
+ this.portfolioClient = new PortfolioClient(EnvConfig.apiURL, credentials, { interceptors });
176
+ } else {
177
+ this.portfolioClient = new PortfolioClient(EnvConfig.apiURL, EnvConfig.apiCredentials);
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Resolve a single SecurityProto by UUID. If `asOf` is supplied, fetch
183
+ * the version of the entity as of that timestamp; otherwise fetch the
184
+ * latest. Cached + concurrent-deduped on the (uuid, asOf) pair.
185
+ * Throws if the server doesn't return the UUID (no silent null).
186
+ */
187
+ async getSecurity(uuid: UUID, asOf?: LocalTimestampProto): Promise<Security> {
188
+ const proto = await this.fetchSecurityProto(uuid, asOf);
189
+ return Security.create(proto);
190
+ }
191
+
192
+ /**
193
+ * Resolve a single PortfolioProto by UUID, optionally as of `asOf`.
194
+ * Cached + concurrent-deduped on (uuid, asOf).
195
+ */
196
+ async getPortfolio(uuid: UUID, asOf?: LocalTimestampProto): Promise<Portfolio> {
197
+ const proto = await this.fetchPortfolioProto(uuid, asOf);
198
+ return new Portfolio(proto);
199
+ }
200
+
201
+ /**
202
+ * Walk `items`, find the ones whose embedded security is `is_link=true`,
203
+ * batch-fetch the unique (uuid, as_of) pairs (grouped by as_of so each
204
+ * GetByIds RPC carries one timestamp), and mutate each item's proto in
205
+ * place so subsequent `item.getSecurity()` calls return the full entity.
206
+ * Returns the same array for chaining.
207
+ *
208
+ * Honors per-link `as_of`: if the embedded sub-message has `as_of` set,
209
+ * the resolver fetches the version of the entity at that timestamp,
210
+ * not the latest.
211
+ *
212
+ * `T` is structural: anything with a `proto` field that exposes
213
+ * `getSecurity()` / `setSecurity()` works (Price, Transaction, etc).
214
+ */
215
+ async resolveSecurities<T extends ResolvableSecurity>(items: T[]): Promise<T[]> {
216
+ if (items.length === 0) return items;
217
+
218
+ // Group: as_of bucket → (cacheKey → UUID) for items not yet cached.
219
+ const buckets = new Map<string, Map<string, UUID>>();
220
+ for (const item of items) {
221
+ const sec = item.proto.getSecurity();
222
+ if (!sec || !sec.getIsLink()) continue;
223
+ const uuidProto = sec.getUuid();
224
+ if (!uuidProto) continue;
225
+ const uuid = UUID.fromU8Array(uuidProto.getRawUuid_asU8());
226
+ const asOf = sec.getAsOf();
227
+ const bucketKey = asOfKey(asOf);
228
+ const cacheKey = `${uuid.toString()}@${bucketKey}`;
229
+ // Skip if already cached for this exact (uuid, as_of).
230
+ if (this.securityCache.get(cacheKey)) continue;
231
+ let bucket = buckets.get(bucketKey);
232
+ if (!bucket) {
233
+ bucket = new Map<string, UUID>();
234
+ buckets.set(bucketKey, bucket);
235
+ }
236
+ if (!bucket.has(cacheKey)) bucket.set(cacheKey, uuid);
237
+ }
238
+
239
+ // One GetByIds RPC per as_of bucket. Fire in parallel.
240
+ await Promise.all(
241
+ Array.from(buckets.entries()).map(async ([bucketKey, uuidMap]) => {
242
+ // Recover the LocalTimestampProto for this bucket from the first
243
+ // item whose serialized as_of matches. We could store it alongside
244
+ // but it's cheap to re-find.
245
+ const asOf = bucketKey === 'latest' ? undefined : findAsOfForBucket(items, (sec) => sec.getSecurity(), bucketKey);
246
+ const fetched = await this.batchFetchSecurities(Array.from(uuidMap.values()), asOf);
247
+ for (const proto of fetched) {
248
+ const uuidProto = proto.getUuid();
249
+ if (!uuidProto) continue;
250
+ const uuidStr = UUID.fromU8Array(uuidProto.getRawUuid_asU8()).toString();
251
+ this.securityCache.set(`${uuidStr}@${bucketKey}`, proto);
252
+ }
253
+ }),
254
+ );
255
+
256
+ // Mutate each item's embedded security in place.
257
+ for (const item of items) {
258
+ const sec = item.proto.getSecurity();
259
+ if (!sec || !sec.getIsLink()) continue;
260
+ const uuidProto = sec.getUuid();
261
+ if (!uuidProto) continue;
262
+ const uuidStr = UUID.fromU8Array(uuidProto.getRawUuid_asU8()).toString();
263
+ const bucketKey = asOfKey(sec.getAsOf());
264
+ const resolved = this.securityCache.get(`${uuidStr}@${bucketKey}`);
265
+ if (resolved) item.proto.setSecurity(resolved);
266
+ }
267
+
268
+ return items;
269
+ }
270
+
271
+ /**
272
+ * Same shape as resolveSecurities, but for embedded PortfolioProto.
273
+ * Honors per-link `as_of` the same way.
274
+ */
275
+ async resolvePortfolios<T extends ResolvablePortfolio>(items: T[]): Promise<T[]> {
276
+ if (items.length === 0) return items;
277
+
278
+ const buckets = new Map<string, Map<string, UUID>>();
279
+ for (const item of items) {
280
+ const port = item.proto.getPortfolio();
281
+ if (!port || !port.getIsLink()) continue;
282
+ const uuidProto = port.getUuid();
283
+ if (!uuidProto) continue;
284
+ const uuid = UUID.fromU8Array(uuidProto.getRawUuid_asU8());
285
+ const asOf = port.getAsOf();
286
+ const bucketKey = asOfKey(asOf);
287
+ const cacheKey = `${uuid.toString()}@${bucketKey}`;
288
+ if (this.portfolioCache.get(cacheKey)) continue;
289
+ let bucket = buckets.get(bucketKey);
290
+ if (!bucket) {
291
+ bucket = new Map<string, UUID>();
292
+ buckets.set(bucketKey, bucket);
293
+ }
294
+ if (!bucket.has(cacheKey)) bucket.set(cacheKey, uuid);
295
+ }
296
+
297
+ await Promise.all(
298
+ Array.from(buckets.entries()).map(async ([bucketKey, uuidMap]) => {
299
+ const asOf = bucketKey === 'latest' ? undefined : findAsOfForBucket(items, (it) => it.getPortfolio(), bucketKey);
300
+ const fetched = await this.batchFetchPortfolios(Array.from(uuidMap.values()), asOf);
301
+ for (const proto of fetched) {
302
+ const uuidProto = proto.getUuid();
303
+ if (!uuidProto) continue;
304
+ const uuidStr = UUID.fromU8Array(uuidProto.getRawUuid_asU8()).toString();
305
+ this.portfolioCache.set(`${uuidStr}@${bucketKey}`, proto);
306
+ }
307
+ }),
308
+ );
309
+
310
+ for (const item of items) {
311
+ const port = item.proto.getPortfolio();
312
+ if (!port || !port.getIsLink()) continue;
313
+ const uuidProto = port.getUuid();
314
+ if (!uuidProto) continue;
315
+ const uuidStr = UUID.fromU8Array(uuidProto.getRawUuid_asU8()).toString();
316
+ const bucketKey = asOfKey(port.getAsOf());
317
+ const resolved = this.portfolioCache.get(`${uuidStr}@${bucketKey}`);
318
+ if (resolved) item.proto.setPortfolio(resolved);
319
+ }
320
+
321
+ return items;
322
+ }
323
+
324
+ /** Test/debug helper. Not part of the stable API. */
325
+ clearCache(): void {
326
+ this.securityCache.clear();
327
+ this.portfolioCache.clear();
328
+ this.securityInFlight.clear();
329
+ this.portfolioInFlight.clear();
330
+ }
331
+
332
+ // ---------- internals ----------
333
+
334
+ private async fetchSecurityProto(uuid: UUID, asOf?: LocalTimestampProto): Promise<SecurityProto> {
335
+ const key = `${uuid.toString()}@${asOfKey(asOf)}`;
336
+
337
+ const cached = this.securityCache.get(key);
338
+ if (cached) return cached;
339
+
340
+ const inFlight = this.securityInFlight.get(key);
341
+ if (inFlight) return inFlight;
342
+
343
+ const promise = this.batchFetchSecurities([uuid], asOf).then((protos) => {
344
+ if (protos.length === 0) {
345
+ throw new Error(`Security not found: ${uuid.toString()}@${asOfKey(asOf)}`);
346
+ }
347
+ const proto = protos[0];
348
+ this.securityCache.set(key, proto);
349
+ return proto;
350
+ }).finally(() => {
351
+ this.securityInFlight.delete(key);
352
+ });
353
+
354
+ this.securityInFlight.set(key, promise);
355
+ return promise;
356
+ }
357
+
358
+ private async fetchPortfolioProto(uuid: UUID, asOf?: LocalTimestampProto): Promise<PortfolioProto> {
359
+ const key = `${uuid.toString()}@${asOfKey(asOf)}`;
360
+
361
+ const cached = this.portfolioCache.get(key);
362
+ if (cached) return cached;
363
+
364
+ const inFlight = this.portfolioInFlight.get(key);
365
+ if (inFlight) return inFlight;
366
+
367
+ const promise = this.batchFetchPortfolios([uuid], asOf).then((protos) => {
368
+ if (protos.length === 0) {
369
+ throw new Error(`Portfolio not found: ${uuid.toString()}@${asOfKey(asOf)}`);
370
+ }
371
+ const proto = protos[0];
372
+ this.portfolioCache.set(key, proto);
373
+ return proto;
374
+ }).finally(() => {
375
+ this.portfolioInFlight.delete(key);
376
+ });
377
+
378
+ this.portfolioInFlight.set(key, promise);
379
+ return promise;
380
+ }
381
+
382
+ private async batchFetchSecurities(uuids: UUID[], asOf?: LocalTimestampProto): Promise<SecurityProto[]> {
383
+ if (uuids.length === 0) return [];
384
+ const request = new QuerySecurityRequestProto();
385
+ request.setObjectClass('SecurityRequest');
386
+ request.setVersion('0.0.1');
387
+ const uuidProtos: UUIDProto[] = uuids.map((u) => u.toUUIDProto());
388
+ request.setUuidsList(uuidProtos);
389
+ if (asOf) request.setAsOf(asOf);
390
+
391
+ const getByIdsAsync = promisify(this.securityClient.getByIds.bind(this.securityClient));
392
+ const response = (await getByIdsAsync(request)) as QuerySecurityResponseProto;
393
+ return response.getSecurityResponseList();
394
+ }
395
+
396
+ private async batchFetchPortfolios(uuids: UUID[], asOf?: LocalTimestampProto): Promise<PortfolioProto[]> {
397
+ if (uuids.length === 0) return [];
398
+ const request = new QueryPortfolioRequestProto();
399
+ request.setObjectClass('PortfolioRequest');
400
+ request.setVersion('0.0.1');
401
+ const uuidProtos: UUIDProto[] = uuids.map((u) => u.toUUIDProto());
402
+ request.setUuidsList(uuidProtos);
403
+ if (asOf) request.setAsOf(asOf);
404
+
405
+ const getByIdsAsync = promisify(this.portfolioClient.getByIds.bind(this.portfolioClient));
406
+ const response = (await getByIdsAsync(request)) as QueryPortfolioResponseProto;
407
+ return response.getPortfolioResponseList();
408
+ }
409
+ }
410
+
411
+ /**
412
+ * Walk `items` and return the first sub-message's as_of whose serialized
413
+ * key matches `bucketKey`. Used by the bulk resolvers to recover the
414
+ * canonical LocalTimestampProto instance for a bucket.
415
+ */
416
+ function findAsOfForBucket<T extends { proto: any }>(
417
+ items: T[],
418
+ read: (proto: any) => SecurityProto | PortfolioProto | undefined,
419
+ bucketKey: string,
420
+ ): LocalTimestampProto | undefined {
421
+ for (const item of items) {
422
+ const sub = read(item.proto);
423
+ if (!sub || !sub.getIsLink()) continue;
424
+ const asOf = sub.getAsOf();
425
+ if (asOfKey(asOf) === bucketKey) return asOf;
426
+ }
427
+ return undefined;
428
+ }
429
+
430
+ /**
431
+ * Structural type — anything with a proto that has getSecurity/setSecurity
432
+ * (Price, Transaction, etc.) is resolvable.
433
+ */
434
+ export interface ResolvableSecurity {
435
+ proto: {
436
+ getSecurity(): SecurityProto | undefined;
437
+ setSecurity(s: SecurityProto): unknown;
438
+ };
439
+ }
440
+
441
+ export interface ResolvablePortfolio {
442
+ proto: {
443
+ getPortfolio(): PortfolioProto | undefined;
444
+ setPortfolio(p: PortfolioProto): unknown;
445
+ };
446
+ }
447
+
448
+ export default LinkResolver;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fintekkers/ledger-models",
3
3
  "todo": "Replace the version with build script version number",
4
- "version": "0.1.131",
4
+ "version": "0.1.133",
5
5
  "description": "ledger model protos ",
6
6
  "authors": [
7
7
  "David Doherty",
@@ -1,38 +0,0 @@
1
- "use strict";
2
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
- return new (P || (P = Promise))(function (resolve, reject) {
5
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
- step((generator = generator.apply(thisArg, _arguments || [])).next());
9
- });
10
- };
11
- Object.defineProperty(exports, "__esModule", { value: true });
12
- const SecurityService_1 = require("./SecurityService");
13
- const positionfilter_1 = require("../../models/position/positionfilter");
14
- const field_pb_1 = require("../../../fintekkers/models/position/field_pb");
15
- const identifier_1 = require("../../models/security/identifier");
16
- const identifier_pb_1 = require("../../../fintekkers/models/security/identifier/identifier_pb");
17
- const identifier_type_pb_1 = require("../../../fintekkers/models/security/identifier/identifier_type_pb");
18
- test('searchByUuid returns the security matching the given UUID', () => __awaiter(void 0, void 0, void 0, function* () {
19
- const service = new SecurityService_1.SecurityService();
20
- // First find a known security by CUSIP to get its UUID
21
- const filter = new positionfilter_1.PositionFilter();
22
- const identifierProto = new identifier_pb_1.IdentifierProto()
23
- .setIdentifierType(identifier_type_pb_1.IdentifierTypeProto.CUSIP)
24
- .setIdentifierValue('912810TM4');
25
- filter.addObjectFilter(field_pb_1.FieldProto.IDENTIFIER, new identifier_1.Identifier(identifierProto));
26
- const byIdentifier = yield service.searchSecurityAsOfNow(filter);
27
- if (byIdentifier.length === 0) {
28
- console.warn('No security found for test CUSIP — skipping UUID lookup assertion');
29
- return;
30
- }
31
- const original = byIdentifier[0];
32
- const uuidStr = original.getID().toString();
33
- // Now look it up by UUID
34
- const byUuid = yield service.searchByUuid(uuidStr);
35
- expect(byUuid.length).toBeGreaterThan(0);
36
- expect(byUuid[0].getID().toString()).toBe(uuidStr);
37
- }), 30000);
38
- //# sourceMappingURL=SecurityService.searchByUuid.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"SecurityService.searchByUuid.test.js","sourceRoot":"","sources":["SecurityService.searchByUuid.test.ts"],"names":[],"mappings":";;;;;;;;;;;AAAA,uDAAoD;AACpD,yEAAsE;AACtE,2EAA0E;AAC1E,iEAA8D;AAC9D,gGAA+F;AAC/F,0GAAwG;AAExG,IAAI,CAAC,2DAA2D,EAAE,GAAS,EAAE;IAC3E,MAAM,OAAO,GAAG,IAAI,iCAAe,EAAE,CAAC;IAEtC,uDAAuD;IACvD,MAAM,MAAM,GAAG,IAAI,+BAAc,EAAE,CAAC;IACpC,MAAM,eAAe,GAAG,IAAI,+BAAe,EAAE;SAC1C,iBAAiB,CAAC,wCAAmB,CAAC,KAAK,CAAC;SAC5C,kBAAkB,CAAC,WAAW,CAAC,CAAC;IACnC,MAAM,CAAC,eAAe,CAAC,qBAAU,CAAC,UAAU,EAAE,IAAI,uBAAU,CAAC,eAAe,CAAC,CAAC,CAAC;IAE/E,MAAM,YAAY,GAAG,MAAM,OAAO,CAAC,qBAAqB,CAAC,MAAM,CAAC,CAAC;IACjE,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE;QAC7B,OAAO,CAAC,IAAI,CAAC,mEAAmE,CAAC,CAAC;QAClF,OAAO;KACR;IAED,MAAM,QAAQ,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC;IACjC,MAAM,OAAO,GAAG,QAAQ,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,CAAC;IAE5C,yBAAyB;IACzB,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;IAEnD,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;IACzC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AACrD,CAAC,CAAA,EAAE,KAAK,CAAC,CAAC"}
@@ -1,32 +0,0 @@
1
- import { SecurityService } from './SecurityService';
2
- import { PositionFilter } from '../../models/position/positionfilter';
3
- import { FieldProto } from '../../../fintekkers/models/position/field_pb';
4
- import { Identifier } from '../../models/security/identifier';
5
- import { IdentifierProto } from '../../../fintekkers/models/security/identifier/identifier_pb';
6
- import { IdentifierTypeProto } from '../../../fintekkers/models/security/identifier/identifier_type_pb';
7
-
8
- test('searchByUuid returns the security matching the given UUID', async () => {
9
- const service = new SecurityService();
10
-
11
- // First find a known security by CUSIP to get its UUID
12
- const filter = new PositionFilter();
13
- const identifierProto = new IdentifierProto()
14
- .setIdentifierType(IdentifierTypeProto.CUSIP)
15
- .setIdentifierValue('912810TM4');
16
- filter.addObjectFilter(FieldProto.IDENTIFIER, new Identifier(identifierProto));
17
-
18
- const byIdentifier = await service.searchSecurityAsOfNow(filter);
19
- if (byIdentifier.length === 0) {
20
- console.warn('No security found for test CUSIP — skipping UUID lookup assertion');
21
- return;
22
- }
23
-
24
- const original = byIdentifier[0];
25
- const uuidStr = original.getID().toString();
26
-
27
- // Now look it up by UUID
28
- const byUuid = await service.searchByUuid(uuidStr);
29
-
30
- expect(byUuid.length).toBeGreaterThan(0);
31
- expect(byUuid[0].getID().toString()).toBe(uuidStr);
32
- }, 30000);