@smythos/sre 1.5.13 → 1.5.16
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/README.md +2 -2
- package/dist/index.js +8 -8
- package/dist/index.js.map +1 -1
- package/dist/types/subsystems/IO/Storage.service/connectors/S3Storage.class.d.ts +2 -0
- package/dist/types/subsystems/Security/Vault.service/connectors/JSONFileVault.class.d.ts +7 -0
- package/dist/types/utils/lazy-client.d.ts +58 -0
- package/package.json +1 -1
- package/src/helpers/S3Cache.helper.ts +20 -2
- package/src/subsystems/IO/Storage.service/connectors/S3Storage.class.ts +61 -5
- package/src/subsystems/Security/Vault.service/connectors/JSONFileVault.class.ts +29 -2
- package/src/utils/lazy-client.ts +261 -0
|
@@ -15,7 +15,9 @@ export declare class S3Storage extends StorageConnector {
|
|
|
15
15
|
private client;
|
|
16
16
|
private bucket;
|
|
17
17
|
private isInitialized;
|
|
18
|
+
private initializationPromise;
|
|
18
19
|
constructor(_settings: S3Config);
|
|
20
|
+
private ensureInitialized;
|
|
19
21
|
private initialize;
|
|
20
22
|
/**
|
|
21
23
|
* Reads an object from the S3 bucket.
|
|
@@ -16,6 +16,13 @@ export declare class JSONFileVault extends VaultConnector {
|
|
|
16
16
|
constructor(_settings: JSONFileVaultConfig);
|
|
17
17
|
private findVaultFile;
|
|
18
18
|
private getMasterKeyInteractive;
|
|
19
|
+
/**
|
|
20
|
+
* Resolves environment variable references in vault values.
|
|
21
|
+
* Supports syntax: $env(VARIABLE_NAME)
|
|
22
|
+
* @param value The value to process
|
|
23
|
+
* @returns The value with environment variables resolved
|
|
24
|
+
*/
|
|
25
|
+
private resolveEnvironmentVariables;
|
|
19
26
|
protected get(acRequest: AccessRequest, keyId: string): Promise<any>;
|
|
20
27
|
protected exists(acRequest: AccessRequest, keyId: string): Promise<boolean>;
|
|
21
28
|
protected listKeys(acRequest: AccessRequest): Promise<string[]>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for lazy client error handling
|
|
3
|
+
*/
|
|
4
|
+
export interface LazyClientConfig {
|
|
5
|
+
/** Custom package name if different from import path */
|
|
6
|
+
packageName?: string;
|
|
7
|
+
/** Custom display name for the package */
|
|
8
|
+
displayName?: string;
|
|
9
|
+
/** Additional installation notes */
|
|
10
|
+
installNotes?: string;
|
|
11
|
+
/** Documentation URL */
|
|
12
|
+
docsUrl?: string;
|
|
13
|
+
/** Disable auto-installation messages */
|
|
14
|
+
silent?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Generic lazy loading utility for client libraries
|
|
18
|
+
* Preserves strong typing and requires minimal code changes
|
|
19
|
+
*/
|
|
20
|
+
export declare class LazyClient<T = any> {
|
|
21
|
+
private clientFactory;
|
|
22
|
+
private config;
|
|
23
|
+
private _client;
|
|
24
|
+
private _clientPromise;
|
|
25
|
+
constructor(clientFactory: () => Promise<T>, config?: LazyClientConfig);
|
|
26
|
+
/**
|
|
27
|
+
* Creates a proxy that records method calls and executes them when needed
|
|
28
|
+
*/
|
|
29
|
+
private createProxy;
|
|
30
|
+
/**
|
|
31
|
+
* Executes the recorded method chain on the actual client
|
|
32
|
+
*/
|
|
33
|
+
private executeMethodChain;
|
|
34
|
+
/**
|
|
35
|
+
* Ensures the client is loaded and returns it
|
|
36
|
+
*/
|
|
37
|
+
private ensureClient;
|
|
38
|
+
/**
|
|
39
|
+
* Returns the proxy that behaves like the actual client
|
|
40
|
+
*/
|
|
41
|
+
get client(): T;
|
|
42
|
+
/**
|
|
43
|
+
* Get the actual client instance (async)
|
|
44
|
+
*/
|
|
45
|
+
getClient(): Promise<T>;
|
|
46
|
+
/**
|
|
47
|
+
* Check if the client is already loaded
|
|
48
|
+
*/
|
|
49
|
+
get isLoaded(): boolean;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Helper function to create a lazy client with dynamic import
|
|
53
|
+
*/
|
|
54
|
+
export declare function createLazyClient<T>(importFn: () => Promise<any>, clientFactory: (module: any) => T, config?: LazyClientConfig): LazyClient<T>;
|
|
55
|
+
/**
|
|
56
|
+
* Specific helper for common client patterns
|
|
57
|
+
*/
|
|
58
|
+
export declare function createLazyClientFromConstructor<T>(packageName: string, constructorName: string, ...args: any[]): LazyClient<T>;
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { GetBucketLifecycleConfigurationCommandOutput, PutBucketLifecycleConfigurationCommand } from '@aws-sdk/client-s3';
|
|
2
2
|
|
|
3
3
|
import { GetBucketLifecycleConfigurationCommand } from '@aws-sdk/client-s3';
|
|
4
|
-
|
|
5
4
|
import { S3Client } from '@aws-sdk/client-s3';
|
|
5
|
+
import { Logger } from '@sre/helpers/Log.helper';
|
|
6
|
+
const console = Logger('S3Cache');
|
|
6
7
|
|
|
7
8
|
export function generateLifecycleRules() {
|
|
8
9
|
const rules = [];
|
|
@@ -58,7 +59,6 @@ export function generateLifecycleRules() {
|
|
|
58
59
|
return rules;
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
|
|
62
62
|
export function generateExpiryMetadata(expiryDays) {
|
|
63
63
|
let metadataValue;
|
|
64
64
|
|
|
@@ -90,6 +90,17 @@ export function ttlToExpiryDays(ttl: number) {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
export async function checkAndInstallLifecycleRules(bucketName: string, s3Client: S3Client) {
|
|
93
|
+
// Validate inputs
|
|
94
|
+
if (!bucketName || bucketName.trim() === '') {
|
|
95
|
+
throw new Error('Bucket name is required and cannot be empty');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!s3Client) {
|
|
99
|
+
throw new Error('S3Client is required');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log(`Checking lifecycle rules for bucket: ${bucketName}`);
|
|
103
|
+
|
|
93
104
|
try {
|
|
94
105
|
// Check existing lifecycle configuration
|
|
95
106
|
const getLifecycleCommand = new GetBucketLifecycleConfigurationCommand({ Bucket: bucketName });
|
|
@@ -105,6 +116,7 @@ export async function checkAndInstallLifecycleRules(bucketName: string, s3Client
|
|
|
105
116
|
const putLifecycleCommand = new PutBucketLifecycleConfigurationCommand(params);
|
|
106
117
|
// Put the new lifecycle configuration
|
|
107
118
|
await s3Client.send(putLifecycleCommand);
|
|
119
|
+
console.log(`Added ${nonExistingNewRules.length} new lifecycle rules to bucket: ${bucketName}`);
|
|
108
120
|
} else {
|
|
109
121
|
console.log('Lifecycle configuration already exists');
|
|
110
122
|
}
|
|
@@ -124,6 +136,12 @@ export async function checkAndInstallLifecycleRules(bucketName: string, s3Client
|
|
|
124
136
|
console.log('Lifecycle configuration created successfully.');
|
|
125
137
|
} else {
|
|
126
138
|
console.error('Error checking lifecycle configuration:', error);
|
|
139
|
+
console.error('Bucket name provided:', bucketName);
|
|
140
|
+
console.error('Error details:', {
|
|
141
|
+
name: error.name,
|
|
142
|
+
message: error.message,
|
|
143
|
+
code: error.code,
|
|
144
|
+
});
|
|
127
145
|
}
|
|
128
146
|
}
|
|
129
147
|
}
|
|
@@ -58,10 +58,18 @@ export class S3Storage extends StorageConnector {
|
|
|
58
58
|
private client: S3Client;
|
|
59
59
|
private bucket: string;
|
|
60
60
|
private isInitialized: boolean = false;
|
|
61
|
+
private initializationPromise: Promise<void> | null = null;
|
|
61
62
|
|
|
62
63
|
constructor(protected _settings: S3Config) {
|
|
63
64
|
super(_settings);
|
|
64
65
|
//if (!SmythRuntime.Instance) throw new Error('SRE not initialized');
|
|
66
|
+
|
|
67
|
+
// Validate required configuration
|
|
68
|
+
if (!_settings.bucket || _settings.bucket.trim() === '') {
|
|
69
|
+
console.warn('S3 bucket name is required and cannot be empty, connector not initialized');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
65
73
|
this.bucket = _settings.bucket;
|
|
66
74
|
const clientConfig: any = {};
|
|
67
75
|
if (_settings.region) clientConfig.region = _settings.region;
|
|
@@ -73,12 +81,41 @@ export class S3Storage extends StorageConnector {
|
|
|
73
81
|
}
|
|
74
82
|
|
|
75
83
|
this.client = new S3Client(clientConfig);
|
|
76
|
-
|
|
84
|
+
// Don't call initialize() synchronously in constructor
|
|
85
|
+
// It will be called when needed by methods that require initialization
|
|
77
86
|
}
|
|
78
87
|
|
|
79
|
-
private async
|
|
80
|
-
|
|
81
|
-
|
|
88
|
+
private async ensureInitialized(): Promise<void> {
|
|
89
|
+
if (this.isInitialized) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (this.initializationPromise) {
|
|
94
|
+
return this.initializationPromise;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
this.initializationPromise = this.initialize();
|
|
98
|
+
return this.initializationPromise;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private async initialize(): Promise<void> {
|
|
102
|
+
if (!this.client) {
|
|
103
|
+
console.warn('S3 client not initialized');
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (this.isInitialized) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
await checkAndInstallLifecycleRules(this.bucket, this.client);
|
|
112
|
+
this.isInitialized = true;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error('Failed to initialize S3Storage:', error);
|
|
115
|
+
// Reset the initialization promise so it can be retried
|
|
116
|
+
this.initializationPromise = null;
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
82
119
|
}
|
|
83
120
|
|
|
84
121
|
/**
|
|
@@ -90,6 +127,8 @@ export class S3Storage extends StorageConnector {
|
|
|
90
127
|
|
|
91
128
|
@SecureConnector.AccessControl
|
|
92
129
|
public async read(acRequest: AccessRequest, resourceId: string) {
|
|
130
|
+
await this.ensureInitialized();
|
|
131
|
+
|
|
93
132
|
// const accessTicket = await this.getAccessTicket(resourceId, acRequest);
|
|
94
133
|
// if (accessTicket.access !== TAccessResult.Granted) throw new Error('Access Denied');
|
|
95
134
|
const params = {
|
|
@@ -133,6 +172,8 @@ export class S3Storage extends StorageConnector {
|
|
|
133
172
|
|
|
134
173
|
@SecureConnector.AccessControl
|
|
135
174
|
async getMetadata(acRequest: AccessRequest, resourceId: string): Promise<StorageMetadata | undefined> {
|
|
175
|
+
await this.ensureInitialized();
|
|
176
|
+
|
|
136
177
|
// const accessTicket = await this.getAccessTicket(resourceId, acRequest);
|
|
137
178
|
// if (accessTicket.access !== TAccessResult.Granted) throw new Error('Access Denied');
|
|
138
179
|
|
|
@@ -147,6 +188,8 @@ export class S3Storage extends StorageConnector {
|
|
|
147
188
|
|
|
148
189
|
@SecureConnector.AccessControl
|
|
149
190
|
async setMetadata(acRequest: AccessRequest, resourceId: string, metadata: StorageMetadata) {
|
|
191
|
+
await this.ensureInitialized();
|
|
192
|
+
|
|
150
193
|
// const accessTicket = await this.getAccessTicket(resourceId, acRequest);
|
|
151
194
|
// if (accessTicket.access !== TAccessResult.Granted) throw new Error('Access Denied');
|
|
152
195
|
|
|
@@ -171,9 +214,10 @@ export class S3Storage extends StorageConnector {
|
|
|
171
214
|
*/
|
|
172
215
|
@SecureConnector.AccessControl
|
|
173
216
|
async write(acRequest: AccessRequest, resourceId: string, value: StorageData, acl?: IACL, metadata?: StorageMetadata): Promise<void> {
|
|
217
|
+
await this.ensureInitialized();
|
|
218
|
+
|
|
174
219
|
// const accessTicket = await this.getAccessTicket(resourceId, acRequest);
|
|
175
220
|
// if (accessTicket.access !== TAccessResult.Granted) throw new Error('Access Denied');
|
|
176
|
-
if (!this.isInitialized) await this.initialize();
|
|
177
221
|
const accessCandidate = acRequest.candidate;
|
|
178
222
|
|
|
179
223
|
let amzACL = ACL.from(acl).addAccess(accessCandidate.role, accessCandidate.id, TAccessLevel.Owner).ACL;
|
|
@@ -207,6 +251,8 @@ export class S3Storage extends StorageConnector {
|
|
|
207
251
|
*/
|
|
208
252
|
@SecureConnector.AccessControl
|
|
209
253
|
async delete(acRequest: AccessRequest, resourceId: string): Promise<void> {
|
|
254
|
+
await this.ensureInitialized();
|
|
255
|
+
|
|
210
256
|
// const accessTicket = await this.getAccessTicket(resourceId, acRequest);
|
|
211
257
|
// if (accessTicket.access !== TAccessResult.Granted) throw new Error('Access Denied');
|
|
212
258
|
|
|
@@ -225,6 +271,8 @@ export class S3Storage extends StorageConnector {
|
|
|
225
271
|
|
|
226
272
|
@SecureConnector.AccessControl
|
|
227
273
|
async exists(acRequest: AccessRequest, resourceId: string): Promise<boolean> {
|
|
274
|
+
await this.ensureInitialized();
|
|
275
|
+
|
|
228
276
|
// const accessTicket = await this.getAccessTicket(resourceId, acRequest);
|
|
229
277
|
// if (accessTicket.access !== TAccessResult.Granted) throw new Error('Access Denied');
|
|
230
278
|
const command = new HeadObjectCommand({
|
|
@@ -250,6 +298,8 @@ export class S3Storage extends StorageConnector {
|
|
|
250
298
|
//if the resource exists we read it's ACL and return it
|
|
251
299
|
//if the resource does not exist we return an write access ACL for the candidate
|
|
252
300
|
public async getResourceACL(resourceId: string, candidate: IAccessCandidate) {
|
|
301
|
+
await this.ensureInitialized();
|
|
302
|
+
|
|
253
303
|
const s3Metadata = await this.getS3Metadata(resourceId);
|
|
254
304
|
const exists = s3Metadata !== undefined; //undefined metadata means the resource does not exist
|
|
255
305
|
//let acl: ACL = ACL.from(s3Metadata?.['x-amz-meta-acl'] as IACL);
|
|
@@ -263,6 +313,8 @@ export class S3Storage extends StorageConnector {
|
|
|
263
313
|
|
|
264
314
|
@SecureConnector.AccessControl
|
|
265
315
|
async getACL(acRequest: AccessRequest, resourceId: string): Promise<ACL | undefined> {
|
|
316
|
+
await this.ensureInitialized();
|
|
317
|
+
|
|
266
318
|
// const accessTicket = await this.getAccessTicket(resourceId, acRequest);
|
|
267
319
|
// if (accessTicket.access !== TAccessResult.Granted) throw new Error('Access Denied');
|
|
268
320
|
|
|
@@ -277,6 +329,8 @@ export class S3Storage extends StorageConnector {
|
|
|
277
329
|
|
|
278
330
|
@SecureConnector.AccessControl
|
|
279
331
|
async setACL(acRequest: AccessRequest, resourceId: string, acl: IACL) {
|
|
332
|
+
await this.ensureInitialized();
|
|
333
|
+
|
|
280
334
|
// const accessTicket = await this.getAccessTicket(resourceId, acRequest);
|
|
281
335
|
// if (accessTicket.access !== TAccessResult.Granted) throw new Error('Access Denied');
|
|
282
336
|
|
|
@@ -294,6 +348,8 @@ export class S3Storage extends StorageConnector {
|
|
|
294
348
|
|
|
295
349
|
@SecureConnector.AccessControl
|
|
296
350
|
async expire(acRequest: AccessRequest, resourceId: string, ttl: number) {
|
|
351
|
+
await this.ensureInitialized();
|
|
352
|
+
|
|
297
353
|
const expiryMetadata = generateExpiryMetadata(ttlToExpiryDays(ttl)); // seconds to days
|
|
298
354
|
const s3PutObjectTaggingCommand = new PutObjectTaggingCommand({
|
|
299
355
|
Bucket: this.bucket,
|
|
@@ -23,7 +23,7 @@ export type JSONFileVaultConfig = {
|
|
|
23
23
|
shared?: string;
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
-
export class JSONFileVault extends VaultConnector {
|
|
26
|
+
export class JSONFileVault extends VaultConnector {
|
|
27
27
|
public name: string = 'JSONFileVault';
|
|
28
28
|
private vaultData: any;
|
|
29
29
|
private index: any;
|
|
@@ -119,12 +119,39 @@ export class JSONFileVault extends VaultConnector {
|
|
|
119
119
|
return masterKey;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Resolves environment variable references in vault values.
|
|
124
|
+
* Supports syntax: $env(VARIABLE_NAME)
|
|
125
|
+
* @param value The value to process
|
|
126
|
+
* @returns The value with environment variables resolved
|
|
127
|
+
*/
|
|
128
|
+
private resolveEnvironmentVariables(value: any): any {
|
|
129
|
+
if (typeof value !== 'string') {
|
|
130
|
+
return value;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Match $env(VARIABLE_NAME) pattern
|
|
134
|
+
const envVarPattern = /\$env\(([^)]+)\)/g;
|
|
135
|
+
|
|
136
|
+
return value.replace(envVarPattern, (match, envVarName) => {
|
|
137
|
+
const envValue = process.env[envVarName];
|
|
138
|
+
if (envValue === undefined) {
|
|
139
|
+
console.warn(`Environment variable ${envVarName} not found, keeping original value: ${match}`);
|
|
140
|
+
return match;
|
|
141
|
+
}
|
|
142
|
+
return envValue;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
122
146
|
@SecureConnector.AccessControl
|
|
123
147
|
protected async get(acRequest: AccessRequest, keyId: string) {
|
|
124
148
|
const accountConnector = ConnectorService.getAccountConnector();
|
|
125
149
|
const teamId = await accountConnector.getCandidateTeam(acRequest.candidate);
|
|
126
150
|
|
|
127
|
-
|
|
151
|
+
const rawValue = this.vaultData?.[teamId]?.[keyId] || this.vaultData?.[this.shared]?.[keyId];
|
|
152
|
+
|
|
153
|
+
// Resolve environment variables if the value contains $env() references
|
|
154
|
+
return this.resolveEnvironmentVariables(rawValue);
|
|
128
155
|
}
|
|
129
156
|
|
|
130
157
|
@SecureConnector.AccessControl
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for lazy client error handling
|
|
3
|
+
*/
|
|
4
|
+
export interface LazyClientConfig {
|
|
5
|
+
/** Custom package name if different from import path */
|
|
6
|
+
packageName?: string;
|
|
7
|
+
/** Custom display name for the package */
|
|
8
|
+
displayName?: string;
|
|
9
|
+
/** Additional installation notes */
|
|
10
|
+
installNotes?: string;
|
|
11
|
+
/** Documentation URL */
|
|
12
|
+
docsUrl?: string;
|
|
13
|
+
/** Disable auto-installation messages */
|
|
14
|
+
silent?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Detects the package manager being used
|
|
19
|
+
*/
|
|
20
|
+
function detectPackageManager(): string {
|
|
21
|
+
// Check npm_config_user_agent environment variable
|
|
22
|
+
const userAgent = process.env.npm_config_user_agent;
|
|
23
|
+
if (userAgent?.includes('pnpm')) return 'pnpm';
|
|
24
|
+
if (userAgent?.includes('yarn')) return 'yarn';
|
|
25
|
+
|
|
26
|
+
// Fallback: check for lock files in cwd (if accessible)
|
|
27
|
+
try {
|
|
28
|
+
const fs = require('fs');
|
|
29
|
+
if (fs.existsSync('pnpm-lock.yaml')) return 'pnpm';
|
|
30
|
+
if (fs.existsSync('yarn.lock')) return 'yarn';
|
|
31
|
+
} catch {
|
|
32
|
+
// Ignore file system errors
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return 'npm';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Extracts package name from import path
|
|
40
|
+
*/
|
|
41
|
+
function extractPackageName(importPath: string, config: LazyClientConfig): string {
|
|
42
|
+
if (config.packageName) {
|
|
43
|
+
return config.packageName;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Handle scoped packages: @scope/package or @scope/package/subpath
|
|
47
|
+
if (importPath.startsWith('@')) {
|
|
48
|
+
const parts = importPath.split('/');
|
|
49
|
+
return `${parts[0]}/${parts[1]}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Handle regular packages: package or package/subpath
|
|
53
|
+
return importPath.split('/')[0];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Creates a helpful error message for missing packages
|
|
58
|
+
*/
|
|
59
|
+
function createInstallationMessage(importPath: string, originalError: Error, config: LazyClientConfig): Error {
|
|
60
|
+
if (config.silent) {
|
|
61
|
+
return originalError;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const packageName = extractPackageName(importPath, config);
|
|
65
|
+
const displayName = config.displayName || packageName;
|
|
66
|
+
const packageManager = detectPackageManager();
|
|
67
|
+
|
|
68
|
+
let message = `\n🔌 ${displayName} is required but not installed\n\n`;
|
|
69
|
+
|
|
70
|
+
message += `Quick install:\n`;
|
|
71
|
+
message += ` ${packageManager} add ${packageName}\n\n`;
|
|
72
|
+
|
|
73
|
+
// Add alternative package managers
|
|
74
|
+
if (packageManager !== 'npm') {
|
|
75
|
+
message += `Or with npm:\n npm install ${packageName}\n\n`;
|
|
76
|
+
}
|
|
77
|
+
if (packageManager !== 'pnpm') {
|
|
78
|
+
message += `Or with pnpm:\n pnpm add ${packageName}\n\n`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (config.installNotes) {
|
|
82
|
+
message += `📝 Note: ${config.installNotes}\n\n`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (config.docsUrl) {
|
|
86
|
+
message += `📚 Documentation: ${config.docsUrl}\n\n`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
message += `Original error: ${originalError.message}`;
|
|
90
|
+
|
|
91
|
+
const enhancedError = new Error(message);
|
|
92
|
+
enhancedError.name = 'LazyClientImportError';
|
|
93
|
+
enhancedError.stack = originalError.stack;
|
|
94
|
+
return enhancedError;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Generic lazy loading utility for client libraries
|
|
99
|
+
* Preserves strong typing and requires minimal code changes
|
|
100
|
+
*/
|
|
101
|
+
export class LazyClient<T = any> {
|
|
102
|
+
private _client: T | null = null;
|
|
103
|
+
private _clientPromise: Promise<T> | null = null;
|
|
104
|
+
|
|
105
|
+
constructor(private clientFactory: () => Promise<T>, private config: LazyClientConfig = {}) {}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Creates a proxy that records method calls and executes them when needed
|
|
109
|
+
*/
|
|
110
|
+
private createProxy(methodPath: Array<{ method: string; args: any[] }> = []): any {
|
|
111
|
+
const self = this;
|
|
112
|
+
|
|
113
|
+
// Create a proxy that can be both called and have properties accessed
|
|
114
|
+
return new Proxy(() => {}, {
|
|
115
|
+
get(target, prop, receiver) {
|
|
116
|
+
if (prop === 'then') {
|
|
117
|
+
// This is being awaited - execute the method chain and return a thenable
|
|
118
|
+
const promise = self.executeMethodChain(methodPath);
|
|
119
|
+
return promise.then.bind(promise);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (prop === 'catch') {
|
|
123
|
+
// Handle .catch() calls
|
|
124
|
+
const promise = self.executeMethodChain(methodPath);
|
|
125
|
+
return promise.catch.bind(promise);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (prop === 'finally') {
|
|
129
|
+
// Handle .finally() calls
|
|
130
|
+
const promise = self.executeMethodChain(methodPath);
|
|
131
|
+
return promise.finally.bind(promise);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (prop === Symbol.toStringTag) {
|
|
135
|
+
return 'LazyClient';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (typeof prop !== 'string') {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Return a function that extends the method chain
|
|
143
|
+
return (...args: any[]) => {
|
|
144
|
+
const newPath = [...methodPath, { method: prop, args }];
|
|
145
|
+
return self.createProxy(newPath);
|
|
146
|
+
};
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
apply(target, thisArg, argumentsList) {
|
|
150
|
+
// If called as a function, extend the method chain
|
|
151
|
+
return self.createProxy(methodPath);
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
has(target, prop) {
|
|
155
|
+
return true;
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Executes the recorded method chain on the actual client
|
|
162
|
+
*/
|
|
163
|
+
private async executeMethodChain(methodPath: Array<{ method: string; args: any[] }>): Promise<any> {
|
|
164
|
+
const client = await this.ensureClient();
|
|
165
|
+
|
|
166
|
+
let current: any = client;
|
|
167
|
+
|
|
168
|
+
// Execute each method call in sequence
|
|
169
|
+
for (const { method, args } of methodPath) {
|
|
170
|
+
if (current && typeof current[method] === 'function') {
|
|
171
|
+
current = current[method].apply(current, args);
|
|
172
|
+
} else if (current && current[method] !== undefined) {
|
|
173
|
+
current = current[method];
|
|
174
|
+
} else {
|
|
175
|
+
throw new Error(`Method or property '${method}' not found`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return current;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Ensures the client is loaded and returns it
|
|
184
|
+
*/
|
|
185
|
+
private async ensureClient(): Promise<T> {
|
|
186
|
+
if (this._client) {
|
|
187
|
+
return this._client;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!this._clientPromise) {
|
|
191
|
+
this._clientPromise = this.clientFactory().catch((error) => {
|
|
192
|
+
// Reset promise so next attempt will retry
|
|
193
|
+
this._clientPromise = null;
|
|
194
|
+
throw error;
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
this._client = await this._clientPromise;
|
|
199
|
+
return this._client;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Returns the proxy that behaves like the actual client
|
|
204
|
+
*/
|
|
205
|
+
get client(): T {
|
|
206
|
+
return this.createProxy() as T;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Get the actual client instance (async)
|
|
211
|
+
*/
|
|
212
|
+
async getClient(): Promise<T> {
|
|
213
|
+
return this.ensureClient();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Check if the client is already loaded
|
|
218
|
+
*/
|
|
219
|
+
get isLoaded(): boolean {
|
|
220
|
+
return this._client !== null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Helper function to create a lazy client with dynamic import
|
|
226
|
+
*/
|
|
227
|
+
export function createLazyClient<T>(importFn: () => Promise<any>, clientFactory: (module: any) => T, config: LazyClientConfig = {}): LazyClient<T> {
|
|
228
|
+
return new LazyClient<T>(async () => {
|
|
229
|
+
try {
|
|
230
|
+
const module = await importFn();
|
|
231
|
+
return clientFactory(module);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
// Try to extract import path from the error or use the configured package name
|
|
234
|
+
const importPath = config.packageName || 'unknown-package';
|
|
235
|
+
throw createInstallationMessage(importPath, error as Error, config);
|
|
236
|
+
}
|
|
237
|
+
}, config);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Specific helper for common client patterns
|
|
242
|
+
*/
|
|
243
|
+
// export function createLazyClientFromConstructor<T>(
|
|
244
|
+
// importFn: () => Promise<any>,
|
|
245
|
+
// constructorName: string,
|
|
246
|
+
// config: LazyClientConfig = {},
|
|
247
|
+
// ...args: any[]
|
|
248
|
+
// ): LazyClient<T> {
|
|
249
|
+
// return createLazyClient<T>(importFn, (module) => new module[constructorName](...args), config);
|
|
250
|
+
// }
|
|
251
|
+
export function createLazyClientFromConstructor<T>(packageName: string, constructorName: string, ...args: any[]): LazyClient<T> {
|
|
252
|
+
const importFn = () => import(packageName);
|
|
253
|
+
const config = {
|
|
254
|
+
packageName,
|
|
255
|
+
displayName: constructorName,
|
|
256
|
+
//installNotes: `Install ${packageName} with your package manager`,
|
|
257
|
+
//docsUrl: `https://www.npmjs.com/package/${packageName}`,
|
|
258
|
+
};
|
|
259
|
+
console.log('LazyClient', packageName, constructorName);
|
|
260
|
+
return createLazyClient<T>(importFn, (module) => new module[constructorName](...args), config);
|
|
261
|
+
}
|