title: "Scaffold the log module and its resources"
context: |
Build a `log` domain: the append-only audit trail of every request and
exception the app handles — one flat row per request/response cycle (level,
message, acting user identity, HTTP context, exception + stack). The
`@ooneex/logger` package already defines the storage layer — `LogsEntity`
(table `app_logs`), `LogsRepository`, `LogsDatabase`, a migration, the
`LevelType`/`ELogLevel` levels, and `FindByCriteriaType`/`FindByResultType`
query contracts — align to those; this module's real work is the admin-only
read surface, not a new schema. Logs are written by the framework (logging
middleware) on each request, never created/edited/deleted via a public
endpoint, and are the most sensitive data in the system, so every read/list is
strictly admin-only with no owner-based access. If `modules/log/` already
exists, this work is void — do not run.
goal: |
Create the `log` module + needed resources, admin-only paginated/filterable read.
## Notes
- If `modules/log/` exists, STOP and report. Else `/module:create` log, then
build each resource via its `*:create` skill (`--module=log`), respecting
controllers → services → repositories → entities, registering all.
- Model `Log` against `@ooneex/logger`: mirror `LogsEntity` (table `app_logs`),
import every type/class from the package rather than re-declaring, and prefer
consuming its `LogsRepository`/`LogsDatabase` over hand-rolled persistence:
import {
type FindByCriteriaType,
type FindByResultType,
type LevelType,
ELogLevel,
LogsDatabase,
LogsEntity,
LogsRepository,
type RequestFileLogType,
} from "@ooneex/logger";
- Mind the level type split: the entity `level` column is `LevelType` (the
`${ELogLevel}` string union); the `FindByCriteriaType.level` filter is the
`ELogLevel` enum. Keep both as the package declares them; do not unify.
- Append-only — diverge from the standard shape: NO `updatedAt`, NO `deletedAt`,
NO update/delete endpoints. Retention/purge is an out-of-band admin job.
- Acting-user identity (`userId`, `email`, `firstName`, `lastName`) is a
denormalized snapshot of plain columns, NOT a `@ManyToOne(() => User)`, so the
record survives the user's rename/deletion and lives in a separate database.
- Own datasource: `LogsDatabase` reads `LOGS_DATABASE_URL` with
`synchronize: false`; do not register `Log` on the default datasource.
- Judge each resource; create the justified, skip the rest with a reason.
Defaults: entity + repository always (mirror `LogsEntity`/`LogsRepository`:
`create`, `find(id)`, paginated `findBy(criteria)` ordered `date DESC`,
honoring package indexes); service + controller for read-one and list/search
via `FindByCriteriaType` + pagination, both admin-only (no public
create/update/delete); permission always (admin-only on every read/list, no
owner check, reuse the permission service); migration only if applicable
(mirror the package migration on the logs datasource); queue a strong fit (log
write must not block the response — push onto a queue if one exists, else
write inline but never let logging break the request); event/translation/
storage/seed/workflow skip.
- Redact known credential fields (passwords, tokens, auth headers) before
persisting; keep sensitive fields admin-only.
- Throw typed exceptions (e.g. `LogNotFoundException`), never return null.
### Data Model
- `Log` is relation-free and flat — one row per request/response cycle, no
foreign keys; user identity is denormalized plain columns, never a `User`
relation, on its own database.
dod: |
- [ ] Aborts with a report if `modules/log/` exists
- [ ] `log` module created and registered into the app and `SharedModule`
- [ ] `Log` entity aligned to `LogsEntity` (table `app_logs`) with fields:
`level` (stored as `LevelType`; the `findBy` filter takes the `ELogLevel`
enum), `message` (nullable), `date`, `userId`/`email`/`firstName`/`lastName`
(nullable, denormalized), `status` (nullable), `exceptionName` (nullable),
`stackTrace` (jsonb, nullable), `ip` (nullable), `method` (`HttpMethodType`,
nullable), `path` (nullable), `version` (nullable), `userAgent`/`referer`
(nullable), `params`/`queries` (jsonb, nullable), `payload` (jsonb, nullable),
`host`/`lang` (nullable), `requestHeaders`/`cookies`/`form` (jsonb, nullable),
`files` (`RequestFileLogType[]`, jsonb, nullable), `responseHeaders`/
`responseData` (jsonb, nullable); NO `updatedAt`, NO `deletedAt`
- [ ] Logs created by the framework, read/searched by admins; `findBy` filters
by level, userId, email, names, status, exceptionName, method, path,
paginated and ordered `date DESC`
- [ ] Reading and listing both rejected for non-admins; no public
create/update/delete endpoint; identity denormalized, not a `User` relation
- [ ] Logs on their own `LOGS_DATABASE_URL` datasource, separate from the app DB
- [ ] Sensitive fields admin-only; known secrets redacted before persisting
- [ ] If queues are used, log writes run through a queue and never break the
request on failure
- [ ] Unneeded resources skipped and reported with a reason
- [ ] `bun run fmt`, `bun run lint`, `bun run test` pass from the root