fix: review comments and added dataFormat options

Signed-off-by: John Redwood <john.r.k.redwood@gmail.com>
This commit is contained in:
John Redwood
2025-08-13 08:06:43 +10:00
parent 133519ba25
commit 8f776ef81a
4 changed files with 181 additions and 172 deletions
+16
View File
@@ -796,6 +796,22 @@ export interface Config {
*/
mediaType?: 'text/plain' | 'application/json';
/**
* Optional data format configuration.
* If not provided, defaults to text/plain for both key and value.
*/
dataFormat?: {
/**
* Type of the key in the cache.
* @default 'text/plain'
*/
keyType?: 'text/plain' | 'application/json';
/**
* Type of the value in the cache.
* @default 'text/plain'
*/
valueType?: 'text/plain' | 'application/json';
};
/**
* Infinispan server host and port configuration.
* If this is an array, the client will connect to all servers in the list based on TOPOLOGY_AWARE routing.
@@ -261,26 +261,29 @@ export class CacheManager {
const stores: Record<string, typeof KeyvRedis> = {};
return (pluginId, defaultTtl) => {
if (this.storeOptions?.type !== 'redis') {
throw new Error(
`Internal error: Wrong config type passed to redis factory: ${this.storeOptions?.type}`,
);
}
if (!stores[pluginId]) {
if (this.storeOptions?.type === 'redis') {
const redisOptions = this.storeOptions?.client || {
keyPrefixSeparator: ':',
};
if (this.storeOptions?.cluster) {
// Create a Redis cluster
const cluster = createCluster(this.storeOptions?.cluster);
stores[pluginId] = new KeyvRedis(cluster, redisOptions);
} else {
// Create a regular Redis connection
stores[pluginId] = new KeyvRedis(this.connection, redisOptions);
}
// Always provide an error handler to avoid stopping the process
stores[pluginId].on('error', (err: Error) => {
this.logger?.error('Failed to create redis cache client', err);
this.errorHandler?.(err);
});
const redisOptions = this.storeOptions?.client || {
keyPrefixSeparator: ':',
};
if (this.storeOptions?.cluster) {
// Create a Redis cluster
const cluster = createCluster(this.storeOptions?.cluster);
stores[pluginId] = new KeyvRedis(cluster, redisOptions);
} else {
// Create a regular Redis connection
stores[pluginId] = new KeyvRedis(this.connection, redisOptions);
}
// Always provide an error handler to avoid stopping the process
stores[pluginId].on('error', (err: Error) => {
this.logger?.error('Failed to create redis cache client', err);
this.errorHandler?.(err);
});
}
return new Keyv({
namespace: pluginId,
@@ -298,26 +301,29 @@ export class CacheManager {
const stores: Record<string, typeof KeyvValkey> = {};
return (pluginId, defaultTtl) => {
if (this.storeOptions?.type !== 'valkey') {
throw new Error(
`Internal error: Wrong config type passed to valkey factory: ${this.storeOptions?.type}`,
);
}
if (!stores[pluginId]) {
if (this.storeOptions?.type === 'valkey') {
const valkeyOptions = this.storeOptions?.client || {
keyPrefixSeparator: ':',
};
if (this.storeOptions?.cluster) {
// Create a Valkey cluster (Redis cluster under the hood)
const cluster = createCluster(this.storeOptions?.cluster);
stores[pluginId] = new KeyvValkey(cluster, valkeyOptions);
} else {
// Create a regular Valkey connection
stores[pluginId] = new KeyvValkey(this.connection, valkeyOptions);
}
// Always provide an error handler to avoid stopping the process
stores[pluginId].on('error', (err: Error) => {
this.logger?.error('Failed to create valkey cache client', err);
this.errorHandler?.(err);
});
const valkeyOptions = this.storeOptions?.client || {
keyPrefixSeparator: ':',
};
if (this.storeOptions?.cluster) {
// Create a Valkey cluster (Redis cluster under the hood)
const cluster = createCluster(this.storeOptions?.cluster);
stores[pluginId] = new KeyvValkey(cluster, valkeyOptions);
} else {
// Create a regular Valkey connection
stores[pluginId] = new KeyvValkey(this.connection, valkeyOptions);
}
// Always provide an error handler to avoid stopping the process
stores[pluginId].on('error', (err: Error) => {
this.logger?.error('Failed to create valkey cache client', err);
this.errorHandler?.(err);
});
}
return new Keyv({
namespace: pluginId,
@@ -366,33 +372,37 @@ export class CacheManager {
const stores: Record<string, InfinispanKeyvStore> = {};
return (pluginId, defaultTtl) => {
if (this.storeOptions?.type !== 'infinispan') {
throw new Error(
`Internal error: Wrong config type passed to infinispan factory: ${this.storeOptions?.type}`,
);
}
if (!stores[pluginId]) {
if (this.storeOptions?.type === 'infinispan') {
// Use sync version for testing environments
const isTest =
process.env.NODE_ENV === 'test' || typeof jest !== 'undefined';
// Use sync version for testing environments
const isTest =
process.env.NODE_ENV === 'test' || typeof jest !== 'undefined';
// Create the client promise ONCE and reuse it
const clientPromise: Promise<InfinispanClientCacheInterface> = isTest
? this.createInfinispanClientSync()
: this.createInfinispanClientAsync();
// Create the client promise ONCE and reuse it
const clientPromise: Promise<InfinispanClientCacheInterface> = isTest
? this.createInfinispanClientSync()
: this.createInfinispanClientAsync();
this.logger?.info(
`Creating Infinispan cache client for plugin ${pluginId} isTest = ${isTest}`,
);
const storeInstance = new InfinispanKeyvStore({
clientPromise,
logger: this.logger!,
});
this.logger?.info(
`Creating Infinispan cache client for plugin ${pluginId} isTest = ${isTest}`,
);
const storeInstance = new InfinispanKeyvStore({
clientPromise,
logger: this.logger!,
});
stores[pluginId] = storeInstance;
stores[pluginId] = storeInstance;
// Always provide an error handler to avoid stopping the process
storeInstance.on('error', (err: Error) => {
this.logger?.error('Failed to create infinispan cache client', err);
this.errorHandler?.(err);
});
}
// Always provide an error handler to avoid stopping the process
storeInstance.on('error', (err: Error) => {
this.logger?.error('Failed to create infinispan cache client', err);
this.errorHandler?.(err);
});
}
return new Keyv({
@@ -19,6 +19,7 @@ import {
RootConfigService,
} from '@backstage/backend-plugin-api';
import {
DataFormatOptions,
InfinispanAuthOptions,
InfinispanCacheStoreOptions,
InfinispanClientAuthOptions,
@@ -55,29 +56,23 @@ export class InfinispanOptionsMapper {
serverConf =>
({
host: serverConf.getString('host'),
port: serverConf.getNumber('port'),
port: serverConf.getOptionalNumber('port') ?? 11222,
} as InfinispanServerConfig),
);
} else if (typeof serversConfig === 'object' && serversConfig !== null) {
const serverConf = infinispanConfig.getConfig('servers');
parsedOptions.servers = {
host: serverConf.getString('host'),
port: serverConf.getNumber('port'),
host: serverConf.getOptionalString('host') ?? '127.0.0.1',
port: serverConf.getOptionalString('port') ?? 11222,
} as InfinispanServerConfig;
} else {
logger?.error(
`Infinispan 'servers' configuration at ${storeConfigPath} must be an object or an array.`,
);
throw new Error(
`Infinispan 'servers' configuration at ${storeConfigPath} is invalid.`,
`Infinispan 'servers' configuration at ${storeConfigPath} is invalid, must be an object or an array.`,
);
}
} else {
logger?.error(
`Infinispan configuration at ${storeConfigPath} is missing the 'servers' definition.`,
);
throw new Error(
`Infinispan configuration at ${storeConfigPath} must define 'servers'.`,
logger?.warn(
`Infinispan configuration at ${storeConfigPath} is missing the 'servers' definition, will use client defaults.`,
);
}
@@ -85,42 +80,35 @@ export class InfinispanOptionsMapper {
// This will be used to configure the Infinispan client behavior.
const behaviorOptions: Partial<InfinispanClientBehaviorOptions> = {};
if (infinispanConfig.has('clusters')) {
const clustersConfig = infinispanConfig.getConfigArray('clusters');
const clusterOptions: InfinispanClusterConfig[] = [];
clustersConfig?.forEach(clusterConf => {
behaviorOptions.clusters = infinispanConfig
.getOptionalConfigArray('clusters')
?.map(clusterConf => {
const name = clusterConf.getOptionalString('name');
const cluster: InfinispanClusterConfig = {
return {
...(name && { name }),
servers: clusterConf.getConfigArray('servers').map(serverConf => ({
host: serverConf.getString('host'),
port: serverConf.getNumber('port'),
})),
};
clusterOptions.push(cluster);
behaviorOptions.clusters = clusterOptions;
} as InfinispanClusterConfig;
});
}
// Parse Default Options Start...
if (infinispanConfig.has('version')) {
const clientVersion = infinispanConfig.getString('version');
if (
clientVersion === '2.9' ||
clientVersion === '2.5' ||
clientVersion === '2.2'
) {
behaviorOptions.version = clientVersion;
} else if (clientVersion !== null && clientVersion !== undefined) {
logger?.warn(
`Invalid Infinispan client version "${clientVersion}" in config at ${storeConfigPath}.version. Must be "2.9", "2.5", or "2.2". It will be ignored, and the client may use a default or fail.`,
);
}
const clientVersion =
infinispanConfig.getOptionalString('version') ?? '2.9';
if (
clientVersion === '2.9' ||
clientVersion === '2.5' ||
clientVersion === '2.2'
) {
behaviorOptions.version = clientVersion;
} else if (clientVersion !== null && clientVersion !== undefined) {
logger?.warn(
`Invalid Infinispan client version "${clientVersion}" in config at ${storeConfigPath}.version. Must be "2.9", "2.5", or "2.2". It will be ignored, and the client may use a default or fail.`,
);
}
if (infinispanConfig.has('cacheName')) {
behaviorOptions.cacheName = infinispanConfig.getString('cacheName');
}
behaviorOptions.cacheName = infinispanConfig.getOptionalString('cacheName');
const mediaType = infinispanConfig.getOptionalString('mediaType');
if (mediaType === 'text/plain' || mediaType === 'application/json') {
@@ -131,95 +119,84 @@ export class InfinispanOptionsMapper {
);
}
if (infinispanConfig.has('maxRetries')) {
behaviorOptions.maxRetries = infinispanConfig.getNumber('maxRetries');
}
behaviorOptions.maxRetries =
infinispanConfig.getOptionalNumber('maxRetries');
if (infinispanConfig.has('topologyUpdates')) {
behaviorOptions.topologyUpdates =
infinispanConfig.getBoolean('topologyUpdates');
}
behaviorOptions.topologyUpdates =
infinispanConfig.getOptionalBoolean('topologyUpdates') ?? true;
// Default Options End...
// Parse Authentication and SSL Options
if (infinispanConfig.has('authentication')) {
const authConfig = infinispanConfig.getConfig('authentication');
const auth: Partial<InfinispanAuthOptions> = {};
const authConfig = infinispanConfig.getOptionalConfig('authentication');
const auth: Partial<InfinispanAuthOptions> = {};
if (authConfig.has('enabled')) {
auth.enabled = authConfig.getBoolean('enabled');
}
if (authConfig.has('saslMechanism')) {
auth.saslMechanism = authConfig.getString('saslMechanism');
}
if (authConfig.has('userName')) {
auth.userName = authConfig.getString('userName');
}
if (authConfig.has('password')) {
auth.password = authConfig.getString('password');
}
if (authConfig.has('token')) {
auth.token = authConfig.getString('token');
}
if (authConfig.has('authzid')) {
auth.authzid = authConfig.getString('authzid');
}
auth.enabled = authConfig?.getOptionalBoolean('enabled');
auth.saslMechanism = authConfig?.getOptionalString('saslMechanism');
auth.userName = authConfig?.getOptionalString('userName');
auth.password = authConfig?.getOptionalString('password');
auth.token = authConfig?.getOptionalString('token');
auth.authzid = authConfig?.getOptionalString('authzid');
behaviorOptions.authentication = auth as InfinispanAuthOptions;
behaviorOptions.authentication = auth as InfinispanAuthOptions;
const sslConfig = infinispanConfig.getOptionalConfig('ssl');
const ssl: Partial<InfinispanSslOptions> = {};
ssl.enabled = sslConfig?.getOptionalBoolean('enabled');
ssl.secureProtocol = sslConfig?.getOptionalString('secureProtocol');
ssl.trustCerts = sslConfig?.getOptionalStringArray('trustCerts');
ssl.sniHostName = sslConfig?.getOptionalString('sniHostname');
const clientAuth = infinispanConfig.getOptionalConfig('clientAuth');
const clientAuthOpts: Partial<InfinispanClientAuthOptions> = {};
clientAuthOpts.key = clientAuth?.getOptionalString('key');
clientAuthOpts.passphrase = clientAuth?.getOptionalString('passphrase');
clientAuthOpts.cert = clientAuth?.getOptionalString('cert');
ssl.clientAuth = clientAuthOpts as InfinispanClientAuthOptions;
const cryptoStore = infinispanConfig.getOptionalConfig('cryptoStore');
const cryptoStoreOpts: Partial<InfinispanClientAuthOptions> = {};
cryptoStoreOpts.key = cryptoStore?.getOptionalString('path');
cryptoStoreOpts.passphrase = cryptoStore?.getOptionalString('passphrase');
ssl.cryptoStore = cryptoStoreOpts as InfinispanClientAuthOptions;
behaviorOptions.ssl = ssl as InfinispanSslOptions;
const dataFormat = infinispanConfig.getOptionalConfig('dataFormat');
const dataFormatOpts: Partial<DataFormatOptions> = {};
const keyType = dataFormat?.getOptionalString('keyType') ?? 'text/plain';
const valueType =
dataFormat?.getOptionalString('valueType') ?? 'text/plain';
if (keyType === 'text/plain' || keyType === 'application/json') {
dataFormatOpts.keyType = keyType;
} else if (keyType !== null && keyType !== undefined) {
logger?.warn(
`Invalid Infinispan dataFormat.keyType "${keyType}" in config at ${storeConfigPath}.dataFormat.keyType. Must be "text/plain" | "application/json". Not mapped, client will use default 'text/plain'.`,
);
}
if (infinispanConfig.has('ssl')) {
const sslConfig = infinispanConfig.getConfig('ssl');
const ssl: Partial<InfinispanSslOptions> = {};
if (sslConfig.has('enabled')) {
ssl.enabled = sslConfig.getBoolean('enabled');
}
if (sslConfig.has('secureProtocol')) {
ssl.secureProtocol = sslConfig.getString('secureProtocol');
}
if (sslConfig.has('trustCerts')) {
ssl.trustCerts = sslConfig.getStringArray('trustCerts');
}
if (sslConfig.has('sniHostname')) {
ssl.sniHostName = sslConfig.getString('sniHostname');
}
if (sslConfig.has('clientAuth')) {
const clientAuth = infinispanConfig.getConfig('clientAuth');
const clientAuthOpts: Partial<InfinispanClientAuthOptions> = {};
if (clientAuth.has('key')) {
clientAuthOpts.key = clientAuth.getString('key');
}
if (clientAuth.has('passphrase')) {
clientAuthOpts.passphrase = clientAuth.getString('passphrase');
}
if (clientAuth.has('cert')) {
clientAuthOpts.cert = clientAuth.getString('cert');
}
ssl.clientAuth = clientAuthOpts as InfinispanClientAuthOptions;
}
if (sslConfig.has('cryptoStore')) {
const cryptoStore = infinispanConfig.getConfig('cryptoStore');
const cryptoStoreOpts: Partial<InfinispanClientAuthOptions> = {};
if (cryptoStore.has('path')) {
cryptoStoreOpts.key = cryptoStore.getString('path');
}
if (cryptoStore.has('passphrase')) {
cryptoStoreOpts.passphrase = cryptoStore.getString('passphrase');
}
ssl.cryptoStore = cryptoStoreOpts as InfinispanClientAuthOptions;
}
behaviorOptions.ssl = ssl as InfinispanSslOptions;
if (valueType === 'text/plain' || valueType === 'application/json') {
dataFormatOpts.valueType = valueType;
} else if (valueType !== null && valueType !== undefined) {
logger?.warn(
`Invalid Infinispan dataFormat.valueType "${valueType}" in config at ${storeConfigPath}.dataFormat.valueType. Must be "text/plain" | "application/json". Not mapped, client will use default 'text/plain'.`,
);
}
behaviorOptions.dataFormat = dataFormatOpts as DataFormatOptions;
parsedOptions.options = behaviorOptions as InfinispanClientBehaviorOptions;
logger?.debug(
`Parsed Infinispan options from config at ${storeConfigPath}: ${JSON.stringify(
parsedOptions,
)}`,
);
return parsedOptions as InfinispanCacheStoreOptions;
}
}
@@ -135,6 +135,11 @@ export type InfinispanClusterConfig = {
servers: InfinispanServerConfig[];
};
export type DataFormatOptions = {
keyType: 'text/plain' | 'application/json';
valueType: 'text/plain' | 'application/json';
};
/**
* Detailed client behavior options for the Infinispan client.
* @public
@@ -150,4 +155,5 @@ export type InfinispanClientBehaviorOptions = {
mediaType?: 'text/plain' | 'application/json';
topologyUpdates?: boolean;
clusters?: InfinispanClusterConfig[];
dataFormat: DataFormatOptions;
};