@better-forge/audit 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 BetterStatusPage contributors
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 BetterStatusPage contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
- # @better-forge/audit
2
-
3
- Audit log: write, read, entity-type registry and routes.
4
-
5
- Status: skeleton. Depends on: `@better-forge/db`.
1
+ # @better-forge/audit
2
+
3
+ Audit log: write, read, entity-type registry and routes.
4
+
5
+ Status: skeleton. Depends on: `@better-forge/db`.
@@ -1,19 +1,19 @@
1
1
  /** The audit log. Steps run in this order. */
2
2
  export const auditMigrations = [
3
- { kind: 'exec', sql: `
4
- CREATE TABLE IF NOT EXISTS audit_log (
5
- id INTEGER PRIMARY KEY AUTOINCREMENT,
6
- user_id INTEGER NOT NULL,
7
- user_email TEXT NOT NULL,
8
- action TEXT NOT NULL,
9
- entity_type TEXT NOT NULL,
10
- entity_id TEXT,
11
- entity_name TEXT NOT NULL,
12
- diff TEXT,
13
- timestamp INTEGER NOT NULL
14
- );
15
- CREATE INDEX IF NOT EXISTS idx_audit_log_timestamp ON audit_log(timestamp DESC);
16
- CREATE INDEX IF NOT EXISTS idx_audit_log_user ON audit_log(user_id);
3
+ { kind: 'exec', sql: `
4
+ CREATE TABLE IF NOT EXISTS audit_log (
5
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
6
+ user_id INTEGER NOT NULL,
7
+ user_email TEXT NOT NULL,
8
+ action TEXT NOT NULL,
9
+ entity_type TEXT NOT NULL,
10
+ entity_id TEXT,
11
+ entity_name TEXT NOT NULL,
12
+ diff TEXT,
13
+ timestamp INTEGER NOT NULL
14
+ );
15
+ CREATE INDEX IF NOT EXISTS idx_audit_log_timestamp ON audit_log(timestamp DESC);
16
+ CREATE INDEX IF NOT EXISTS idx_audit_log_user ON audit_log(user_id);
17
17
  ` },
18
18
  ];
19
19
  //# sourceMappingURL=migrations.js.map
package/package.json CHANGED
@@ -1,54 +1,54 @@
1
- {
2
- "name": "@better-forge/audit",
3
- "version": "0.1.0",
4
- "description": "Audit log: write, read, entity-type registry and routes.",
5
- "license": "MIT",
6
- "repository": {
7
- "type": "git",
8
- "url": "git+https://github.com/BElluu/Better-Forge.git",
9
- "directory": "packages/audit"
10
- },
11
- "homepage": "https://github.com/BElluu/Better-Forge/tree/main/packages/audit#readme",
12
- "bugs": {
13
- "url": "https://github.com/BElluu/Better-Forge/issues"
14
- },
15
- "publishConfig": {
16
- "access": "public"
17
- },
18
- "type": "module",
19
- "main": "./dist/index.js",
20
- "types": "./dist/index.d.ts",
21
- "exports": {
22
- ".": {
23
- "types": "./dist/index.d.ts",
24
- "import": "./dist/index.js"
25
- },
26
- "./package.json": "./package.json"
27
- },
28
- "files": [
29
- "dist",
30
- "src",
31
- "!src/**/*.test.*"
32
- ],
33
- "scripts": {
34
- "build": "tsc -p tsconfig.json",
35
- "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput"
36
- },
37
- "peerDependencies": {
38
- "drizzle-orm": "^0.45.2",
39
- "fastify": "^5.3.2"
40
- },
41
- "devDependencies": {
42
- "@types/node": "^26.6.2",
43
- "drizzle-orm": "^0.45.2",
44
- "fastify": "^5.3.2",
45
- "typescript": "^5.7.3"
46
- },
47
- "dependencies": {
48
- "@better-forge/contracts": "0.1.0",
49
- "@better-forge/db": "0.1.0"
50
- },
51
- "engines": {
52
- "node": ">=26.10.0"
53
- }
54
- }
1
+ {
2
+ "name": "@better-forge/audit",
3
+ "version": "0.1.1",
4
+ "description": "Audit log: write, read, entity-type registry and routes.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/BElluu/Better-Forge.git",
9
+ "directory": "packages/audit"
10
+ },
11
+ "homepage": "https://github.com/BElluu/Better-Forge/tree/main/packages/audit#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/BElluu/Better-Forge/issues"
14
+ },
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "type": "module",
19
+ "main": "./dist/index.js",
20
+ "types": "./dist/index.d.ts",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "import": "./dist/index.js"
25
+ },
26
+ "./package.json": "./package.json"
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "src",
31
+ "!src/**/*.test.*"
32
+ ],
33
+ "scripts": {
34
+ "build": "tsc -p tsconfig.json",
35
+ "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput"
36
+ },
37
+ "peerDependencies": {
38
+ "drizzle-orm": "^0.45.2",
39
+ "fastify": "^5.3.2"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^26.6.2",
43
+ "drizzle-orm": "^0.45.2",
44
+ "fastify": "^5.3.2",
45
+ "typescript": "^5.7.3"
46
+ },
47
+ "dependencies": {
48
+ "@better-forge/contracts": "0.1.1",
49
+ "@better-forge/db": "0.1.1"
50
+ },
51
+ "engines": {
52
+ "node": ">=26.10.0"
53
+ }
54
+ }
package/src/audit.ts CHANGED
@@ -1,149 +1,149 @@
1
- import type { FastifyPluginAsync } from 'fastify'
2
- import { and, desc, eq, gte, like, lte, sql } from 'drizzle-orm'
3
- import type { AuditAction } from '@better-forge/contracts'
4
- import type { SqliteDatabase } from '@better-forge/db'
5
- import { parsePagination } from './pagination.js'
6
- import { auditLog } from './tables.js'
7
-
8
- /** Keys whose values never reach the log. */
9
- export const DEFAULT_SENSITIVE_KEYS = /(password|secret|token|authorization|cookie|credential|vaultconfig|encryptedvalue|recoverycode)/i
10
-
11
- export interface Actor {
12
- userId: number
13
- userEmail: string
14
- }
15
-
16
- export interface AuditOptions {
17
- /** The application's database handle. It is used lazily, so it may be opened after this is created. */
18
- db: SqliteDatabase
19
- /** Keys whose values are replaced by `[redacted]`. */
20
- sensitiveKeys?: RegExp
21
- }
22
-
23
- /** Audit log: writing entries, redacted diffs and the admin read route. All state is in the returned closure. */
24
- export function createAudit(options: AuditOptions) {
25
- const { db } = options
26
- const sensitiveKey = options.sensitiveKeys ?? DEFAULT_SENSITIVE_KEYS
27
-
28
- function redactValue(value: unknown): unknown {
29
- if (Array.isArray(value)) return value.map(redactValue)
30
- if (value && typeof value === 'object') return redactObj(value as Record<string, unknown>)
31
- return value
32
- }
33
-
34
- /** Mask sensitive keys in a flat object. */
35
- function redactObj(obj: Record<string, unknown>): Record<string, unknown> {
36
- const out: Record<string, unknown> = {}
37
- for (const [k, v] of Object.entries(obj)) {
38
- out[k] = sensitiveKey.test(k) ? '[redacted]' : redactValue(v)
39
- }
40
- return out
41
- }
42
-
43
- /**
44
- * Compute a field-level diff between two flat objects.
45
- * Only changed keys are included. Sensitive keys are masked.
46
- */
47
- function diffObjects(
48
- before: Record<string, unknown>,
49
- after: Record<string, unknown>,
50
- ): Record<string, { from: unknown; to: unknown }> {
51
- const diff: Record<string, { from: unknown; to: unknown }> = {}
52
- const keys = new Set([...Object.keys(before), ...Object.keys(after)])
53
- for (const key of keys) {
54
- if (JSON.stringify(before[key]) !== JSON.stringify(after[key])) {
55
- diff[key] = {
56
- from: sensitiveKey.test(key) ? '[redacted]' : redactValue(before[key]),
57
- to: sensitiveKey.test(key) ? '[redacted]' : redactValue(after[key]),
58
- }
59
- }
60
- }
61
- return diff
62
- }
63
-
64
- /** Snapshot of an entity for a create/delete entry. Sensitive keys masked. */
65
- function snapshot(obj: Record<string, unknown>): Record<string, unknown> {
66
- return redactObj(obj)
67
- }
68
-
69
- /** Audit actor for an authenticated identity; an API token appears under its own name, with user id 0 (no user). */
70
- function actor(identity: { userId: number; email: string }): Actor {
71
- return { userId: identity.userId, userEmail: identity.email }
72
- }
73
-
74
- /**
75
- * Write an audit log entry. Failures are swallowed so they never break the
76
- * primary operation.
77
- */
78
- async function write(
79
- who: Actor,
80
- action: AuditAction,
81
- entityType: string,
82
- entityId: number | string | null,
83
- entityName: string,
84
- diff?: Record<string, unknown>,
85
- ): Promise<void> {
86
- try {
87
- await db.insert(auditLog).values({
88
- userId: who.userId,
89
- userEmail: who.userEmail,
90
- action,
91
- entityType,
92
- entityId: entityId !== null ? String(entityId) : null,
93
- entityName,
94
- diff: diff ? JSON.stringify(redactObj(diff)) : null,
95
- timestamp: Date.now(),
96
- })
97
- } catch (err) {
98
- console.error('[audit] write failed:', err)
99
- }
100
- }
101
-
102
- /** `GET /`: the log, newest first, filterable. Register it under the prefix and the guards of your choice. */
103
- const routes: FastifyPluginAsync = async (app) => {
104
- app.get<{
105
- Querystring: {
106
- page?: string
107
- limit?: string
108
- userEmail?: string
109
- entityType?: string
110
- action?: string
111
- from?: string // unix ms
112
- to?: string // unix ms
113
- }
114
- }>('/', async (req) => {
115
- const { page, limit, offset } = parsePagination(req.query, { defaultLimit: 50, maxLimit: 100 })
116
-
117
- const conditions = []
118
- if (req.query.userEmail) conditions.push(like(auditLog.userEmail, `%${req.query.userEmail}%`))
119
- if (req.query.entityType) conditions.push(eq(auditLog.entityType, req.query.entityType))
120
- if (req.query.action) conditions.push(eq(auditLog.action, req.query.action))
121
- if (req.query.from) conditions.push(gte(auditLog.timestamp, Number(req.query.from)))
122
- if (req.query.to) conditions.push(lte(auditLog.timestamp, Number(req.query.to)))
123
-
124
- const where = conditions.length > 0 ? and(...conditions) : undefined
125
-
126
- const [entries, countResult] = await Promise.all([
127
- db.select().from(auditLog)
128
- .where(where)
129
- .orderBy(desc(auditLog.timestamp))
130
- .limit(limit)
131
- .offset(offset),
132
- db.select({ count: sql<number>`count(*)` }).from(auditLog).where(where),
133
- ])
134
-
135
- const total = countResult[0]?.count ?? 0
136
- return {
137
- entries: entries.map((e) => ({ ...e, diff: e.diff ? JSON.parse(e.diff) : null })),
138
- total,
139
- page,
140
- limit,
141
- pages: Math.ceil(total / limit),
142
- }
143
- })
144
- }
145
-
146
- return { write, diffObjects, snapshot, actor, routes }
147
- }
148
-
149
- export type Audit = ReturnType<typeof createAudit>
1
+ import type { FastifyPluginAsync } from 'fastify'
2
+ import { and, desc, eq, gte, like, lte, sql } from 'drizzle-orm'
3
+ import type { AuditAction } from '@better-forge/contracts'
4
+ import type { SqliteDatabase } from '@better-forge/db'
5
+ import { parsePagination } from './pagination.js'
6
+ import { auditLog } from './tables.js'
7
+
8
+ /** Keys whose values never reach the log. */
9
+ export const DEFAULT_SENSITIVE_KEYS = /(password|secret|token|authorization|cookie|credential|vaultconfig|encryptedvalue|recoverycode)/i
10
+
11
+ export interface Actor {
12
+ userId: number
13
+ userEmail: string
14
+ }
15
+
16
+ export interface AuditOptions {
17
+ /** The application's database handle. It is used lazily, so it may be opened after this is created. */
18
+ db: SqliteDatabase
19
+ /** Keys whose values are replaced by `[redacted]`. */
20
+ sensitiveKeys?: RegExp
21
+ }
22
+
23
+ /** Audit log: writing entries, redacted diffs and the admin read route. All state is in the returned closure. */
24
+ export function createAudit(options: AuditOptions) {
25
+ const { db } = options
26
+ const sensitiveKey = options.sensitiveKeys ?? DEFAULT_SENSITIVE_KEYS
27
+
28
+ function redactValue(value: unknown): unknown {
29
+ if (Array.isArray(value)) return value.map(redactValue)
30
+ if (value && typeof value === 'object') return redactObj(value as Record<string, unknown>)
31
+ return value
32
+ }
33
+
34
+ /** Mask sensitive keys in a flat object. */
35
+ function redactObj(obj: Record<string, unknown>): Record<string, unknown> {
36
+ const out: Record<string, unknown> = {}
37
+ for (const [k, v] of Object.entries(obj)) {
38
+ out[k] = sensitiveKey.test(k) ? '[redacted]' : redactValue(v)
39
+ }
40
+ return out
41
+ }
42
+
43
+ /**
44
+ * Compute a field-level diff between two flat objects.
45
+ * Only changed keys are included. Sensitive keys are masked.
46
+ */
47
+ function diffObjects(
48
+ before: Record<string, unknown>,
49
+ after: Record<string, unknown>,
50
+ ): Record<string, { from: unknown; to: unknown }> {
51
+ const diff: Record<string, { from: unknown; to: unknown }> = {}
52
+ const keys = new Set([...Object.keys(before), ...Object.keys(after)])
53
+ for (const key of keys) {
54
+ if (JSON.stringify(before[key]) !== JSON.stringify(after[key])) {
55
+ diff[key] = {
56
+ from: sensitiveKey.test(key) ? '[redacted]' : redactValue(before[key]),
57
+ to: sensitiveKey.test(key) ? '[redacted]' : redactValue(after[key]),
58
+ }
59
+ }
60
+ }
61
+ return diff
62
+ }
63
+
64
+ /** Snapshot of an entity for a create/delete entry. Sensitive keys masked. */
65
+ function snapshot(obj: Record<string, unknown>): Record<string, unknown> {
66
+ return redactObj(obj)
67
+ }
68
+
69
+ /** Audit actor for an authenticated identity; an API token appears under its own name, with user id 0 (no user). */
70
+ function actor(identity: { userId: number; email: string }): Actor {
71
+ return { userId: identity.userId, userEmail: identity.email }
72
+ }
73
+
74
+ /**
75
+ * Write an audit log entry. Failures are swallowed so they never break the
76
+ * primary operation.
77
+ */
78
+ async function write(
79
+ who: Actor,
80
+ action: AuditAction,
81
+ entityType: string,
82
+ entityId: number | string | null,
83
+ entityName: string,
84
+ diff?: Record<string, unknown>,
85
+ ): Promise<void> {
86
+ try {
87
+ await db.insert(auditLog).values({
88
+ userId: who.userId,
89
+ userEmail: who.userEmail,
90
+ action,
91
+ entityType,
92
+ entityId: entityId !== null ? String(entityId) : null,
93
+ entityName,
94
+ diff: diff ? JSON.stringify(redactObj(diff)) : null,
95
+ timestamp: Date.now(),
96
+ })
97
+ } catch (err) {
98
+ console.error('[audit] write failed:', err)
99
+ }
100
+ }
101
+
102
+ /** `GET /`: the log, newest first, filterable. Register it under the prefix and the guards of your choice. */
103
+ const routes: FastifyPluginAsync = async (app) => {
104
+ app.get<{
105
+ Querystring: {
106
+ page?: string
107
+ limit?: string
108
+ userEmail?: string
109
+ entityType?: string
110
+ action?: string
111
+ from?: string // unix ms
112
+ to?: string // unix ms
113
+ }
114
+ }>('/', async (req) => {
115
+ const { page, limit, offset } = parsePagination(req.query, { defaultLimit: 50, maxLimit: 100 })
116
+
117
+ const conditions = []
118
+ if (req.query.userEmail) conditions.push(like(auditLog.userEmail, `%${req.query.userEmail}%`))
119
+ if (req.query.entityType) conditions.push(eq(auditLog.entityType, req.query.entityType))
120
+ if (req.query.action) conditions.push(eq(auditLog.action, req.query.action))
121
+ if (req.query.from) conditions.push(gte(auditLog.timestamp, Number(req.query.from)))
122
+ if (req.query.to) conditions.push(lte(auditLog.timestamp, Number(req.query.to)))
123
+
124
+ const where = conditions.length > 0 ? and(...conditions) : undefined
125
+
126
+ const [entries, countResult] = await Promise.all([
127
+ db.select().from(auditLog)
128
+ .where(where)
129
+ .orderBy(desc(auditLog.timestamp))
130
+ .limit(limit)
131
+ .offset(offset),
132
+ db.select({ count: sql<number>`count(*)` }).from(auditLog).where(where),
133
+ ])
134
+
135
+ const total = countResult[0]?.count ?? 0
136
+ return {
137
+ entries: entries.map((e) => ({ ...e, diff: e.diff ? JSON.parse(e.diff) : null })),
138
+ total,
139
+ page,
140
+ limit,
141
+ pages: Math.ceil(total / limit),
142
+ }
143
+ })
144
+ }
145
+
146
+ return { write, diffObjects, snapshot, actor, routes }
147
+ }
148
+
149
+ export type Audit = ReturnType<typeof createAudit>
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { createAudit, DEFAULT_SENSITIVE_KEYS, type Actor, type Audit, type AuditOptions } from './audit.js'
2
- export { auditDbModule } from './module.js'
3
- export { auditLog } from './tables.js'
4
- export { parsePagination, type Pagination, type PaginationOptions } from './pagination.js'
1
+ export { createAudit, DEFAULT_SENSITIVE_KEYS, type Actor, type Audit, type AuditOptions } from './audit.js'
2
+ export { auditDbModule } from './module.js'
3
+ export { auditLog } from './tables.js'
4
+ export { parsePagination, type Pagination, type PaginationOptions } from './pagination.js'
package/src/migrations.ts CHANGED
@@ -1,20 +1,20 @@
1
- import type { MigrationStep } from '@better-forge/db'
2
-
3
- /** The audit log. Steps run in this order. */
4
- export const auditMigrations: readonly MigrationStep[] = [
5
- { kind: 'exec', sql: `
6
- CREATE TABLE IF NOT EXISTS audit_log (
7
- id INTEGER PRIMARY KEY AUTOINCREMENT,
8
- user_id INTEGER NOT NULL,
9
- user_email TEXT NOT NULL,
10
- action TEXT NOT NULL,
11
- entity_type TEXT NOT NULL,
12
- entity_id TEXT,
13
- entity_name TEXT NOT NULL,
14
- diff TEXT,
15
- timestamp INTEGER NOT NULL
16
- );
17
- CREATE INDEX IF NOT EXISTS idx_audit_log_timestamp ON audit_log(timestamp DESC);
18
- CREATE INDEX IF NOT EXISTS idx_audit_log_user ON audit_log(user_id);
19
- ` },
20
- ]
1
+ import type { MigrationStep } from '@better-forge/db'
2
+
3
+ /** The audit log. Steps run in this order. */
4
+ export const auditMigrations: readonly MigrationStep[] = [
5
+ { kind: 'exec', sql: `
6
+ CREATE TABLE IF NOT EXISTS audit_log (
7
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
8
+ user_id INTEGER NOT NULL,
9
+ user_email TEXT NOT NULL,
10
+ action TEXT NOT NULL,
11
+ entity_type TEXT NOT NULL,
12
+ entity_id TEXT,
13
+ entity_name TEXT NOT NULL,
14
+ diff TEXT,
15
+ timestamp INTEGER NOT NULL
16
+ );
17
+ CREATE INDEX IF NOT EXISTS idx_audit_log_timestamp ON audit_log(timestamp DESC);
18
+ CREATE INDEX IF NOT EXISTS idx_audit_log_user ON audit_log(user_id);
19
+ ` },
20
+ ]
package/src/module.ts CHANGED
@@ -1,6 +1,6 @@
1
- import type { DbModule } from '@better-forge/db'
2
- import * as tables from './tables.js'
3
- import { auditMigrations } from './migrations.js'
4
-
5
- /** The audit log table and its migrations, for the application's list of database modules. */
6
- export const auditDbModule = { id: 'audit', tables, migrations: auditMigrations } satisfies DbModule
1
+ import type { DbModule } from '@better-forge/db'
2
+ import * as tables from './tables.js'
3
+ import { auditMigrations } from './migrations.js'
4
+
5
+ /** The audit log table and its migrations, for the application's list of database modules. */
6
+ export const auditDbModule = { id: 'audit', tables, migrations: auditMigrations } satisfies DbModule
package/src/pagination.ts CHANGED
@@ -1,27 +1,27 @@
1
- export interface Pagination {
2
- page: number
3
- limit: number
4
- offset: number
5
- }
6
-
7
- export interface PaginationOptions {
8
- defaultLimit: number
9
- maxLimit: number
10
- /** Highest page accepted; keeps OFFSET within a sane range. */
11
- maxPage?: number
12
- }
13
-
14
- const DEFAULT_MAX_PAGE = 100_000
15
-
16
- /**
17
- * Lenient paging for admin lists: missing, non-numeric or out-of-range values fall back to the
18
- * default or are clamped into range, so a hand-edited URL never fails the page.
19
- */
20
- export function parsePagination(query: { page?: string; limit?: string }, options: PaginationOptions): Pagination {
21
- const maxPage = options.maxPage ?? DEFAULT_MAX_PAGE
22
- const rawPage = Math.floor(Number(query.page ?? 1)) || 1
23
- const rawLimit = Math.floor(Number(query.limit ?? options.defaultLimit)) || options.defaultLimit
24
- const page = Math.min(maxPage, Math.max(1, rawPage))
25
- const limit = Math.min(options.maxLimit, Math.max(1, rawLimit))
26
- return { page, limit, offset: (page - 1) * limit }
27
- }
1
+ export interface Pagination {
2
+ page: number
3
+ limit: number
4
+ offset: number
5
+ }
6
+
7
+ export interface PaginationOptions {
8
+ defaultLimit: number
9
+ maxLimit: number
10
+ /** Highest page accepted; keeps OFFSET within a sane range. */
11
+ maxPage?: number
12
+ }
13
+
14
+ const DEFAULT_MAX_PAGE = 100_000
15
+
16
+ /**
17
+ * Lenient paging for admin lists: missing, non-numeric or out-of-range values fall back to the
18
+ * default or are clamped into range, so a hand-edited URL never fails the page.
19
+ */
20
+ export function parsePagination(query: { page?: string; limit?: string }, options: PaginationOptions): Pagination {
21
+ const maxPage = options.maxPage ?? DEFAULT_MAX_PAGE
22
+ const rawPage = Math.floor(Number(query.page ?? 1)) || 1
23
+ const rawLimit = Math.floor(Number(query.limit ?? options.defaultLimit)) || options.defaultLimit
24
+ const page = Math.min(maxPage, Math.max(1, rawPage))
25
+ const limit = Math.min(options.maxLimit, Math.max(1, rawLimit))
26
+ return { page, limit, offset: (page - 1) * limit }
27
+ }
package/src/tables.ts CHANGED
@@ -1,13 +1,13 @@
1
- import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core'
2
-
3
- export const auditLog = sqliteTable('audit_log', {
4
- id: integer('id').primaryKey({ autoIncrement: true }),
5
- userId: integer('user_id').notNull(),
6
- userEmail: text('user_email').notNull(),
7
- action: text('action').notNull(), // 'create' | 'update' | 'delete'
8
- entityType: text('entity_type').notNull(),
9
- entityId: text('entity_id'),
10
- entityName: text('entity_name').notNull(),
11
- diff: text('diff'), // JSON | null
12
- timestamp: integer('timestamp').notNull(),
13
- })
1
+ import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core'
2
+
3
+ export const auditLog = sqliteTable('audit_log', {
4
+ id: integer('id').primaryKey({ autoIncrement: true }),
5
+ userId: integer('user_id').notNull(),
6
+ userEmail: text('user_email').notNull(),
7
+ action: text('action').notNull(), // 'create' | 'update' | 'delete'
8
+ entityType: text('entity_type').notNull(),
9
+ entityId: text('entity_id'),
10
+ entityName: text('entity_name').notNull(),
11
+ diff: text('diff'), // JSON | null
12
+ timestamp: integer('timestamp').notNull(),
13
+ })