@ordius/adonisjs-currencyx 1.4.9
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/LICENSE.md +9 -0
- package/README.md +508 -0
- package/build/configure.d.ts +2 -0
- package/build/configure.js +50 -0
- package/build/index.d.ts +5 -0
- package/build/index.js +13 -0
- package/build/providers/currency_provider.d.ts +34 -0
- package/build/providers/currency_provider.js +46 -0
- package/build/services/main.d.ts +19 -0
- package/build/services/main.js +29 -0
- package/build/src/define_config.d.ts +31 -0
- package/build/src/define_config.js +56 -0
- package/build/src/exchanges/database.d.ts +54 -0
- package/build/src/exchanges/database.js +361 -0
- package/build/src/symbols.d.ts +5 -0
- package/build/src/symbols.js +5 -0
- package/build/src/types.d.ts +111 -0
- package/build/src/types.js +1 -0
- package/build/stubs/config/currency.stub +97 -0
- package/build/stubs/main.d.ts +5 -0
- package/build/stubs/main.js +7 -0
- package/build/stubs/migrations/create_currencies_table.stub +41 -0
- package/build/stubs/models/currency.stub +111 -0
- package/package.json +146 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @ordius/adonisjs-currencyx
|
|
3
|
+
*
|
|
4
|
+
* (c) Mixxtor
|
|
5
|
+
*
|
|
6
|
+
* For the full copyright and license information, please view the LICENSE
|
|
7
|
+
* file that was distributed with this source code.
|
|
8
|
+
*/
|
|
9
|
+
import app from '@adonisjs/core/services/app';
|
|
10
|
+
/**
|
|
11
|
+
* Currency service with full type inference
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* ```ts
|
|
15
|
+
* import currency from '@ordius/adonisjs-currencyx/services/currency'
|
|
16
|
+
*
|
|
17
|
+
* // Direct usage - no type casting needed
|
|
18
|
+
* const rates = await currency.latestRates()
|
|
19
|
+
*
|
|
20
|
+
* // Provider switching with type inference
|
|
21
|
+
* const googleProvider = currency.use('google') // Only configured providers
|
|
22
|
+
* const rates = await googleProvider.latestRates()
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
let currency;
|
|
26
|
+
await app.booted(async () => {
|
|
27
|
+
currency = await app.container.make('currency.manager');
|
|
28
|
+
});
|
|
29
|
+
export { currency as default };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { DatabaseConfig, ServiceConfigProvider, ExchangeFactory } from './types.js';
|
|
2
|
+
import { DatabaseExchange } from './exchanges/database.js';
|
|
3
|
+
import type { ConfigProvider } from '@adonisjs/core/types';
|
|
4
|
+
/**
|
|
5
|
+
* Define database exchange provider configuration
|
|
6
|
+
* Returns a factory function to avoid eager instantiation
|
|
7
|
+
*/
|
|
8
|
+
declare function database(config: DatabaseConfig): DatabaseExchange;
|
|
9
|
+
/**
|
|
10
|
+
* Exchange configuration helpers
|
|
11
|
+
*/
|
|
12
|
+
export declare const exchanges: {
|
|
13
|
+
readonly database: typeof database;
|
|
14
|
+
readonly google: (config?: import("@mixxtor/currencyx-js").GoogleFinanceConfig) => import("@mixxtor/currencyx-js").GoogleFinanceExchange;
|
|
15
|
+
readonly fixer: (config: import("@mixxtor/currencyx-js").FixerConfig) => import("@mixxtor/currencyx-js").FixerExchange;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Helper to remap known exchange exchanges to factory functions
|
|
19
|
+
*/
|
|
20
|
+
type ResolvedConfig<Exchanges extends Record<string, ExchangeFactory>> = {
|
|
21
|
+
default: keyof Exchanges;
|
|
22
|
+
exchanges: {
|
|
23
|
+
[K in keyof Exchanges]: Exchanges[K] extends ServiceConfigProvider<infer A> ? A : Exchanges[K];
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Define currency configuration with type inference
|
|
28
|
+
* Following AdonisJS pattern for better type safety
|
|
29
|
+
*/
|
|
30
|
+
export declare function defineConfig<Exchanges extends Record<string, any>>(config: ResolvedConfig<Exchanges>): ConfigProvider<ResolvedConfig<Exchanges>>;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { exchanges as currencyExchanges } from '@mixxtor/currencyx-js';
|
|
2
|
+
import { DatabaseExchange } from './exchanges/database.js';
|
|
3
|
+
import { configProvider } from '@adonisjs/core';
|
|
4
|
+
/**
|
|
5
|
+
* Define database exchange provider configuration
|
|
6
|
+
* Returns a factory function to avoid eager instantiation
|
|
7
|
+
*/
|
|
8
|
+
function database(config) {
|
|
9
|
+
if (!config.model) {
|
|
10
|
+
throw new Error('Database exchange requires a model');
|
|
11
|
+
}
|
|
12
|
+
const dbConfig = {
|
|
13
|
+
model: config.model,
|
|
14
|
+
base: config.base || 'USD',
|
|
15
|
+
columns: {
|
|
16
|
+
code: 'code',
|
|
17
|
+
rate: 'exchange_rate',
|
|
18
|
+
...config.columns,
|
|
19
|
+
},
|
|
20
|
+
cache: config.cache,
|
|
21
|
+
};
|
|
22
|
+
return new DatabaseExchange(dbConfig);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Exchange configuration helpers
|
|
26
|
+
*/
|
|
27
|
+
export const exchanges = {
|
|
28
|
+
...currencyExchanges,
|
|
29
|
+
database,
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Define currency configuration with type inference
|
|
33
|
+
* Following AdonisJS pattern for better type safety
|
|
34
|
+
*/
|
|
35
|
+
export function defineConfig(config) {
|
|
36
|
+
return configProvider.create(async (_app) => {
|
|
37
|
+
const { exchanges: exchangesFactory, default: defaultExchange } = config;
|
|
38
|
+
const exchangesNames = Object.keys(exchangesFactory);
|
|
39
|
+
/**
|
|
40
|
+
* Configured exchanges
|
|
41
|
+
*/
|
|
42
|
+
const exchangeExchanges = {};
|
|
43
|
+
/**
|
|
44
|
+
* Looping over providers and resolving their config providers
|
|
45
|
+
* to get factory functions
|
|
46
|
+
*/
|
|
47
|
+
for (let providerName of exchangesNames) {
|
|
48
|
+
const exchange = exchangesFactory[providerName];
|
|
49
|
+
exchangeExchanges[providerName] = exchange;
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
default: defaultExchange,
|
|
53
|
+
exchanges: exchangeExchanges,
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { CurrencyCode, ConversionResult, ExchangeRatesResult, ConvertParams, ExchangeRatesParams } from '@mixxtor/currencyx-js';
|
|
2
|
+
import { BaseCurrencyExchange } from '@mixxtor/currencyx-js';
|
|
3
|
+
import type { DatabaseConfig } from '../types.js';
|
|
4
|
+
import type { CacheService } from '@adonisjs/cache/types';
|
|
5
|
+
import { PROVIDER_CURRENCY_MODEL } from '../symbols.js';
|
|
6
|
+
import type { LucidModel } from '@adonisjs/lucid/types/model';
|
|
7
|
+
export declare class DatabaseExchange<Model extends LucidModel = LucidModel> extends BaseCurrencyExchange {
|
|
8
|
+
#private;
|
|
9
|
+
[PROVIDER_CURRENCY_MODEL]: InstanceType<Model>;
|
|
10
|
+
readonly name = "database";
|
|
11
|
+
protected model?: Model;
|
|
12
|
+
private columns;
|
|
13
|
+
private configModel?;
|
|
14
|
+
private cache?;
|
|
15
|
+
private cacheSetupPromise?;
|
|
16
|
+
private config;
|
|
17
|
+
constructor(config: DatabaseConfig<Model>);
|
|
18
|
+
/**
|
|
19
|
+
* Imports the model from the provider, returns and caches it
|
|
20
|
+
* for further operations.
|
|
21
|
+
*/
|
|
22
|
+
protected getModel(): Promise<Model>;
|
|
23
|
+
/**
|
|
24
|
+
* Imports the cache service from the provider, returns and caches it
|
|
25
|
+
* for further operations.
|
|
26
|
+
*/
|
|
27
|
+
protected getCacheService(): Promise<CacheService>;
|
|
28
|
+
/**
|
|
29
|
+
* Convert currency using database rates
|
|
30
|
+
*/
|
|
31
|
+
convert(params: ConvertParams): Promise<ConversionResult>;
|
|
32
|
+
/**
|
|
33
|
+
* Get latest rates (required abstract method)
|
|
34
|
+
*/
|
|
35
|
+
latestRates(params?: ExchangeRatesParams & {
|
|
36
|
+
cache?: boolean;
|
|
37
|
+
}): Promise<ExchangeRatesResult>;
|
|
38
|
+
/**
|
|
39
|
+
* Clear the currency cache
|
|
40
|
+
*/
|
|
41
|
+
clearCache(): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Refresh currency data from database
|
|
44
|
+
*/
|
|
45
|
+
refreshCurrencyData(): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Get convert rate (required abstract method)
|
|
48
|
+
*/
|
|
49
|
+
getConvertRate(from: CurrencyCode, to: CurrencyCode): Promise<number | undefined>;
|
|
50
|
+
/**
|
|
51
|
+
* Cleanup method for graceful shutdown
|
|
52
|
+
*/
|
|
53
|
+
cleanup(): Promise<void>;
|
|
54
|
+
}
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { BaseCurrencyExchange } from '@mixxtor/currencyx-js';
|
|
2
|
+
export class DatabaseExchange extends BaseCurrencyExchange {
|
|
3
|
+
name = 'database';
|
|
4
|
+
model;
|
|
5
|
+
columns;
|
|
6
|
+
configModel;
|
|
7
|
+
cache;
|
|
8
|
+
cacheSetupPromise;
|
|
9
|
+
config;
|
|
10
|
+
#defaultCacheTTL = '1h'; // in milliseconds or human-readable string (e.g., '1d')
|
|
11
|
+
#defaultCacheKeyPrefix = 'currency';
|
|
12
|
+
constructor(config) {
|
|
13
|
+
super();
|
|
14
|
+
this.config = config;
|
|
15
|
+
this.columns = {
|
|
16
|
+
code: config.columns?.code || 'code',
|
|
17
|
+
rate: config.columns?.rate || 'exchange_rate',
|
|
18
|
+
created_at: config.columns?.created_at || 'created_at',
|
|
19
|
+
updated_at: config.columns?.updated_at || 'updated_at',
|
|
20
|
+
...config.columns,
|
|
21
|
+
};
|
|
22
|
+
this.base = config.base || 'USD';
|
|
23
|
+
this.configModel = config.model;
|
|
24
|
+
// Validate configuration
|
|
25
|
+
this.#validateConfig();
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Validate configuration to prevent runtime errors
|
|
29
|
+
*/
|
|
30
|
+
#validateConfig() {
|
|
31
|
+
if (!this.configModel) {
|
|
32
|
+
throw new Error('Currency model configuration is required');
|
|
33
|
+
}
|
|
34
|
+
const cacheConfig = this.config.cache;
|
|
35
|
+
if (cacheConfig !== false && cacheConfig && !cacheConfig.service) {
|
|
36
|
+
throw new Error('Cache service configuration is required when cache is enabled');
|
|
37
|
+
}
|
|
38
|
+
// Validate base currency format
|
|
39
|
+
if (this.base && !/^[A-Z]{3}$/.test(this.base)) {
|
|
40
|
+
console.warn(`Base currency '${this.base}' should be a 3-letter ISO currency code`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Imports the model from the provider, returns and caches it
|
|
45
|
+
* for further operations.
|
|
46
|
+
*/
|
|
47
|
+
async getModel() {
|
|
48
|
+
if (!this.configModel) {
|
|
49
|
+
throw new Error('Currency model not configured');
|
|
50
|
+
}
|
|
51
|
+
if (this.model && !('hot' in import.meta)) {
|
|
52
|
+
return this.model;
|
|
53
|
+
}
|
|
54
|
+
const importedModel = await this.configModel();
|
|
55
|
+
this.model = 'default' in importedModel ? importedModel.default : importedModel;
|
|
56
|
+
return this.model;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Imports the cache service from the provider, returns and caches it
|
|
60
|
+
* for further operations.
|
|
61
|
+
*/
|
|
62
|
+
async getCacheService() {
|
|
63
|
+
const cacheConfig = this.config.cache;
|
|
64
|
+
if (cacheConfig === false || !cacheConfig || !cacheConfig.service) {
|
|
65
|
+
throw new Error('Currency cache not configured');
|
|
66
|
+
}
|
|
67
|
+
if (this.cache && !('hot' in import.meta)) {
|
|
68
|
+
return this.cache;
|
|
69
|
+
}
|
|
70
|
+
const importedCache = await cacheConfig.service();
|
|
71
|
+
this.cache = 'default' in importedCache ? importedCache.default : importedCache;
|
|
72
|
+
return this.cache;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Setup cache based on configuration (lazy initialization)
|
|
76
|
+
*/
|
|
77
|
+
async #ensureCacheSetup() {
|
|
78
|
+
if (this.cacheSetupPromise) {
|
|
79
|
+
return this.cacheSetupPromise;
|
|
80
|
+
}
|
|
81
|
+
this.cacheSetupPromise = this.#setupCache().catch((error) => {
|
|
82
|
+
// Reset the promise on error so it can be retried
|
|
83
|
+
this.cacheSetupPromise = undefined;
|
|
84
|
+
throw error;
|
|
85
|
+
});
|
|
86
|
+
return this.cacheSetupPromise;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Setup cache based on configuration
|
|
90
|
+
*/
|
|
91
|
+
async #setupCache() {
|
|
92
|
+
const cacheConfig = this.config.cache;
|
|
93
|
+
if (cacheConfig === false || !cacheConfig) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
this.cache = await this.getCacheService();
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
console.warn('Cache setup failed, continuing without cache:', error.message);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Convert currency using database rates
|
|
105
|
+
*/
|
|
106
|
+
async convert(params) {
|
|
107
|
+
// Input validation
|
|
108
|
+
const { amount, from, to } = params;
|
|
109
|
+
if (!amount || amount <= 0) {
|
|
110
|
+
return {
|
|
111
|
+
success: false,
|
|
112
|
+
query: { from, to, amount },
|
|
113
|
+
info: { timestamp: Date.now() },
|
|
114
|
+
date: new Date().toISOString(),
|
|
115
|
+
error: { info: 'Invalid amount: must be greater than 0' },
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (!from || !to) {
|
|
119
|
+
return {
|
|
120
|
+
success: false,
|
|
121
|
+
query: { from, to, amount },
|
|
122
|
+
info: { timestamp: Date.now() },
|
|
123
|
+
date: new Date().toISOString(),
|
|
124
|
+
error: { info: 'Invalid currency codes: from and to are required' },
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
const result = {
|
|
128
|
+
success: false,
|
|
129
|
+
query: { from, to, amount },
|
|
130
|
+
info: { timestamp: Date.now(), rate: 1 },
|
|
131
|
+
date: new Date().toISOString(),
|
|
132
|
+
result: amount,
|
|
133
|
+
};
|
|
134
|
+
// Same currency conversion
|
|
135
|
+
if (from === to) {
|
|
136
|
+
result.success = true;
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
const currencies = await this.#getCurrenciesByCodes([from, to]);
|
|
141
|
+
const fromCurrency = currencies?.find((c) => this.#getCurrencyCode(c) === from);
|
|
142
|
+
const toCurrency = currencies?.find((c) => this.#getCurrencyCode(c) === to);
|
|
143
|
+
if (!fromCurrency || !toCurrency) {
|
|
144
|
+
return {
|
|
145
|
+
...result,
|
|
146
|
+
error: {
|
|
147
|
+
info: `Currency not found: ${!fromCurrency ? from : to}`,
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
const fromRate = this.#getCurrencyRate(fromCurrency);
|
|
152
|
+
const toRate = this.#getCurrencyRate(toCurrency);
|
|
153
|
+
const updatedAt = this.#getCurrencyUpdatedAt(fromCurrency) || this.#getCurrencyUpdatedAt(toCurrency);
|
|
154
|
+
if (!fromRate || !toRate) {
|
|
155
|
+
return {
|
|
156
|
+
...result,
|
|
157
|
+
error: {
|
|
158
|
+
info: 'Invalid exchange rates found in database',
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
// Conversion formula: amount * (1/fromCurrencyRate) * toCurrencyRate
|
|
163
|
+
const convertRate = (1 / fromRate) * toRate;
|
|
164
|
+
const convertAmount = amount * convertRate;
|
|
165
|
+
result.success = true;
|
|
166
|
+
result.info.rate = convertRate;
|
|
167
|
+
result.result = convertAmount;
|
|
168
|
+
if (updatedAt) {
|
|
169
|
+
const timestamp = new Date(updatedAt).getTime();
|
|
170
|
+
result.info.timestamp = timestamp;
|
|
171
|
+
result.date = new Date(updatedAt).toISOString();
|
|
172
|
+
}
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown database error';
|
|
177
|
+
return {
|
|
178
|
+
success: false,
|
|
179
|
+
query: { from, to, amount },
|
|
180
|
+
info: { timestamp: Date.now() },
|
|
181
|
+
date: new Date().toISOString(),
|
|
182
|
+
error: {
|
|
183
|
+
info: errorMessage,
|
|
184
|
+
type: 'database_error',
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async #currencyList(useCache = true) {
|
|
190
|
+
// Ensure cache is setup before using it
|
|
191
|
+
await this.#ensureCacheSetup();
|
|
192
|
+
const Model = await this.getModel();
|
|
193
|
+
const query = Model.query().select(Object.values(this.columns));
|
|
194
|
+
if (!useCache || !this.cache || !this.config.cache) {
|
|
195
|
+
return await query;
|
|
196
|
+
}
|
|
197
|
+
const { prefix = this.#defaultCacheKeyPrefix, ttl = this.#defaultCacheTTL } = this.config.cache;
|
|
198
|
+
return await this.cache.getOrSet({ key: prefix, factory: () => query, ttl });
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Get specific currencies by codes (optimized for targeted queries)
|
|
202
|
+
*/
|
|
203
|
+
async #getCurrenciesByCodes(codes, useCache = true) {
|
|
204
|
+
if (!codes || codes.length === 0) {
|
|
205
|
+
return this.#currencyList(useCache);
|
|
206
|
+
}
|
|
207
|
+
// Ensure cache is setup before using it
|
|
208
|
+
await this.#ensureCacheSetup();
|
|
209
|
+
const Model = await this.getModel();
|
|
210
|
+
const query = Model.query()
|
|
211
|
+
.select(Object.values(this.columns))
|
|
212
|
+
.whereIn(this.columns.code, codes);
|
|
213
|
+
if (!useCache || !this.cache || !this.config.cache) {
|
|
214
|
+
return await query;
|
|
215
|
+
}
|
|
216
|
+
const { prefix = this.#defaultCacheKeyPrefix, ttl = this.#defaultCacheTTL } = this.config.cache;
|
|
217
|
+
const cacheKey = `${prefix}_${codes.sort().join('_')}`;
|
|
218
|
+
return await this.cache.getOrSet({ key: cacheKey, factory: () => query, ttl });
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Helper method to get currency code from a record
|
|
222
|
+
*/
|
|
223
|
+
#getCurrencyCode(record) {
|
|
224
|
+
return record[this.columns.code];
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Helper method to get currency rate from a record
|
|
228
|
+
*/
|
|
229
|
+
#getCurrencyRate(record) {
|
|
230
|
+
return record[this.columns.rate];
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Helper method to get currency updated at from a record
|
|
234
|
+
*/
|
|
235
|
+
#getCurrencyUpdatedAt(record) {
|
|
236
|
+
const updatedAtColumn = this.columns.updated_at || 'updated_at';
|
|
237
|
+
return record[updatedAtColumn];
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Get latest rates (required abstract method)
|
|
241
|
+
*/
|
|
242
|
+
async latestRates(params) {
|
|
243
|
+
const { base = this.base, codes: currencyCodes, cache = true } = params || {};
|
|
244
|
+
const result = {
|
|
245
|
+
success: false,
|
|
246
|
+
timestamp: new Date().getTime(),
|
|
247
|
+
date: new Date().toISOString(),
|
|
248
|
+
base: base,
|
|
249
|
+
rates: {},
|
|
250
|
+
error: undefined,
|
|
251
|
+
};
|
|
252
|
+
try {
|
|
253
|
+
const currencies = await this.#currencyList(cache);
|
|
254
|
+
if (!currencies || currencies.length === 0) {
|
|
255
|
+
result.error = {
|
|
256
|
+
info: 'No currencies found in database',
|
|
257
|
+
type: 'database_error',
|
|
258
|
+
};
|
|
259
|
+
return result;
|
|
260
|
+
}
|
|
261
|
+
let latestDate;
|
|
262
|
+
for (const record of currencies) {
|
|
263
|
+
const code = this.#getCurrencyCode(record);
|
|
264
|
+
const rate = this.#getCurrencyRate(record);
|
|
265
|
+
const updatedAt = this.#getCurrencyUpdatedAt(record);
|
|
266
|
+
if (!code || rate === undefined || rate === null) {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
// Filter by currency codes if specified
|
|
270
|
+
if (!currencyCodes || currencyCodes.length === 0 || currencyCodes.includes(code)) {
|
|
271
|
+
result.rates[code] = rate;
|
|
272
|
+
// Track the latest update date
|
|
273
|
+
if (updatedAt) {
|
|
274
|
+
const updatedAtDate = new Date(updatedAt);
|
|
275
|
+
if (!latestDate || updatedAtDate > latestDate) {
|
|
276
|
+
latestDate = updatedAtDate;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// Update result with latest date if found
|
|
282
|
+
if (latestDate) {
|
|
283
|
+
result.date = latestDate.toISOString();
|
|
284
|
+
result.timestamp = latestDate.getTime();
|
|
285
|
+
}
|
|
286
|
+
result.success = Object.keys(result.rates).length > 0;
|
|
287
|
+
if (!result.success) {
|
|
288
|
+
result.error = {
|
|
289
|
+
info: currencyCodes?.length
|
|
290
|
+
? `No matching currencies found for codes: ${currencyCodes.join(', ')}`
|
|
291
|
+
: 'No valid currencies found in database',
|
|
292
|
+
type: 'database_error',
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown database error';
|
|
298
|
+
result.error = {
|
|
299
|
+
info: errorMessage,
|
|
300
|
+
type: 'database_error',
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
return result;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Clear the currency cache
|
|
307
|
+
*/
|
|
308
|
+
async clearCache() {
|
|
309
|
+
if (!this.cache || !this.config.cache) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const { prefix = this.#defaultCacheKeyPrefix } = this.config.cache;
|
|
313
|
+
await this.cache.delete({ key: prefix });
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Refresh currency data from database
|
|
317
|
+
*/
|
|
318
|
+
async refreshCurrencyData() {
|
|
319
|
+
await this.clearCache();
|
|
320
|
+
// Pre-warm the cache
|
|
321
|
+
await this.#currencyList(true);
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Get convert rate (required abstract method)
|
|
325
|
+
*/
|
|
326
|
+
async getConvertRate(from, to) {
|
|
327
|
+
try {
|
|
328
|
+
const currencies = await this.#getCurrenciesByCodes([from, to]);
|
|
329
|
+
const fromCurrency = currencies?.find((c) => this.#getCurrencyCode(c) === from);
|
|
330
|
+
const toCurrency = currencies?.find((c) => this.#getCurrencyCode(c) === to);
|
|
331
|
+
if (!fromCurrency || !toCurrency) {
|
|
332
|
+
return undefined;
|
|
333
|
+
}
|
|
334
|
+
const fromRate = this.#getCurrencyRate(fromCurrency);
|
|
335
|
+
const toRate = this.#getCurrencyRate(toCurrency);
|
|
336
|
+
if (fromRate && toRate && fromRate > 0 && toRate > 0) {
|
|
337
|
+
return (1 / fromRate) * toRate;
|
|
338
|
+
}
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
return undefined;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Cleanup method for graceful shutdown
|
|
347
|
+
*/
|
|
348
|
+
async cleanup() {
|
|
349
|
+
if (this.cacheSetupPromise) {
|
|
350
|
+
try {
|
|
351
|
+
await this.cacheSetupPromise;
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
// Ignore errors during cleanup
|
|
355
|
+
}
|
|
356
|
+
this.cacheSetupPromise = undefined;
|
|
357
|
+
}
|
|
358
|
+
this.cache = undefined;
|
|
359
|
+
this.model = undefined;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { type CacheOptions, type CacheService } from '@adonisjs/cache/types';
|
|
2
|
+
import { type ApplicationService, type ConfigProvider } from '@adonisjs/core/types';
|
|
3
|
+
import { type LucidModel } from '@adonisjs/lucid/types/model';
|
|
4
|
+
import type BaseCurrencyService from '@mixxtor/currencyx-js';
|
|
5
|
+
import type { CurrencyExchanges, BaseCurrencyExchange, createCurrency } from '@mixxtor/currencyx-js';
|
|
6
|
+
export type { CurrencyExchanges, CurrencyCode } from '@mixxtor/currencyx-js';
|
|
7
|
+
/**
|
|
8
|
+
* Database configuration for currency provider
|
|
9
|
+
*/
|
|
10
|
+
export interface DatabaseConfig<Model extends LucidModel = LucidModel, Cache extends CacheConfig | undefined | false = CacheConfig | undefined> {
|
|
11
|
+
/**
|
|
12
|
+
* The Lucid model to use for currency queries
|
|
13
|
+
*/
|
|
14
|
+
model: () => Promise<{
|
|
15
|
+
default: Model;
|
|
16
|
+
}> | Model;
|
|
17
|
+
/**
|
|
18
|
+
* Base currency - all exchange rates in database are relative to this currency
|
|
19
|
+
* @default 'USD'
|
|
20
|
+
* @example 'USD' // 1 USD = 0.85 EUR, 1 USD = 0.73 GBP
|
|
21
|
+
*/
|
|
22
|
+
base?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Column mapping for the currency table
|
|
25
|
+
*/
|
|
26
|
+
columns?: {
|
|
27
|
+
/**
|
|
28
|
+
* Currency code column (e.g., 'USD', 'EUR')
|
|
29
|
+
* @default 'code'
|
|
30
|
+
*/
|
|
31
|
+
code: string;
|
|
32
|
+
/**
|
|
33
|
+
* Exchange rate column
|
|
34
|
+
* @default 'exchange_rate'
|
|
35
|
+
*/
|
|
36
|
+
rate: string;
|
|
37
|
+
/**
|
|
38
|
+
* Created at column
|
|
39
|
+
* @default 'created_at'
|
|
40
|
+
*/
|
|
41
|
+
created_at?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Updated at column
|
|
44
|
+
* @default 'updated_at'
|
|
45
|
+
*/
|
|
46
|
+
updated_at?: string;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Cache configuration for this database provider
|
|
50
|
+
* @default false
|
|
51
|
+
*/
|
|
52
|
+
cache?: Cache | undefined | false;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Cache configuration for database provider
|
|
56
|
+
*/
|
|
57
|
+
export interface CacheConfig extends CacheOptions {
|
|
58
|
+
/**
|
|
59
|
+
* The AdonisJS cache service instance
|
|
60
|
+
* @requires @adonisjs/cache
|
|
61
|
+
*/
|
|
62
|
+
service: () => Promise<{
|
|
63
|
+
default: CacheService;
|
|
64
|
+
}> | CacheService;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Complete currency configuration for AdonisJS
|
|
68
|
+
*/
|
|
69
|
+
export interface CurrencyConfig<KnownExchanges extends CurrencyExchanges = CurrencyExchanges> {
|
|
70
|
+
/**
|
|
71
|
+
* Default provider to use
|
|
72
|
+
*/
|
|
73
|
+
default: keyof KnownExchanges;
|
|
74
|
+
/**
|
|
75
|
+
* Provider configurations
|
|
76
|
+
*/
|
|
77
|
+
exchanges: Record<keyof KnownExchanges, BaseCurrencyExchange>;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Infer the providers from the user config
|
|
81
|
+
*/
|
|
82
|
+
export type InferExchanges<T extends ConfigProvider<{
|
|
83
|
+
exchanges: Record<string, ExchangeFactory>;
|
|
84
|
+
}>> = Awaited<ReturnType<T['resolver']>>['exchanges'];
|
|
85
|
+
/**
|
|
86
|
+
* Currency record interface for database queries
|
|
87
|
+
*/
|
|
88
|
+
export interface CurrencyRecord {
|
|
89
|
+
[key: string]: any;
|
|
90
|
+
code?: string;
|
|
91
|
+
rate?: number;
|
|
92
|
+
updated_at?: Date;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Representation of a factory function that returns
|
|
96
|
+
* an instance of a driver.
|
|
97
|
+
*/
|
|
98
|
+
export type ExchangeFactory = BaseCurrencyExchange;
|
|
99
|
+
/**
|
|
100
|
+
* Main Currency Service Implementation
|
|
101
|
+
*/
|
|
102
|
+
export interface CurrencyService extends BaseCurrencyService<CurrencyExchanges extends Record<string, ReturnType<typeof createCurrency>> ? CurrencyExchanges : never> {
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Service config provider is an extension of the config
|
|
106
|
+
* provider and accepts the name of the disk service
|
|
107
|
+
*/
|
|
108
|
+
export type ServiceConfigProvider<Factory extends ExchangeFactory> = {
|
|
109
|
+
type: 'provider';
|
|
110
|
+
resolver: (name: string, app: ApplicationService) => Promise<Factory>;
|
|
111
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|