diff --git a/providers/README.md b/providers/README.md deleted file mode 100644 index ac2ab06..0000000 --- a/providers/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# The providers directory - -The `providers` directory contains the service providers exported by your application. Make sure to register these providers within the `exports` collection (aka package entrypoints) defined within the `package.json` file. - -Learn more about [package entrypoints](https://nodejs.org/api/packages.html#package-entry-points). diff --git a/providers/activity_log_provider.ts b/providers/activity_log_provider.ts new file mode 100644 index 0000000..b302a73 --- /dev/null +++ b/providers/activity_log_provider.ts @@ -0,0 +1,25 @@ +import { ApplicationService } from '@adonisjs/core/types' +import MorphMap from '../src/morph_map.js' +import { LogManager } from '../src/logger.js' +import ActivityLog from '../src/models/activity_log.js' + +declare module '@adonisjs/core/types' { + export interface ContainerBindings { + morphMap: MorphMap + } +} +export default class ActivityLogProvider { + constructor(protected app: ApplicationService) {} + + register() { + this.app.container.singleton('morphMap', async () => { + return new MorphMap() + }) + } + + async boot() { + LogManager.setModelClass(ActivityLog) + const map = await this.app.container.make('morphMap') + LogManager.setMorphMap(map) + } +} diff --git a/src/decorators.ts b/src/decorators.ts new file mode 100644 index 0000000..8f4a69c --- /dev/null +++ b/src/decorators.ts @@ -0,0 +1,23 @@ +import app from '@adonisjs/core/services/app' + +export function MorphMap(param: string) { + return function (target: T) { + const service = async function () { + var result = await app.container.make('morphMap') + result.set(param, target) + return param + } + + target.prototype.__morphMapName = service() + target.prototype.__morphMapName = param + } +} + +export function getClassPath(clazz: T): string { + const morphMapName = clazz.prototype.__morphMapName + if (!morphMapName) { + throw new Error('morph map name not specified') + } + + return morphMapName +} diff --git a/src/logger.ts b/src/logger.ts new file mode 100644 index 0000000..39f1da1 --- /dev/null +++ b/src/logger.ts @@ -0,0 +1,148 @@ +import { BaseModel } from '@adonisjs/lucid/orm' +import { + LucidModel, + ModelAdapterOptions, + ModelQueryBuilderContract, +} from '@adonisjs/lucid/types/model' +import { LogModel, MorphInterface } from './types.js' + +export class LogManager { + static _modelClass: typeof BaseModel + + private static _map: MorphInterface + + static setModelClass(modelClass: typeof BaseModel) { + this._modelClass = modelClass + } + + static setMorphMap(map: MorphInterface) { + this._map = map + } + + static morphMap() { + return this._map + } +} + +export class ActivityBuilder { + private _adapterOptions?: ModelAdapterOptions + private _queryBuilder: ModelQueryBuilderContract | null = null + + queryOptions(options?: ModelAdapterOptions) { + this._adapterOptions = options + return this + } + + private _name: string | null = null + private _description: string | null = null + + private _modelId: string | number | null = null + private _modelType: string | null = null + + private _event: string | null = null + + private _entityId: string | number | null = null + private _entityType: string | null = null + + private _properties: Object | null = null + private _batchId: string | null = null + + name(name: string) { + this._name = name + return this + } + + by(model: string, modelId: string | number): ActivityBuilder + by(model: LogModel): ActivityBuilder + by(model: LogModel | string, modelId?: string | number) { + if (typeof model !== 'string') { + this._modelId = model.getModelId() + this._modelType = LogManager.morphMap().getAlias(model) + } else if (typeof modelId === 'string' || typeof modelId === 'number') { + this._modelId = modelId + this._modelType = model + } else { + throw new Error('Invalid arguments provided') + } + return this + } + + making(event: string) { + this._event = event + return this + } + + on(entity: string, entityId: string | number): ActivityBuilder + on(entity: LogModel): ActivityBuilder + on(entity: LogModel | string, entityId?: string | number) { + if (typeof entity !== 'string') { + this._entityId = entity.getModelId() + this._entityType = LogManager.morphMap().getAlias(entity) + } else if (typeof entityId === 'string' || typeof entityId === 'number') { + this._entityId = entityId + this._entityType = entity + } else { + throw new Error('Invalid arguments provided') + } + return this + } + + havingProperties(state: Object) { + this._properties = state + return this + } + + withBatch(batchId: string) { + this._batchId = batchId + return this + } + + log(message: string) { + const state = this.state() + state.description = message + // Here you would typically save the log to the database or perform the logging operation + console.log(state) + } + + state() { + return { + name: this._name, + model_id: this._modelId, + model_type: this._modelType, + event: this._event, + entity_id: this._entityId, + entity_type: this._entityType, + properties: this._properties, + batch_id: this._batchId, + description: this._description, + } + } + + customQuery(callback: (query: ModelQueryBuilderContract) => void) { + callback(this.getBuilder()) + return this + } + + private getBuilder() { + if (!this._queryBuilder) { + this._queryBuilder = LogManager._modelClass.query(this._adapterOptions) + } + return this._queryBuilder + } +} + +export function activity() { + return new ActivityBuilder() +} + +// Example usage: +activity() + .by('User', 1) + .making('edit') + .on('Product', 2) + .havingProperties({}) + .customQuery((query) => { + query.where('status', 'active') + query.where('created_at', '>=', new Date('2023-01-01')) + }) + .log('Edited product') diff --git a/src/models/activity_log.ts b/src/models/activity_log.ts new file mode 100644 index 0000000..a90a000 --- /dev/null +++ b/src/models/activity_log.ts @@ -0,0 +1,47 @@ +import { DateTime } from 'luxon' +import { BaseModel, column, scope } from '@adonisjs/lucid/orm' + +export default class ActivityLog extends BaseModel { + @column({ isPrimary: true }) + declare id: number + + @column() + declare name: string + + @column() + declare description: string + + @column() + declare model_type: string + + @column() + declare model_id: string + + @column() + declare event: string + + @column() + declare entity_type: string + + @column() + declare entity_id: string + + @column() + declare properties: Object + + @column() + declare batch_id: string + + @column.dateTime({ autoCreate: true }) + declare createdAt: DateTime + + @column.dateTime({ autoCreate: true, autoUpdate: true }) + declare updatedAt: DateTime + + static published = scope((query) => { + query.where('publishedOn', '<=', DateTime.utc().toSQLDate()) + }) +} + +const a = ActivityLog.query() +a.where('a', 4).withScopes((scopes) => scopes.published()) diff --git a/src/morph_map.ts b/src/morph_map.ts new file mode 100644 index 0000000..e03ade1 --- /dev/null +++ b/src/morph_map.ts @@ -0,0 +1,53 @@ +import { MorphInterface, MorphMapInterface } from './types.js' + +export default class MorphMap implements MorphInterface { + private _map: MorphMapInterface = {} + + private static _instance?: MorphMap + + static create() { + if (this._instance) { + return this._instance + } + + return new MorphMap() + } + + set(alias: string, target: any) { + this._map[alias] = target + } + + get(alias: string) { + if (!(alias in this._map)) { + throw new Error('morph map not found for ' + alias) + } + + return this._map[alias] || null + } + + has(alias: string) { + return alias in this._map + } + + hasTarget(target: any) { + const keys = Object.keys(this._map) + for (const key of keys) { + if (this._map[key] === target) { + return true + } + } + + return false + } + + getAlias(target: any) { + const keys = Object.keys(this._map) + for (const key of keys) { + if (target instanceof this._map[key] || target === this._map[key]) { + return key + } + } + + throw new Error('Target not found') + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..7202d84 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,20 @@ +import { LucidModel } from '@adonisjs/lucid/types/model' + +export interface LogModelInterface { + getModelId(): string | number +} + +// export type LogModel = InstanceType & LogModelInterface +export interface LogModel extends LucidModel, LogModelInterface {} + +export interface MorphMapInterface { + [key: string]: any +} + +export interface MorphInterface { + set(alias: string, target: any): void + get(alias: string): any + has(alias: string): boolean + hasTarget(target: any): boolean + getAlias(target: any): string +} diff --git a/stubs/migrations/create_db.stub b/stubs/migrations/create_db.stub new file mode 100644 index 0000000..ad24068 --- /dev/null +++ b/stubs/migrations/create_db.stub @@ -0,0 +1,36 @@ +{{{ + exports({ + to: app.makePath('database', 'migrations', prefix + '_create_role_permissions_table.ts') + }) +}}} +import { BaseSchema } from '@adonisjs/lucid/schema' + +export default class extends BaseSchema { + protected tableName = 'activity_logs' + + async up() { + this.schema.createTable(this.tableName, (table) => { + table.increments('id') + + table.string('name').nullable().index() + table.text('description') + table.string('model_type').nullable() + table.string('model_id').nullable() + table.string('event').nullable() + table.string('entity_type').nullable() + table.string('entity_id').nullable() + table.json('properties').nullable() + table.string('batch_id').nullable().index() + + table.index(['model_type', 'model_id']) + table.index(['entity_type', 'entity_id']) + + table.timestamp('created_at') + table.timestamp('updated_at') + }) + } + + async down() { + this.schema.dropTable(this.tableName) + } +}