@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.
- package/node/fintekkers/models/portfolio/portfolio_pb.js +19 -13
- package/node/fintekkers/models/position/field_pb.js +7 -1
- package/node/fintekkers/models/position/measure_pb.js +7 -1
- package/node/fintekkers/models/position/position_filter_pb.js +13 -7
- package/node/fintekkers/models/position/position_pb.js +17 -11
- package/node/fintekkers/models/position/position_status_pb.js +7 -1
- package/node/fintekkers/models/position/position_util_pb.js +17 -11
- package/node/fintekkers/models/price/price_pb.js +20 -14
- package/node/fintekkers/models/price/price_type_pb.js +7 -1
- package/node/fintekkers/models/security/bond/auction_type_pb.js +7 -1
- package/node/fintekkers/models/security/bond/issuance_pb.js +23 -17
- package/node/fintekkers/models/security/coupon_frequency_pb.js +7 -1
- package/node/fintekkers/models/security/coupon_type_pb.js +7 -1
- package/node/fintekkers/models/security/identifier/identifier_pb.js +15 -9
- package/node/fintekkers/models/security/identifier/identifier_type_pb.js +7 -1
- package/node/fintekkers/models/security/index/index_type_pb.js +7 -1
- package/node/fintekkers/models/security/index_composition_grpc_pb.js +1 -0
- package/node/fintekkers/models/security/index_composition_pb.js +29 -23
- package/node/fintekkers/models/security/security_pb.d.ts +6 -0
- package/node/fintekkers/models/security/security_pb.js +147 -90
- package/node/fintekkers/models/security/security_quantity_type_pb.js +7 -1
- package/node/fintekkers/models/security/security_type_pb.js +7 -1
- package/node/fintekkers/models/security/tenor_pb.js +15 -9
- package/node/fintekkers/models/security/tenor_type_pb.js +7 -1
- package/node/fintekkers/models/strategy/strategy_allocation_pb.js +19 -13
- package/node/fintekkers/models/strategy/strategy_pb.js +20 -14
- package/node/fintekkers/models/transaction/transaction_pb.js +30 -24
- package/node/fintekkers/models/transaction/transaction_type_pb.js +7 -1
- package/node/fintekkers/models/util/api/api_key_pb.js +16 -10
- package/node/fintekkers/models/util/currency_grpc_pb.js +1 -0
- package/node/fintekkers/models/util/currency_pb.js +10 -4
- package/node/fintekkers/models/util/date_range_pb.js +14 -8
- package/node/fintekkers/models/util/decimal_value_pb.js +10 -4
- package/node/fintekkers/models/util/endpoint_pb.js +13 -7
- package/node/fintekkers/models/util/local_date_pb.js +11 -5
- package/node/fintekkers/models/util/local_timestamp_pb.js +11 -5
- package/node/fintekkers/models/util/lock/node_partition_pb.js +15 -9
- package/node/fintekkers/models/util/lock/node_state_pb.js +16 -10
- package/node/fintekkers/models/util/uuid_pb.js +9 -3
- package/node/fintekkers/models/valuation/cashflow_grpc_pb.js +1 -0
- package/node/fintekkers/models/valuation/cashflow_pb.js +13 -7
- package/node/fintekkers/requests/index_composition/create_index_composition_request_grpc_pb.js +1 -0
- package/node/fintekkers/requests/index_composition/create_index_composition_request_pb.js +22 -16
- package/node/fintekkers/requests/index_composition/get_index_composition_request_grpc_pb.js +1 -0
- package/node/fintekkers/requests/index_composition/get_index_composition_request_pb.js +22 -16
- package/node/fintekkers/requests/portfolio/create_portfolio_request_pb.js +13 -7
- package/node/fintekkers/requests/portfolio/create_portfolio_response_pb.js +14 -8
- package/node/fintekkers/requests/portfolio/query_portfolio_request_pb.js +17 -11
- package/node/fintekkers/requests/portfolio/query_portfolio_response_pb.js +14 -8
- package/node/fintekkers/requests/position/query_position_request_pb.js +27 -15
- package/node/fintekkers/requests/position/query_position_response_pb.js +16 -10
- package/node/fintekkers/requests/price/create_price_request_pb.js +13 -7
- package/node/fintekkers/requests/price/create_price_response_pb.js +14 -8
- package/node/fintekkers/requests/price/query_price_request_pb.d.ts +3 -0
- package/node/fintekkers/requests/price/query_price_request_pb.js +48 -12
- package/node/fintekkers/requests/price/query_price_response_pb.d.ts +7 -0
- package/node/fintekkers/requests/price/query_price_response_pb.js +68 -9
- package/node/fintekkers/requests/security/create_security_request_pb.js +13 -7
- package/node/fintekkers/requests/security/create_security_response_pb.js +15 -9
- package/node/fintekkers/requests/security/get_field_values_request_pb.js +13 -7
- package/node/fintekkers/requests/security/get_field_values_response_pb.js +13 -7
- package/node/fintekkers/requests/security/get_fields_response_pb.js +17 -8
- package/node/fintekkers/requests/security/query_security_request_pb.js +17 -11
- package/node/fintekkers/requests/security/query_security_response_pb.js +15 -9
- package/node/fintekkers/requests/transaction/create_transaction_request_pb.js +13 -7
- package/node/fintekkers/requests/transaction/create_transaction_response_pb.js +14 -8
- package/node/fintekkers/requests/transaction/query_transaction_request_pb.js +16 -10
- package/node/fintekkers/requests/transaction/query_transaction_response_pb.js +15 -9
- package/node/fintekkers/requests/util/delete_request_grpc_pb.js +1 -0
- package/node/fintekkers/requests/util/delete_request_pb.js +34 -28
- package/node/fintekkers/requests/util/errors/error_pb.js +13 -7
- package/node/fintekkers/requests/util/errors/message_pb.js +12 -6
- package/node/fintekkers/requests/util/errors/summary_pb.js +10 -4
- package/node/fintekkers/requests/util/lock/lock_request_pb.js +14 -8
- package/node/fintekkers/requests/util/lock/lock_response_pb.js +15 -9
- package/node/fintekkers/requests/util/operation_pb.js +7 -1
- package/node/fintekkers/requests/valuation/curve_request_grpc_pb.js +1 -0
- package/node/fintekkers/requests/valuation/curve_request_pb.d.ts +12 -0
- package/node/fintekkers/requests/valuation/curve_request_pb.js +125 -14
- package/node/fintekkers/requests/valuation/curve_response_grpc_pb.js +1 -0
- package/node/fintekkers/requests/valuation/curve_response_pb.js +21 -15
- package/node/fintekkers/requests/valuation/product_inputs.test.d.ts +6 -0
- package/node/fintekkers/requests/valuation/product_inputs.test.js +146 -0
- package/node/fintekkers/requests/valuation/product_inputs.test.js.map +1 -0
- package/node/fintekkers/requests/valuation/product_inputs_grpc_pb.js +1 -0
- package/node/fintekkers/requests/valuation/product_inputs_pb.d.ts +42 -0
- package/node/fintekkers/requests/valuation/product_inputs_pb.js +360 -27
- package/node/fintekkers/requests/valuation/valuation_request_pb.js +25 -16
- package/node/fintekkers/requests/valuation/valuation_response_pb.js +16 -10
- package/node/fintekkers/services/index-composition-service/index_composition_service_grpc_pb.js +14 -14
- package/node/fintekkers/services/index-composition-service/index_composition_service_pb.js +7 -1
- package/node/fintekkers/services/lock-service/lock_service_grpc_pb.js +23 -23
- package/node/fintekkers/services/lock-service/lock_service_pb.js +21 -15
- package/node/fintekkers/services/portfolio-service/portfolio_service_grpc_pb.js +8 -8
- package/node/fintekkers/services/portfolio-service/portfolio_service_pb.js +7 -1
- package/node/fintekkers/services/position-service/position_service_grpc_pb.js +10 -10
- package/node/fintekkers/services/position-service/position_service_pb.js +7 -1
- package/node/fintekkers/services/price-service/price_service_grpc_pb.js +6 -6
- package/node/fintekkers/services/price-service/price_service_pb.js +7 -1
- package/node/fintekkers/services/security-service/security_service_grpc_pb.js +12 -12
- package/node/fintekkers/services/security-service/security_service_pb.js +7 -1
- package/node/fintekkers/services/transaction-service/transaction_service_grpc_pb.js +11 -11
- package/node/fintekkers/services/transaction-service/transaction_service_pb.js +7 -1
- package/node/fintekkers/services/valuation-service/valuation_service_grpc_pb.js +5 -5
- package/node/fintekkers/services/valuation-service/valuation_service_pb.js +7 -1
- package/node/wrappers/models/price/Price.d.ts +5 -0
- package/node/wrappers/models/price/Price.js +7 -0
- package/node/wrappers/models/price/Price.js.map +1 -1
- package/node/wrappers/models/price/Price.ts +8 -0
- package/node/wrappers/models/security/BondSecurity.d.ts +8 -0
- package/node/wrappers/models/security/BondSecurity.js +13 -0
- package/node/wrappers/models/security/BondSecurity.js.map +1 -1
- package/node/wrappers/models/security/BondSecurity.ts +13 -0
- package/node/wrappers/models/security/identifier.d.ts +26 -0
- package/node/wrappers/models/security/identifier.js +39 -0
- package/node/wrappers/models/security/identifier.js.map +1 -1
- package/node/wrappers/models/security/identifier.test.js +62 -0
- package/node/wrappers/models/security/identifier.test.js.map +1 -1
- package/node/wrappers/models/security/identifier.test.ts +70 -0
- package/node/wrappers/models/security/identifier.ts +44 -0
- package/node/wrappers/models/security/security.d.ts +42 -1
- package/node/wrappers/models/security/security.js +53 -2
- package/node/wrappers/models/security/security.js.map +1 -1
- package/node/wrappers/models/security/security.test.js +72 -0
- package/node/wrappers/models/security/security.test.js.map +1 -1
- package/node/wrappers/models/security/security.test.ts +80 -0
- package/node/wrappers/models/security/security.ts +56 -3
- package/node/wrappers/services/price-service/PriceService.d.ts +19 -0
- package/node/wrappers/services/price-service/PriceService.js +26 -0
- package/node/wrappers/services/price-service/PriceService.js.map +1 -1
- package/node/wrappers/services/price-service/PriceService.ts +29 -0
- package/node/wrappers/services/searchWithSecurities.test.js +125 -0
- package/node/wrappers/services/searchWithSecurities.test.js.map +1 -0
- package/node/wrappers/services/searchWithSecurities.test.ts +103 -0
- package/node/wrappers/services/transaction-service/TransactionService.d.ts +14 -0
- package/node/wrappers/services/transaction-service/TransactionService.js +25 -0
- package/node/wrappers/services/transaction-service/TransactionService.js.map +1 -1
- package/node/wrappers/services/transaction-service/TransactionService.ts +29 -0
- package/node/wrappers/util/link-resolver.d.ts +127 -0
- package/node/wrappers/util/link-resolver.js +378 -0
- package/node/wrappers/util/link-resolver.js.map +1 -0
- package/node/wrappers/util/link-resolver.test.d.ts +1 -0
- package/node/wrappers/util/link-resolver.test.js +349 -0
- package/node/wrappers/util/link-resolver.test.js.map +1 -0
- package/node/wrappers/util/link-resolver.test.ts +402 -0
- package/node/wrappers/util/link-resolver.ts +448 -0
- package/package.json +1 -1
- package/node/wrappers/services/security-service/SecurityService.searchByUuid.test.js +0 -38
- package/node/wrappers/services/security-service/SecurityService.searchByUuid.test.js.map +0 -1
- package/node/wrappers/services/security-service/SecurityService.searchByUuid.test.ts +0 -32
- /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,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);
|