@open-kingdom/shared-backend-data-access-activity-log

v0.0.2-17

Published

A NestJS module providing a polymorphic activity log — log any kind of activity (note, call, task, status-change, comment, …) against any kind of related record (contact, lead, project, ticket, …). Each entry has an owner, an optional due date, and an opt

Readme

@open-kingdom/shared-backend-data-access-activity-log

A NestJS module providing a polymorphic activity log — log any kind of activity (note, call, task, status-change, comment, …) against any kind of related record (contact, lead, project, ticket, …). Each entry has an owner, an optional due date, and an optional completion timestamp; completing an activity can append outcome notes to the existing description.

This library is domain-agnostic. The schema is fully generic — (related_type, related_id, type, subject, description, due_at, completed_at, owner_id) — and the host application supplies the vocabularies for related_type and type at module registration time via forRoot({ allowedRelatedTypes, allowedActivityTypes }). Unknown values are rejected with BadRequestException. When no allowed-types lists are supplied, any non-empty string is accepted (useful for tests).


Exports

| Export | Kind | Description | | -------------------------------------------------------------------------------------------------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | DataAccessActivityLogModule | class | Dynamic NestJS module. Use forRoot({ allowedRelatedTypes?, allowedActivityTypes? }) to constrain the accepted vocabulary; the bare module accepts any non-empty string. | | DataAccessActivityLogOptions | interface | { allowedRelatedTypes?, allowedActivityTypes?: ReadonlyArray<string> }. Both lists optional. | | ACTIVITY_LOG_OPTIONS | 'ACTIVITY_LOG_OPTIONS' | DI token for the options object. | | ActivityLogService | class | Injectable service for activity CRUD, completion, and per-owner / per-record queries. Exposes isAllowedRelatedType(value) and isAllowedActivityType(value) for callers that need to validate before calling create(). | | ActivityLogController | class | REST controller mounted at /activities. | | activityLog | BetterSQLite3Table | Drizzle table. | | ActivityLogTableName | 'activity_log' | String constant. | | ActivityLogEntry / NewActivityLogEntry | type | Inferred select/insert types. | | ActivityLogEntryDto, CreateActivityLogEntryDto, UpdateActivityLogEntryDto, CompleteActivityLogEntryDto | class | Swagger DTOs. relatedType and type are typed as string — runtime values constrained via forRoot(). |


Drizzle Schema

activity_log Table

| Column | Type | Constraints | Description | | -------------- | ------------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------- | | id | integer | Primary key, auto-increment | | | related_type | text | Not null | Polymorphic record kind. Validated at write time against allowedRelatedTypes (when supplied via forRoot()). | | related_id | integer | Not null | Primary key of the row in the related table. No foreign key — kept as a soft reference because the table varies. | | type | text | Not null | Activity kind. Validated at write time against allowedActivityTypes (when supplied via forRoot()). | | subject | text | Not null | Short headline rendered in lists. | | description | text | Nullable | Long-form body. complete() may append outcome notes to this field. | | due_at | integer timestamp | Nullable | Meaningful for task, call, meeting (per supportsDueDate() in crm-poly-util-domain). | | completed_at | integer timestamp | Nullable | Set by complete(). null means open. | | owner_id | integer | Not null, FK → users.id | | | created_at | integer timestamp | Not null, default now | | | updated_at | integer timestamp | Not null, default now | Bumped on every write. |

Indexes: activity_log_related_idx (related_type, related_id), activity_log_owner_idx (owner_id), activity_log_due_idx (due_at).


Module Registration

Dynamic module. The recommended form is forRoot() with the host's allowed-types vocabularies.

import { DataAccessActivityLogModule } from '@open-kingdom/shared-backend-data-access-activity-log';
import { ACTIVITY_TYPES, RELATED_ENTITY_TYPES } from '@open-kingdom/crm-poly-util-domain';

@Module({
  imports: [
    DataAccessActivityLogModule.forRoot({
      allowedRelatedTypes: RELATED_ENTITY_TYPES,
      allowedActivityTypes: ACTIVITY_TYPES,
    }),
  ],
})
export class SomeFeatureModule {}

Importing the bare module (no forRoot()) is supported and accepts any non-empty string — useful for tests and for hosts that haven't yet decided on a fixed vocabulary.

Add the table to the root schema composition:

import { activityLog } from '@open-kingdom/shared-backend-data-access-activity-log';

const schema = {
  // …
  activityLog,
};

Configuration

| Option | Type | Default | Description | | ---------------------- | ----------------------- | --------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | allowedRelatedTypes | ReadonlyArray<string> | null (any value accepted) | Whitelist of acceptable values for related_type. Unknown values rejected with BadRequestException from create(). | | allowedActivityTypes | ReadonlyArray<string> | null (any value accepted) | Whitelist of acceptable values for type. Unknown values rejected with BadRequestException from create(). |

The GET /activities?relatedType=…&relatedId=… endpoint also validates relatedType against the same list (rejecting with ForbiddenException) — querying by an unknown vocabulary is treated as an attempt to enumerate something the host hasn't permitted.

Database access resolves via the global DB_TAG token; no DI plumbing required.


ActivityLogService API

constructor(private activities: ActivityLogService) {}

| Method | Parameters | Returns | Description | | ----------------------- | --------------------------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | isAllowedRelatedType | value: string | boolean | true if value is in the configured allowedRelatedTypes list, or if no list was configured. | | isAllowedActivityType | value: string | boolean | true if value is in the configured allowedActivityTypes list, or if no list was configured. | | findForRecord | relatedType: string, relatedId: number | Promise<ActivityLogEntry[]> | All activities for one record, newest first (ordered by createdAt DESC). | | findOpenForOwner | ownerId: number | Promise<ActivityLogEntry[]> | All open (completed_at IS NULL) activities owned by ownerId, ordered by dueAt ASC then id ASC. | | findOverdueForOwner | ownerId: number, now?: Date | Promise<ActivityLogEntry[]> | Subset of open activities where due_at <= now. now defaults to new Date(). | | findById | id: number | Promise<ActivityLogEntry \| undefined> | Lookup by primary key. | | create | input: CreateActivityLogEntryDto, ownerId: number | Promise<ActivityLogEntry> | Inserts. Throws BadRequestException if relatedType or type are outside the configured allowed-types lists. | | update | id: number, input: UpdateActivityLogEntryDto | Promise<ActivityLogEntry> | Patches subject, description, dueAt. description: null clears the field; description: undefined leaves it untouched. | | complete | id: number, input?: CompleteActivityLogEntryDto | Promise<ActivityLogEntry> | Sets completed_at = now. If outcomeNotes is provided, appends it to the existing description separated by a blank line. | | delete | id: number | Promise<void> | Hard-delete. Throws NotFoundException if missing. |


REST Endpoints

All endpoints require authentication and an RBAC permission on the activities resource.

| Method | Path | Permission | Description | | -------- | -------------------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | GET | /activities | activities:read | List. With relatedType + relatedId query params: activities for that record. Without: open activities for the authenticated user (or scope=overdue for the overdue subset). | | GET | /activities/:id | activities:read | Get one. | | POST | /activities | activities:create | Create. Owner = authenticated user. | | PATCH | /activities/:id | activities:update | Patch. | | POST | /activities/:id/complete | activities:update | Mark complete. Body: { outcomeNotes?: string }. | | DELETE | /activities/:id | activities:delete | Delete. Returns 204 No Content. |


Usage Example

import { Injectable } from '@nestjs/common';
import { ActivityLogService } from '@open-kingdom/shared-backend-data-access-activity-log';

@Injectable()
export class OpportunityFollowupService {
  constructor(private readonly activities: ActivityLogService) {}

  async logFollowupCall(opportunityId: number, ownerId: number, dueIn: number) {
    return this.activities.create(
      {
        relatedType: 'opportunity',
        relatedId: opportunityId,
        type: 'call',
        subject: 'Follow-up call',
        description: 'Confirm pricing and align on next steps.',
        dueAt: new Date(Date.now() + dueIn),
      },
      ownerId
    );
  }
}

Testing

nx test data-access-activity-log