s3db.js
Version:
Use AWS S3, the world's most reliable document storage, as a database with this ORM.
210 lines (174 loc) • 5.74 kB
JavaScript
import EventEmitter from "events";
export class Plugin extends EventEmitter {
constructor(options = {}) {
super();
this.name = this.constructor.name;
this.options = options;
this.hooks = new Map();
}
async setup(database) {
this.database = database;
this.beforeSetup();
await this.onSetup();
this.afterSetup();
}
async start() {
this.beforeStart();
await this.onStart();
this.afterStart();
}
async stop() {
this.beforeStop();
await this.onStop();
this.afterStop();
}
// Override these methods in subclasses
async onSetup() {
// Override in subclasses
}
async onStart() {
// Override in subclasses
}
async onStop() {
// Override in subclasses
}
// Hook management methods
addHook(resource, event, handler) {
if (!this.hooks.has(resource)) {
this.hooks.set(resource, new Map());
}
const resourceHooks = this.hooks.get(resource);
if (!resourceHooks.has(event)) {
resourceHooks.set(event, []);
}
resourceHooks.get(event).push(handler);
}
removeHook(resource, event, handler) {
const resourceHooks = this.hooks.get(resource);
if (resourceHooks && resourceHooks.has(event)) {
const handlers = resourceHooks.get(event);
const index = handlers.indexOf(handler);
if (index > -1) {
handlers.splice(index, 1);
}
}
}
// Enhanced resource method wrapping that supports multiple plugins
wrapResourceMethod(resource, methodName, wrapper) {
const originalMethod = resource[methodName];
if (!resource._pluginWrappers) {
resource._pluginWrappers = new Map();
}
if (!resource._pluginWrappers.has(methodName)) {
resource._pluginWrappers.set(methodName, []);
}
// Store the wrapper
resource._pluginWrappers.get(methodName).push(wrapper);
// Create the wrapped method if it doesn't exist
if (!resource[`_wrapped_${methodName}`]) {
resource[`_wrapped_${methodName}`] = originalMethod;
// Preserve jest mock if it's a mock function
const isJestMock = originalMethod && originalMethod._isMockFunction;
resource[methodName] = async function(...args) {
let result = await resource[`_wrapped_${methodName}`](...args);
// Apply all wrappers in order
for (const wrapper of resource._pluginWrappers.get(methodName)) {
result = await wrapper.call(this, result, args, methodName);
}
return result;
};
// Preserve jest mock properties if it was a mock
if (isJestMock) {
Object.setPrototypeOf(resource[methodName], Object.getPrototypeOf(originalMethod));
Object.assign(resource[methodName], originalMethod);
}
}
}
/**
* Add a middleware to intercept a resource method (Koa/Express style).
* Middleware signature: async (next, ...args) => { ... }
* - Chame next(...args) para continuar a cadeia.
* - Retorne sem chamar next para interromper.
* - Pode modificar argumentos/resultados.
*/
addMiddleware(resource, methodName, middleware) {
if (!resource._pluginMiddlewares) {
resource._pluginMiddlewares = {};
}
if (!resource._pluginMiddlewares[methodName]) {
resource._pluginMiddlewares[methodName] = [];
// Wrap the original method only once
const originalMethod = resource[methodName].bind(resource);
resource[methodName] = async function(...args) {
let idx = -1;
const next = async (...nextArgs) => {
idx++;
if (idx < resource._pluginMiddlewares[methodName].length) {
// Call next middleware
return await resource._pluginMiddlewares[methodName][idx].call(this, next, ...nextArgs);
} else {
// Call original method
return await originalMethod(...nextArgs);
}
};
return await next(...args);
};
}
resource._pluginMiddlewares[methodName].push(middleware);
}
// Partition-aware helper methods
getPartitionValues(data, resource) {
if (!resource.config?.partitions) return {};
const partitionValues = {};
for (const [partitionName, partitionDef] of Object.entries(resource.config.partitions)) {
if (partitionDef.fields) {
partitionValues[partitionName] = {};
for (const [fieldName, rule] of Object.entries(partitionDef.fields)) {
const value = this.getNestedFieldValue(data, fieldName);
// Only add field if value exists
if (value !== null && value !== undefined) {
partitionValues[partitionName][fieldName] = resource.applyPartitionRule(value, rule);
}
}
} else {
partitionValues[partitionName] = {};
}
}
return partitionValues;
}
getNestedFieldValue(data, fieldPath) {
if (!fieldPath.includes('.')) {
return data[fieldPath] ?? null;
}
const keys = fieldPath.split('.');
let value = data;
for (const key of keys) {
if (value && typeof value === 'object' && key in value) {
value = value[key];
} else {
return null;
}
}
return value ?? null;
}
// Event emission methods
beforeSetup() {
this.emit("plugin.beforeSetup", new Date());
}
afterSetup() {
this.emit("plugin.afterSetup", new Date());
}
beforeStart() {
this.emit("plugin.beforeStart", new Date());
}
afterStart() {
this.emit("plugin.afterStart", new Date());
}
beforeStop() {
this.emit("plugin.beforeStop", new Date());
}
afterStop() {
this.emit("plugin.afterStop", new Date());
}
}
export default Plugin;