@vendure/core
Version:
A modern, headless ecommerce framework
389 lines • 15.8 kB
JavaScript
;
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SettingsStoreService = void 0;
const common_1 = require("@nestjs/common");
const core_1 = require("@nestjs/core");
const generated_types_1 = require("@vendure/common/lib/generated-types");
const ms_1 = __importDefault(require("ms"));
const errors_1 = require("../../../common/error/errors");
const injector_1 = require("../../../common/injector");
const config_service_1 = require("../../../config/config.service");
const vendure_logger_1 = require("../../../config/logger/vendure-logger");
const settings_store_types_1 = require("../../../config/settings-store/settings-store-types");
const transactional_connection_1 = require("../../../connection/transactional-connection");
const settings_store_entry_entity_1 = require("../../../entity/settings-store-entry/settings-store-entry.entity");
/**
* @description
* The SettingsStoreService provides a flexible settings storage system with support for
* scoping, permissions, and validation. It allows plugins and the core system to
* store and retrieve configuration data with fine-grained control over access and isolation.
*
* ## Usage
*
* Values are automatically scoped according to their field configuration:
*
* @example
* ```ts
* // In a service
* const userTheme = await this.settingsStoreService.get('dashboard.theme', ctx);
* await this.settingsStoreService.set('dashboard.theme', 'dark', ctx);
*
* // Get multiple values
* const settings = await this.settingsStoreService.getMany([
* 'dashboard.theme',
* 'dashboard.tableFilters'
* ], ctx);
* ```
*
* @docsCategory services
* @since 3.4.0
*/
let SettingsStoreService = class SettingsStoreService {
constructor(connection, moduleRef, configService) {
this.connection = connection;
this.moduleRef = moduleRef;
this.configService = configService;
this.fieldRegistry = new Map();
this.injector = new injector_1.Injector(this.moduleRef);
}
onModuleInit() {
this.initializeFieldRegistrations();
}
/**
* @description
* Initialize field registrations from the Vendure configuration.
* Called during module initialization.
*/
initializeFieldRegistrations() {
const settingsStoreFields = this.configService.settingsStoreFields || {};
for (const [namespace, fields] of Object.entries(settingsStoreFields)) {
this.register({ namespace, fields });
}
}
/**
* @description
* Register settings store fields. This is typically called during application
* bootstrap when processing the VendureConfig.
*/
register(registration) {
for (const field of registration.fields) {
const fullKey = `${registration.namespace}.${field.name}`;
this.fieldRegistry.set(fullKey, field);
vendure_logger_1.Logger.debug(`Registered settings store field: ${fullKey}`);
}
}
/**
* @description
* Get a value for the specified key. The value is automatically scoped
* according to the field's scope configuration.
*
* @param key - The full key (namespace.field)
* @param ctx - Request context for scoping and permissions
* @returns The stored value or undefined if not found or access denied
*/
async get(key, ctx) {
const fieldConfig = this.getFieldConfig(key);
if (!this.hasPermission(ctx, fieldConfig)) {
return undefined;
}
const scope = this.generateScope(key, undefined, ctx, fieldConfig);
const entry = await this.connection.getRepository(ctx, settings_store_entry_entity_1.SettingsStoreEntry).findOne({
where: { key, scope },
});
return entry === null || entry === void 0 ? void 0 : entry.value;
}
/**
* @description
* Get multiple values efficiently. Each key is scoped according to
* its individual field configuration.
*
* @param keys - Array of full keys to retrieve
* @param ctx - Request context for scoping and permissions
* @returns Object mapping keys to their values
*/
async getMany(keys, ctx) {
const result = {};
// Build array of key/scopeKey pairs for authorized keys
const queries = [];
for (const key of keys) {
const fieldConfig = this.getFieldConfig(key);
if (this.hasPermission(ctx, fieldConfig)) {
const scope = this.generateScope(key, undefined, ctx, fieldConfig);
queries.push({ key, scope });
}
}
if (queries.length === 0) {
return result;
}
// Execute single query for all authorized keys using OR conditions
const qb = this.connection.getRepository(ctx, settings_store_entry_entity_1.SettingsStoreEntry).createQueryBuilder('entry');
// Build OR conditions for each key/scope pair
const orConditions = queries
.map((q, index) => `(entry.key = :key${index} AND entry.scope = :scope${index})`)
.join(' OR ');
if (orConditions) {
qb.where(orConditions);
// Add parameters
queries.forEach((q, index) => {
qb.setParameter(`key${index}`, q.key);
qb.setParameter(`scope${index}`, q.scope);
});
}
const entries = await qb.getMany();
// Map results back to keys
for (const entry of entries) {
result[entry.key] = entry.value;
}
return result;
}
/**
* @description
* Set a value for the specified key with structured result feedback.
* This version returns detailed information about the success or failure
* of the operation instead of throwing errors.
*
* @param key - The full key (namespace.field)
* @param value - The value to store (must be JSON serializable)
* @param ctx - Request context for scoping and permissions
* @returns SetSettingsStoreValueResult with operation status and error details
*/
async set(key, value, ctx) {
try {
const fieldConfig = this.getFieldConfig(key);
if (!this.hasPermission(ctx, fieldConfig)) {
return {
key,
result: false,
error: 'Insufficient permissions to set settings store value',
};
}
if (fieldConfig.readonly) {
return {
key,
result: false,
error: 'Cannot modify readonly settings store field via API',
};
}
// Validate the value
await this.validateValue(key, value, ctx);
const scope = this.generateScope(key, value, ctx, fieldConfig);
const repo = this.connection.getRepository(ctx, settings_store_entry_entity_1.SettingsStoreEntry);
// Find existing entry or create new one
const entry = await repo.findOne({
where: { key, scope },
});
if (entry) {
entry.value = value;
await repo.save(entry);
}
else {
await repo.save({
key,
scope,
value,
});
}
return {
key,
result: true,
};
}
catch (error) {
return {
key,
result: false,
error: error instanceof Error ? error.message : 'Unknown error occurred',
};
}
}
/**
* @description
* Set multiple values with structured result feedback for each operation.
* This method will not throw errors but will return
* detailed results for each key-value pair.
*
* @param values - Object mapping keys to their values
* @param ctx - Request context for scoping and permissions
* @returns Array of SetSettingsStoreValueResult with operation status for each key
*/
async setMany(values, ctx) {
const results = [];
for (const [key, value] of Object.entries(values)) {
const result = await this.set(key, value, ctx);
results.push(result);
}
return results;
}
/**
* @description
* Get the field configuration for a key.
*/
getFieldDefinition(key) {
return this.fieldRegistry.get(key);
}
/**
* @description
* Validate a value against its field definition.
*/
async validateValue(key, value, ctx) {
const fieldConfig = this.fieldRegistry.get(key);
if (!(fieldConfig === null || fieldConfig === void 0 ? void 0 : fieldConfig.validate)) {
return;
}
const result = await fieldConfig.validate(value, this.injector, ctx);
if (typeof result === 'string') {
throw new errors_1.UserInputError(`Validation failed for ${key}: ${result}`);
}
if (Array.isArray(result)) {
throw new errors_1.UserInputError(`Validation failed for ${key}: ${JSON.stringify(result)}`);
}
}
/**
* @description
* Generate the scope key for a given field and context.
*/
generateScope(key, value, ctx, fieldConfig) {
const scopeFunction = fieldConfig.scope || settings_store_types_1.SettingsStoreScopes.global;
return scopeFunction({ key, value, ctx });
}
/**
* @description
* Get field configuration, throwing if not found.
*/
getFieldConfig(key) {
const config = this.fieldRegistry.get(key);
if (!config) {
throw new errors_1.InternalServerError(`Settings store field not registered: ${key}`);
}
return config;
}
/**
* @description
* Find orphaned settings store entries that no longer have corresponding field definitions.
*
* @param options - Options for filtering orphaned entries
* @returns Array of orphaned entries
*/
async findOrphanedEntries(options = {}) {
const { olderThan = '7d', maxDeleteCount = 1000 } = options;
// Parse duration to get cutoff date
const cutoffDate = this.parseDuration(olderThan);
const qb = this.connection.rawConnection
.getRepository(settings_store_entry_entity_1.SettingsStoreEntry)
.createQueryBuilder('entry')
.where('entry.updatedAt < :cutoffDate', { cutoffDate })
.orderBy('entry.updatedAt', 'ASC')
.limit(maxDeleteCount);
const allEntries = await qb.getMany();
const orphanedEntries = [];
// Check each entry against registered fields
for (const entry of allEntries) {
const fieldConfig = this.fieldRegistry.get(entry.key);
if (!fieldConfig) {
// This entry has no field definition - it's orphaned
orphanedEntries.push({
key: entry.key,
scope: entry.scope || '',
updatedAt: entry.updatedAt,
valuePreview: this.getValuePreview(entry.value),
});
}
}
return orphanedEntries;
}
/**
* @description
* Clean up orphaned settings store entries from the database.
*
* @param options - Options for the cleanup operation
* @returns Result of the cleanup operation
*/
async cleanupOrphanedEntries(options = {}) {
const { dryRun = false, batchSize = 100, maxDeleteCount = 1000 } = options;
// Find orphaned entries first
const orphanedEntries = await this.findOrphanedEntries(options);
if (dryRun) {
return {
deletedCount: orphanedEntries.length,
dryRun: true,
deletedEntries: orphanedEntries.slice(0, 10), // Sample for preview
};
}
let totalDeleted = 0;
const sampleDeletedEntries = [];
// Delete in batches
for (let i = 0; i < orphanedEntries.length && totalDeleted < maxDeleteCount; i += batchSize) {
const batch = orphanedEntries.slice(i, i + batchSize);
// Extract keys and scopes for deletion
const conditions = batch.map(entry => ({ key: entry.key, scope: entry.scope }));
await this.connection.rawConnection.getRepository(settings_store_entry_entity_1.SettingsStoreEntry).delete(conditions);
totalDeleted += batch.length;
// Keep first batch as sample
if (i === 0) {
sampleDeletedEntries.push(...batch.slice(0, 10));
}
vendure_logger_1.Logger.verbose(`Deleted batch of ${batch.length} orphaned settings store entries`);
}
vendure_logger_1.Logger.info(`Cleanup completed: deleted ${totalDeleted} orphaned settings store entries`);
return {
deletedCount: totalDeleted,
dryRun: false,
deletedEntries: sampleDeletedEntries,
};
}
/**
* @description
* Parse a duration string (e.g., '7d', '30m', '2h') into a Date object.
*/
parseDuration(duration) {
const milliseconds = (0, ms_1.default)(duration);
if (!milliseconds) {
throw new Error(`Invalid duration format: ${duration}. Use format like '7d', '2h', '30m'`);
}
return new Date(Date.now() - milliseconds);
}
/**
* @description
* Get a preview of a value for logging purposes, truncating if too large.
*/
getValuePreview(value) {
const stringValue = typeof value === 'string' ? value : JSON.stringify(value);
return stringValue.length > 100 ? stringValue.substring(0, 100) + '...' : stringValue;
}
/**
* @description
* Check if the current user has permission to access a field.
*/
hasPermission(ctx, fieldConfig) {
// Admin API: check required permissions
const requiredPermissions = fieldConfig.requiresPermission;
if (requiredPermissions) {
const permissions = Array.isArray(requiredPermissions)
? requiredPermissions
: [requiredPermissions];
return ctx.userHasPermissions(permissions);
}
// Default: require authentication
return ctx.userHasPermissions([generated_types_1.Permission.Authenticated]);
}
};
exports.SettingsStoreService = SettingsStoreService;
exports.SettingsStoreService = SettingsStoreService = __decorate([
(0, common_1.Injectable)(),
__metadata("design:paramtypes", [transactional_connection_1.TransactionalConnection,
core_1.ModuleRef,
config_service_1.ConfigService])
], SettingsStoreService);
//# sourceMappingURL=settings-store.service.js.map