@fonderie/audit 5.0.0 → 5.0.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/README.md +2 -2
- package/brain/outcomes.md +1 -1
- package/dist/index.cjs +22 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +22 -13
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ import type { IAuditQuery, IAuditPageDTO } from '@fonderie/audit';
|
|
|
30
30
|
You've shipped this plumbing before — auth, teams, billing, messaging —
|
|
31
31
|
and the next project will ask for it again. Fonderie packages it once:
|
|
32
32
|
plain TypeScript modules for
|
|
33
|
-
[`@fonderie/core`](https://github.com/
|
|
33
|
+
[`@fonderie/core`](https://github.com/fonderiejs/sdk/tree/main/packages/core),
|
|
34
34
|
PostgreSQL-backed, self-hosted, MIT. No external control plane, no
|
|
35
35
|
per-seat anything. Register the modules you need; skip the ones you don't.
|
|
36
36
|
|
|
@@ -38,7 +38,7 @@ per-seat anything. Register the modules you need; skip the ones you don't.
|
|
|
38
38
|
human-readable, workspace-scoped activity trail.
|
|
39
39
|
|
|
40
40
|
Browse the whole set at
|
|
41
|
-
[
|
|
41
|
+
[fonderiejs/sdk](https://github.com/fonderiejs/sdk) · follow
|
|
42
42
|
[@fonderiejs](https://x.com/fonderiejs)
|
|
43
43
|
|
|
44
44
|
## License
|
package/brain/outcomes.md
CHANGED
|
@@ -11,4 +11,4 @@ downloading tarballs.
|
|
|
11
11
|
|
|
12
12
|
| Method | Path | Middleware chain (auth / validation / handler) |
|
|
13
13
|
|---|---|---|
|
|
14
|
-
| GET | `/audit` | `requireAuth → async (ctx) => { if (!ctx.workspace) return setApiResponse( HTTP.UNPROCESSABLE, 'MISSING_WORKSPACE', 'Workspace context required', ); const url = new URL(ctx.request.url); const params = url.searchParams; const
|
|
14
|
+
| GET | `/audit` | `requireAuth → async (ctx) => { if (!ctx.workspace) return setApiResponse( HTTP.UNPROCESSABLE, 'MISSING_WORKSPACE', 'Workspace context required', ); const url = new URL(ctx.request.url); const params = url.searchParams; const rawLimit = Number(params.get('limit') ?? 50); const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.trunc(rawLimit), 1), 200) : 50; const type = params.get('type') ?? undefined; const actor = params.get('actorId') ?? undefined; const from = params.get('from') ? new Date(params.get('from')!) : undefined; const to = params.get('to') ? new Date(params.get('to')!) : undefined; const cursor = params.get('cursor') ?? undefined; const query: Parameters<AuditEventModel['list']>[0] = { workspaceId: ctx.workspace.id, limit, }; if (type) query.type = type; if (actor) query.actorId = actor; if (from) query.from = from; if (to) query.to = to; if (cursor) query.cursor = cursor; // The model over-fetches internally to detect a next page; the // cursor carries the row's created_at::text at full microsecond // precision so same-millisecond events can't be skipped. const { events, hasMore } = await new AuditEventModel(store).list(query); const lastEvent = events[events.length - 1]; const nextCursor = hasMore && lastEvent ? encodeCursor(lastEvent.createdAtRaw ?? lastEvent.createdAt, lastEvent.id) : null; return setApiResponse(HTTP.OK, 'AUDIT_FETCHED', 'Audit events retrieved.', { events: events.map(toAuditEventDTO), nextCursor, }); }` |
|
package/dist/index.cjs
CHANGED
|
@@ -39,17 +39,21 @@ function toAuditEventDTO(e) {
|
|
|
39
39
|
createdAt: e.createdAt.toISOString()
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
|
+
var CURSOR_TS_RE = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(\.\d{1,6})?(Z|[+-]\d{2}(:?\d{2})?)?$/;
|
|
43
|
+
var CURSOR_ID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
42
44
|
function encodeCursor(createdAt, id) {
|
|
43
|
-
|
|
45
|
+
const ts = typeof createdAt === "string" ? createdAt : createdAt.toISOString();
|
|
46
|
+
return Buffer.from(`${ts},${id}`).toString("base64");
|
|
44
47
|
}
|
|
45
48
|
function decodeCursor(cursor) {
|
|
49
|
+
if (cursor.length > 256) return null;
|
|
46
50
|
try {
|
|
47
51
|
const raw = Buffer.from(cursor, "base64").toString();
|
|
48
52
|
const commaIdx = raw.indexOf(",");
|
|
49
53
|
if (commaIdx === -1) return null;
|
|
50
54
|
const createdAt = raw.slice(0, commaIdx);
|
|
51
55
|
const id = raw.slice(commaIdx + 1);
|
|
52
|
-
if (!
|
|
56
|
+
if (!CURSOR_TS_RE.test(createdAt) || !CURSOR_ID_RE.test(id)) return null;
|
|
53
57
|
return { createdAt, id };
|
|
54
58
|
} catch {
|
|
55
59
|
return null;
|
|
@@ -63,6 +67,10 @@ var AuditEventModel = class {
|
|
|
63
67
|
this.store = store;
|
|
64
68
|
}
|
|
65
69
|
store;
|
|
70
|
+
// `query.limit` is the CLIENT-facing page size. The +1 over-fetch that
|
|
71
|
+
// detects a next page happens inside this method — keeping it here means
|
|
72
|
+
// no outer clamp can shave it off (which previously made nextCursor
|
|
73
|
+
// unreachable at the max page size).
|
|
66
74
|
async list(query) {
|
|
67
75
|
const limit = Math.min(query.limit ?? 50, MAX_LIMIT);
|
|
68
76
|
const params = [query.workspaceId];
|
|
@@ -92,15 +100,17 @@ var AuditEventModel = class {
|
|
|
92
100
|
);
|
|
93
101
|
}
|
|
94
102
|
}
|
|
95
|
-
params.push(limit);
|
|
96
|
-
|
|
97
|
-
`SELECT id, type, payload, meta, created_at as "createdAt"
|
|
103
|
+
params.push(limit + 1);
|
|
104
|
+
const rows = await this.store.query(
|
|
105
|
+
`SELECT id, type, payload, meta, created_at as "createdAt",
|
|
106
|
+
created_at::text as "createdAtRaw"
|
|
98
107
|
FROM fonderie_events
|
|
99
108
|
WHERE ${where.join(" AND ")}
|
|
100
109
|
ORDER BY created_at DESC, id DESC
|
|
101
110
|
LIMIT $${params.length}`,
|
|
102
111
|
params
|
|
103
112
|
);
|
|
113
|
+
return { events: rows.slice(0, limit), hasMore: rows.length > limit };
|
|
104
114
|
}
|
|
105
115
|
};
|
|
106
116
|
|
|
@@ -120,7 +130,8 @@ function buildAuditRoutes(store) {
|
|
|
120
130
|
);
|
|
121
131
|
const url = new URL(ctx.request.url);
|
|
122
132
|
const params = url.searchParams;
|
|
123
|
-
const
|
|
133
|
+
const rawLimit = Number(params.get("limit") ?? 50);
|
|
134
|
+
const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.trunc(rawLimit), 1), 200) : 50;
|
|
124
135
|
const type = params.get("type") ?? void 0;
|
|
125
136
|
const actor = params.get("actorId") ?? void 0;
|
|
126
137
|
const from = params.get("from") ? new Date(params.get("from")) : void 0;
|
|
@@ -128,20 +139,18 @@ function buildAuditRoutes(store) {
|
|
|
128
139
|
const cursor = params.get("cursor") ?? void 0;
|
|
129
140
|
const query = {
|
|
130
141
|
workspaceId: ctx.workspace.id,
|
|
131
|
-
limit
|
|
142
|
+
limit
|
|
132
143
|
};
|
|
133
144
|
if (type) query.type = type;
|
|
134
145
|
if (actor) query.actorId = actor;
|
|
135
146
|
if (from) query.from = from;
|
|
136
147
|
if (to) query.to = to;
|
|
137
148
|
if (cursor) query.cursor = cursor;
|
|
138
|
-
const events = await new AuditEventModel(store).list(query);
|
|
139
|
-
const
|
|
140
|
-
const
|
|
141
|
-
const lastEvent = page[page.length - 1];
|
|
142
|
-
const nextCursor = hasMore && lastEvent ? encodeCursor(lastEvent.createdAt, lastEvent.id) : null;
|
|
149
|
+
const { events, hasMore } = await new AuditEventModel(store).list(query);
|
|
150
|
+
const lastEvent = events[events.length - 1];
|
|
151
|
+
const nextCursor = hasMore && lastEvent ? encodeCursor(lastEvent.createdAtRaw ?? lastEvent.createdAt, lastEvent.id) : null;
|
|
143
152
|
return (0, import_core.setApiResponse)(import_core.HTTP.OK, "AUDIT_FETCHED", "Audit events retrieved.", {
|
|
144
|
-
events:
|
|
153
|
+
events: events.map(toAuditEventDTO),
|
|
145
154
|
nextCursor
|
|
146
155
|
});
|
|
147
156
|
}
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/routes.ts","../src/dtos/audit.ts","../src/models/event.model.ts","../src/module.ts"],"sourcesContent":["export { AuditModule } from './module';\nexport type { IAuditEvent, IAuditQuery } from './types';\nexport type { IAuditEventDTO, IAuditPageDTO } from './dtos/audit';\n","import type { Middleware } from '@fonderie/core';\nimport { setApiResponse, HTTP } from '@fonderie/core';\nimport { requireAuth } from '@fonderie/core/middlewares';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { AuditEventModel } from './models/event.model';\nimport { toAuditEventDTO, encodeCursor } from './dtos/audit';\n\ntype Route = [string, string, ...Middleware[]];\n\nexport function buildAuditRoutes(store: IStoreAdapter): Route[] {\n\treturn [\n\t\t[\n\t\t\t'GET',\n\t\t\t'/audit',\n\t\t\trequireAuth,\n\t\t\tasync (ctx) => {\n\t\t\t\tif (!ctx.workspace)\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t\t'MISSING_WORKSPACE',\n\t\t\t\t\t\t'Workspace context required',\n\t\t\t\t\t);\n\n\t\t\t\tconst url = new URL(ctx.request.url);\n\t\t\t\tconst params = url.searchParams;\n\n\t\t\t\tconst limit = Math.min(Number(params.get('limit') ?? 50), 200);\n\t\t\t\tconst type = params.get('type') ?? undefined;\n\t\t\t\tconst actor = params.get('actorId') ?? undefined;\n\t\t\t\tconst from = params.get('from') ? new Date(params.get('from')!) : undefined;\n\t\t\t\tconst to = params.get('to') ? new Date(params.get('to')!) : undefined;\n\t\t\t\tconst cursor = params.get('cursor') ?? undefined;\n\n\t\t\t\tconst query: Parameters<AuditEventModel['list']>[0] = {\n\t\t\t\t\tworkspaceId: ctx.workspace.id,\n\t\t\t\t\tlimit: limit + 1,\n\t\t\t\t};\n\t\t\t\tif (type) query.type = type;\n\t\t\t\tif (actor) query.actorId = actor;\n\t\t\t\tif (from) query.from = from;\n\t\t\t\tif (to) query.to = to;\n\t\t\t\tif (cursor) query.cursor = cursor;\n\n\t\t\t\tconst events = await new AuditEventModel(store).list(query);\n\n\t\t\t\t// fetch one extra to determine if there's a next page\n\t\t\t\tconst hasMore = events.length > limit;\n\t\t\t\tconst page = hasMore ? events.slice(0, limit) : events;\n\t\t\t\tconst lastEvent = page[page.length - 1];\n\t\t\t\tconst nextCursor =\n\t\t\t\t\thasMore && lastEvent ? encodeCursor(lastEvent.createdAt, lastEvent.id) : null;\n\n\t\t\t\treturn setApiResponse(HTTP.OK, 'AUDIT_FETCHED', 'Audit events retrieved.', {\n\t\t\t\t\tevents: page.map(toAuditEventDTO),\n\t\t\t\t\tnextCursor,\n\t\t\t\t});\n\t\t\t},\n\t\t],\n\t];\n}\n","import type { IAuditEvent } from '../types';\n\nexport interface IAuditEventDTO {\n\tid: string;\n\ttype: string;\n\tactorId: string | null;\n\trequestId: string | null;\n\tpayload: Record<string, unknown>;\n\tcreatedAt: string;\n}\n\nexport interface IAuditPageDTO {\n\tevents: IAuditEventDTO[];\n\tnextCursor: string | null;\n}\n\nexport function toAuditEventDTO(e: IAuditEvent): IAuditEventDTO {\n\treturn {\n\t\tid: e.id,\n\t\ttype: e.type,\n\t\tactorId: (e.payload['userId'] as string | undefined) ?? null,\n\t\trequestId: (e.meta['requestId'] as string | undefined) ?? null,\n\t\tpayload: e.payload,\n\t\tcreatedAt: e.createdAt.toISOString(),\n\t};\n}\n\nexport function encodeCursor(createdAt: Date, id: string): string {\n\treturn Buffer.from(`${createdAt.toISOString()},${id}`).toString('base64');\n}\n\nexport function decodeCursor(cursor: string): { createdAt: string; id: string } | null {\n\ttry {\n\t\tconst raw = Buffer.from(cursor, 'base64').toString();\n\t\tconst commaIdx = raw.indexOf(',');\n\t\tif (commaIdx === -1) return null;\n\t\tconst createdAt = raw.slice(0, commaIdx);\n\t\tconst id = raw.slice(commaIdx + 1);\n\t\tif (!id || isNaN(Date.parse(createdAt))) return null;\n\t\treturn { createdAt, id };\n\t} catch {\n\t\treturn null;\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IAuditEvent, IAuditQuery } from '../types';\nimport { decodeCursor } from '../dtos/audit';\n\nconst MAX_LIMIT = 200;\n\nexport class AuditEventModel {\n\tconstructor(private readonly store: IStoreAdapter) {}\n\n\tasync list(query: IAuditQuery): Promise<IAuditEvent[]> {\n\t\tconst limit = Math.min(query.limit ?? 50, MAX_LIMIT);\n\t\tconst params: unknown[] = [query.workspaceId];\n\t\tconst where: string[] = [`payload->>'workspaceId' = $1`];\n\n\t\tif (query.type) {\n\t\t\tparams.push(query.type);\n\t\t\twhere.push(`type = $${params.length}`);\n\t\t}\n\n\t\tif (query.actorId) {\n\t\t\tparams.push(query.actorId);\n\t\t\twhere.push(`payload->>'userId' = $${params.length}`);\n\t\t}\n\n\t\tif (query.from) {\n\t\t\tparams.push(query.from);\n\t\t\twhere.push(`created_at >= $${params.length}`);\n\t\t}\n\n\t\tif (query.to) {\n\t\t\tparams.push(query.to);\n\t\t\twhere.push(`created_at <= $${params.length}`);\n\t\t}\n\n\t\tif (query.cursor) {\n\t\t\tconst decoded = decodeCursor(query.cursor);\n\t\t\tif (decoded) {\n\t\t\t\tparams.push(decoded.createdAt, decoded.id);\n\t\t\t\twhere.push(\n\t\t\t\t\t`(created_at, id) < ($${params.length - 1}::timestamptz, $${params.length}::uuid)`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tparams.push(limit);\n\n\t\treturn this.store.query<IAuditEvent>(\n\t\t\t`SELECT id, type, payload, meta, created_at as \"createdAt\"\n\t\t\t FROM fonderie_events\n\t\t\t WHERE ${where.join(' AND ')}\n\t\t\t ORDER BY created_at DESC, id DESC\n\t\t\t LIMIT $${params.length}`,\n\t\t\tparams,\n\t\t);\n\t}\n}\n","import type { IFonderieModule, IFonderieApp } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { buildAuditRoutes } from './routes';\n\nexport class AuditModule implements IFonderieModule {\n\treadonly name = '@fonderie/audit';\n\treadonly deps = ['@fonderie/auth', '@fonderie/workspaces'];\n\n\tconstructor(private readonly store: IStoreAdapter) {}\n\n\tinstall(app: IFonderieApp): void {\n\t\tfor (const [method, path, ...handlers] of buildAuditRoutes(this.store)) {\n\t\t\tapp.addRoute(method, path, ...handlers);\n\t\t}\n\t}\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/routes.ts","../src/dtos/audit.ts","../src/models/event.model.ts","../src/module.ts"],"sourcesContent":["export { AuditModule } from './module';\nexport type { IAuditEvent, IAuditQuery } from './types';\nexport type { IAuditEventDTO, IAuditPageDTO } from './dtos/audit';\n","import type { Middleware } from '@fonderie/core';\nimport { setApiResponse, HTTP } from '@fonderie/core';\nimport { requireAuth } from '@fonderie/core/middlewares';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { AuditEventModel } from './models/event.model';\nimport { toAuditEventDTO, encodeCursor } from './dtos/audit';\n\ntype Route = [string, string, ...Middleware[]];\n\nexport function buildAuditRoutes(store: IStoreAdapter): Route[] {\n\treturn [\n\t\t[\n\t\t\t'GET',\n\t\t\t'/audit',\n\t\t\trequireAuth,\n\t\t\tasync (ctx) => {\n\t\t\t\tif (!ctx.workspace)\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t\t'MISSING_WORKSPACE',\n\t\t\t\t\t\t'Workspace context required',\n\t\t\t\t\t);\n\n\t\t\t\tconst url = new URL(ctx.request.url);\n\t\t\t\tconst params = url.searchParams;\n\n\t\t\t\tconst rawLimit = Number(params.get('limit') ?? 50);\n\t\t\t\tconst limit = Number.isFinite(rawLimit)\n\t\t\t\t\t? Math.min(Math.max(Math.trunc(rawLimit), 1), 200)\n\t\t\t\t\t: 50;\n\t\t\t\tconst type = params.get('type') ?? undefined;\n\t\t\t\tconst actor = params.get('actorId') ?? undefined;\n\t\t\t\tconst from = params.get('from') ? new Date(params.get('from')!) : undefined;\n\t\t\t\tconst to = params.get('to') ? new Date(params.get('to')!) : undefined;\n\t\t\t\tconst cursor = params.get('cursor') ?? undefined;\n\n\t\t\t\tconst query: Parameters<AuditEventModel['list']>[0] = {\n\t\t\t\t\tworkspaceId: ctx.workspace.id,\n\t\t\t\t\tlimit,\n\t\t\t\t};\n\t\t\t\tif (type) query.type = type;\n\t\t\t\tif (actor) query.actorId = actor;\n\t\t\t\tif (from) query.from = from;\n\t\t\t\tif (to) query.to = to;\n\t\t\t\tif (cursor) query.cursor = cursor;\n\n\t\t\t\t// The model over-fetches internally to detect a next page; the\n\t\t\t\t// cursor carries the row's created_at::text at full microsecond\n\t\t\t\t// precision so same-millisecond events can't be skipped.\n\t\t\t\tconst { events, hasMore } = await new AuditEventModel(store).list(query);\n\t\t\t\tconst lastEvent = events[events.length - 1];\n\t\t\t\tconst nextCursor =\n\t\t\t\t\thasMore && lastEvent\n\t\t\t\t\t\t? encodeCursor(lastEvent.createdAtRaw ?? lastEvent.createdAt, lastEvent.id)\n\t\t\t\t\t\t: null;\n\n\t\t\t\treturn setApiResponse(HTTP.OK, 'AUDIT_FETCHED', 'Audit events retrieved.', {\n\t\t\t\t\tevents: events.map(toAuditEventDTO),\n\t\t\t\t\tnextCursor,\n\t\t\t\t});\n\t\t\t},\n\t\t],\n\t];\n}\n","import type { IAuditEvent } from '../types';\n\nexport interface IAuditEventDTO {\n\tid: string;\n\ttype: string;\n\tactorId: string | null;\n\trequestId: string | null;\n\tpayload: Record<string, unknown>;\n\tcreatedAt: string;\n}\n\nexport interface IAuditPageDTO {\n\tevents: IAuditEventDTO[];\n\tnextCursor: string | null;\n}\n\nexport function toAuditEventDTO(e: IAuditEvent): IAuditEventDTO {\n\treturn {\n\t\tid: e.id,\n\t\ttype: e.type,\n\t\tactorId: (e.payload['userId'] as string | undefined) ?? null,\n\t\trequestId: (e.meta['requestId'] as string | undefined) ?? null,\n\t\tpayload: e.payload,\n\t\tcreatedAt: e.createdAt.toISOString(),\n\t};\n}\n\n// Cursors accept ISO timestamps and Postgres' own text format (microsecond\n// precision, e.g. '2026-09-05 00:38:31.123456+00') — the keyset carries the\n// latter, because round-tripping through a millisecond JS Date truncates\n// sub-millisecond digits and silently skips same-millisecond events (rows\n// created in one transaction all share now() exactly) between pages.\nconst CURSOR_TS_RE = /^\\d{4}-\\d{2}-\\d{2}[T ]\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,6})?(Z|[+-]\\d{2}(:?\\d{2})?)?$/;\nconst CURSOR_ID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;\n\nexport function encodeCursor(createdAt: Date | string, id: string): string {\n\tconst ts = typeof createdAt === 'string' ? createdAt : createdAt.toISOString();\n\treturn Buffer.from(`${ts},${id}`).toString('base64');\n}\n\nexport function decodeCursor(cursor: string): { createdAt: string; id: string } | null {\n\tif (cursor.length > 256) return null;\n\ttry {\n\t\tconst raw = Buffer.from(cursor, 'base64').toString();\n\t\tconst commaIdx = raw.indexOf(',');\n\t\tif (commaIdx === -1) return null;\n\t\tconst createdAt = raw.slice(0, commaIdx);\n\t\tconst id = raw.slice(commaIdx + 1);\n\t\t// Both halves feed ::timestamptz / ::uuid casts — validate here so a\n\t\t// crafted cursor yields an empty page, not a Postgres cast error.\n\t\tif (!CURSOR_TS_RE.test(createdAt) || !CURSOR_ID_RE.test(id)) return null;\n\t\treturn { createdAt, id };\n\t} catch {\n\t\treturn null;\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IAuditEvent, IAuditQuery } from '../types';\nimport { decodeCursor } from '../dtos/audit';\n\nconst MAX_LIMIT = 200;\n\nexport type IAuditEventRow = IAuditEvent & {\n\t// created_at::text — full microsecond precision for the keyset cursor\n\t// (node-pg parses timestamptz into a millisecond Date, which would make\n\t// the cursor skip same-millisecond rows between pages).\n\tcreatedAtRaw: string;\n};\n\nexport interface IAuditEventPage {\n\tevents: IAuditEventRow[];\n\thasMore: boolean;\n}\n\nexport class AuditEventModel {\n\tconstructor(private readonly store: IStoreAdapter) {}\n\n\t// `query.limit` is the CLIENT-facing page size. The +1 over-fetch that\n\t// detects a next page happens inside this method — keeping it here means\n\t// no outer clamp can shave it off (which previously made nextCursor\n\t// unreachable at the max page size).\n\tasync list(query: IAuditQuery): Promise<IAuditEventPage> {\n\t\tconst limit = Math.min(query.limit ?? 50, MAX_LIMIT);\n\t\tconst params: unknown[] = [query.workspaceId];\n\t\tconst where: string[] = [`payload->>'workspaceId' = $1`];\n\n\t\tif (query.type) {\n\t\t\tparams.push(query.type);\n\t\t\twhere.push(`type = $${params.length}`);\n\t\t}\n\n\t\tif (query.actorId) {\n\t\t\tparams.push(query.actorId);\n\t\t\twhere.push(`payload->>'userId' = $${params.length}`);\n\t\t}\n\n\t\tif (query.from) {\n\t\t\tparams.push(query.from);\n\t\t\twhere.push(`created_at >= $${params.length}`);\n\t\t}\n\n\t\tif (query.to) {\n\t\t\tparams.push(query.to);\n\t\t\twhere.push(`created_at <= $${params.length}`);\n\t\t}\n\n\t\tif (query.cursor) {\n\t\t\tconst decoded = decodeCursor(query.cursor);\n\t\t\tif (decoded) {\n\t\t\t\tparams.push(decoded.createdAt, decoded.id);\n\t\t\t\twhere.push(\n\t\t\t\t\t`(created_at, id) < ($${params.length - 1}::timestamptz, $${params.length}::uuid)`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tparams.push(limit + 1);\n\n\t\tconst rows = await this.store.query<IAuditEventRow>(\n\t\t\t`SELECT id, type, payload, meta, created_at as \"createdAt\",\n\t\t\t created_at::text as \"createdAtRaw\"\n\t\t\t FROM fonderie_events\n\t\t\t WHERE ${where.join(' AND ')}\n\t\t\t ORDER BY created_at DESC, id DESC\n\t\t\t LIMIT $${params.length}`,\n\t\t\tparams,\n\t\t);\n\n\t\treturn { events: rows.slice(0, limit), hasMore: rows.length > limit };\n\t}\n}\n","import type { IFonderieModule, IFonderieApp } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { buildAuditRoutes } from './routes';\n\nexport class AuditModule implements IFonderieModule {\n\treadonly name = '@fonderie/audit';\n\treadonly deps = ['@fonderie/auth', '@fonderie/workspaces'];\n\n\tconstructor(private readonly store: IStoreAdapter) {}\n\n\tinstall(app: IFonderieApp): void {\n\t\tfor (const [method, path, ...handlers] of buildAuditRoutes(this.store)) {\n\t\t\tapp.addRoute(method, path, ...handlers);\n\t\t}\n\t}\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -13,17 +13,21 @@ function toAuditEventDTO(e) {
|
|
|
13
13
|
createdAt: e.createdAt.toISOString()
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
|
+
var CURSOR_TS_RE = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(\.\d{1,6})?(Z|[+-]\d{2}(:?\d{2})?)?$/;
|
|
17
|
+
var CURSOR_ID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
16
18
|
function encodeCursor(createdAt, id) {
|
|
17
|
-
|
|
19
|
+
const ts = typeof createdAt === "string" ? createdAt : createdAt.toISOString();
|
|
20
|
+
return Buffer.from(`${ts},${id}`).toString("base64");
|
|
18
21
|
}
|
|
19
22
|
function decodeCursor(cursor) {
|
|
23
|
+
if (cursor.length > 256) return null;
|
|
20
24
|
try {
|
|
21
25
|
const raw = Buffer.from(cursor, "base64").toString();
|
|
22
26
|
const commaIdx = raw.indexOf(",");
|
|
23
27
|
if (commaIdx === -1) return null;
|
|
24
28
|
const createdAt = raw.slice(0, commaIdx);
|
|
25
29
|
const id = raw.slice(commaIdx + 1);
|
|
26
|
-
if (!
|
|
30
|
+
if (!CURSOR_TS_RE.test(createdAt) || !CURSOR_ID_RE.test(id)) return null;
|
|
27
31
|
return { createdAt, id };
|
|
28
32
|
} catch {
|
|
29
33
|
return null;
|
|
@@ -37,6 +41,10 @@ var AuditEventModel = class {
|
|
|
37
41
|
this.store = store;
|
|
38
42
|
}
|
|
39
43
|
store;
|
|
44
|
+
// `query.limit` is the CLIENT-facing page size. The +1 over-fetch that
|
|
45
|
+
// detects a next page happens inside this method — keeping it here means
|
|
46
|
+
// no outer clamp can shave it off (which previously made nextCursor
|
|
47
|
+
// unreachable at the max page size).
|
|
40
48
|
async list(query) {
|
|
41
49
|
const limit = Math.min(query.limit ?? 50, MAX_LIMIT);
|
|
42
50
|
const params = [query.workspaceId];
|
|
@@ -66,15 +74,17 @@ var AuditEventModel = class {
|
|
|
66
74
|
);
|
|
67
75
|
}
|
|
68
76
|
}
|
|
69
|
-
params.push(limit);
|
|
70
|
-
|
|
71
|
-
`SELECT id, type, payload, meta, created_at as "createdAt"
|
|
77
|
+
params.push(limit + 1);
|
|
78
|
+
const rows = await this.store.query(
|
|
79
|
+
`SELECT id, type, payload, meta, created_at as "createdAt",
|
|
80
|
+
created_at::text as "createdAtRaw"
|
|
72
81
|
FROM fonderie_events
|
|
73
82
|
WHERE ${where.join(" AND ")}
|
|
74
83
|
ORDER BY created_at DESC, id DESC
|
|
75
84
|
LIMIT $${params.length}`,
|
|
76
85
|
params
|
|
77
86
|
);
|
|
87
|
+
return { events: rows.slice(0, limit), hasMore: rows.length > limit };
|
|
78
88
|
}
|
|
79
89
|
};
|
|
80
90
|
|
|
@@ -94,7 +104,8 @@ function buildAuditRoutes(store) {
|
|
|
94
104
|
);
|
|
95
105
|
const url = new URL(ctx.request.url);
|
|
96
106
|
const params = url.searchParams;
|
|
97
|
-
const
|
|
107
|
+
const rawLimit = Number(params.get("limit") ?? 50);
|
|
108
|
+
const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.trunc(rawLimit), 1), 200) : 50;
|
|
98
109
|
const type = params.get("type") ?? void 0;
|
|
99
110
|
const actor = params.get("actorId") ?? void 0;
|
|
100
111
|
const from = params.get("from") ? new Date(params.get("from")) : void 0;
|
|
@@ -102,20 +113,18 @@ function buildAuditRoutes(store) {
|
|
|
102
113
|
const cursor = params.get("cursor") ?? void 0;
|
|
103
114
|
const query = {
|
|
104
115
|
workspaceId: ctx.workspace.id,
|
|
105
|
-
limit
|
|
116
|
+
limit
|
|
106
117
|
};
|
|
107
118
|
if (type) query.type = type;
|
|
108
119
|
if (actor) query.actorId = actor;
|
|
109
120
|
if (from) query.from = from;
|
|
110
121
|
if (to) query.to = to;
|
|
111
122
|
if (cursor) query.cursor = cursor;
|
|
112
|
-
const events = await new AuditEventModel(store).list(query);
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
const lastEvent = page[page.length - 1];
|
|
116
|
-
const nextCursor = hasMore && lastEvent ? encodeCursor(lastEvent.createdAt, lastEvent.id) : null;
|
|
123
|
+
const { events, hasMore } = await new AuditEventModel(store).list(query);
|
|
124
|
+
const lastEvent = events[events.length - 1];
|
|
125
|
+
const nextCursor = hasMore && lastEvent ? encodeCursor(lastEvent.createdAtRaw ?? lastEvent.createdAt, lastEvent.id) : null;
|
|
117
126
|
return setApiResponse(HTTP.OK, "AUDIT_FETCHED", "Audit events retrieved.", {
|
|
118
|
-
events:
|
|
127
|
+
events: events.map(toAuditEventDTO),
|
|
119
128
|
nextCursor
|
|
120
129
|
});
|
|
121
130
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/routes.ts","../src/dtos/audit.ts","../src/models/event.model.ts","../src/module.ts"],"sourcesContent":["import type { Middleware } from '@fonderie/core';\nimport { setApiResponse, HTTP } from '@fonderie/core';\nimport { requireAuth } from '@fonderie/core/middlewares';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { AuditEventModel } from './models/event.model';\nimport { toAuditEventDTO, encodeCursor } from './dtos/audit';\n\ntype Route = [string, string, ...Middleware[]];\n\nexport function buildAuditRoutes(store: IStoreAdapter): Route[] {\n\treturn [\n\t\t[\n\t\t\t'GET',\n\t\t\t'/audit',\n\t\t\trequireAuth,\n\t\t\tasync (ctx) => {\n\t\t\t\tif (!ctx.workspace)\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t\t'MISSING_WORKSPACE',\n\t\t\t\t\t\t'Workspace context required',\n\t\t\t\t\t);\n\n\t\t\t\tconst url = new URL(ctx.request.url);\n\t\t\t\tconst params = url.searchParams;\n\n\t\t\t\tconst limit = Math.min(Number(params.get('limit') ?? 50), 200);\n\t\t\t\tconst type = params.get('type') ?? undefined;\n\t\t\t\tconst actor = params.get('actorId') ?? undefined;\n\t\t\t\tconst from = params.get('from') ? new Date(params.get('from')!) : undefined;\n\t\t\t\tconst to = params.get('to') ? new Date(params.get('to')!) : undefined;\n\t\t\t\tconst cursor = params.get('cursor') ?? undefined;\n\n\t\t\t\tconst query: Parameters<AuditEventModel['list']>[0] = {\n\t\t\t\t\tworkspaceId: ctx.workspace.id,\n\t\t\t\t\tlimit: limit + 1,\n\t\t\t\t};\n\t\t\t\tif (type) query.type = type;\n\t\t\t\tif (actor) query.actorId = actor;\n\t\t\t\tif (from) query.from = from;\n\t\t\t\tif (to) query.to = to;\n\t\t\t\tif (cursor) query.cursor = cursor;\n\n\t\t\t\tconst events = await new AuditEventModel(store).list(query);\n\n\t\t\t\t// fetch one extra to determine if there's a next page\n\t\t\t\tconst hasMore = events.length > limit;\n\t\t\t\tconst page = hasMore ? events.slice(0, limit) : events;\n\t\t\t\tconst lastEvent = page[page.length - 1];\n\t\t\t\tconst nextCursor =\n\t\t\t\t\thasMore && lastEvent ? encodeCursor(lastEvent.createdAt, lastEvent.id) : null;\n\n\t\t\t\treturn setApiResponse(HTTP.OK, 'AUDIT_FETCHED', 'Audit events retrieved.', {\n\t\t\t\t\tevents: page.map(toAuditEventDTO),\n\t\t\t\t\tnextCursor,\n\t\t\t\t});\n\t\t\t},\n\t\t],\n\t];\n}\n","import type { IAuditEvent } from '../types';\n\nexport interface IAuditEventDTO {\n\tid: string;\n\ttype: string;\n\tactorId: string | null;\n\trequestId: string | null;\n\tpayload: Record<string, unknown>;\n\tcreatedAt: string;\n}\n\nexport interface IAuditPageDTO {\n\tevents: IAuditEventDTO[];\n\tnextCursor: string | null;\n}\n\nexport function toAuditEventDTO(e: IAuditEvent): IAuditEventDTO {\n\treturn {\n\t\tid: e.id,\n\t\ttype: e.type,\n\t\tactorId: (e.payload['userId'] as string | undefined) ?? null,\n\t\trequestId: (e.meta['requestId'] as string | undefined) ?? null,\n\t\tpayload: e.payload,\n\t\tcreatedAt: e.createdAt.toISOString(),\n\t};\n}\n\nexport function encodeCursor(createdAt: Date, id: string): string {\n\treturn Buffer.from(`${createdAt.toISOString()},${id}`).toString('base64');\n}\n\nexport function decodeCursor(cursor: string): { createdAt: string; id: string } | null {\n\ttry {\n\t\tconst raw = Buffer.from(cursor, 'base64').toString();\n\t\tconst commaIdx = raw.indexOf(',');\n\t\tif (commaIdx === -1) return null;\n\t\tconst createdAt = raw.slice(0, commaIdx);\n\t\tconst id = raw.slice(commaIdx + 1);\n\t\tif (!id || isNaN(Date.parse(createdAt))) return null;\n\t\treturn { createdAt, id };\n\t} catch {\n\t\treturn null;\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IAuditEvent, IAuditQuery } from '../types';\nimport { decodeCursor } from '../dtos/audit';\n\nconst MAX_LIMIT = 200;\n\nexport class AuditEventModel {\n\tconstructor(private readonly store: IStoreAdapter) {}\n\n\tasync list(query: IAuditQuery): Promise<IAuditEvent[]> {\n\t\tconst limit = Math.min(query.limit ?? 50, MAX_LIMIT);\n\t\tconst params: unknown[] = [query.workspaceId];\n\t\tconst where: string[] = [`payload->>'workspaceId' = $1`];\n\n\t\tif (query.type) {\n\t\t\tparams.push(query.type);\n\t\t\twhere.push(`type = $${params.length}`);\n\t\t}\n\n\t\tif (query.actorId) {\n\t\t\tparams.push(query.actorId);\n\t\t\twhere.push(`payload->>'userId' = $${params.length}`);\n\t\t}\n\n\t\tif (query.from) {\n\t\t\tparams.push(query.from);\n\t\t\twhere.push(`created_at >= $${params.length}`);\n\t\t}\n\n\t\tif (query.to) {\n\t\t\tparams.push(query.to);\n\t\t\twhere.push(`created_at <= $${params.length}`);\n\t\t}\n\n\t\tif (query.cursor) {\n\t\t\tconst decoded = decodeCursor(query.cursor);\n\t\t\tif (decoded) {\n\t\t\t\tparams.push(decoded.createdAt, decoded.id);\n\t\t\t\twhere.push(\n\t\t\t\t\t`(created_at, id) < ($${params.length - 1}::timestamptz, $${params.length}::uuid)`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tparams.push(limit);\n\n\t\treturn this.store.query<IAuditEvent>(\n\t\t\t`SELECT id, type, payload, meta, created_at as \"createdAt\"\n\t\t\t FROM fonderie_events\n\t\t\t WHERE ${where.join(' AND ')}\n\t\t\t ORDER BY created_at DESC, id DESC\n\t\t\t LIMIT $${params.length}`,\n\t\t\tparams,\n\t\t);\n\t}\n}\n","import type { IFonderieModule, IFonderieApp } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { buildAuditRoutes } from './routes';\n\nexport class AuditModule implements IFonderieModule {\n\treadonly name = '@fonderie/audit';\n\treadonly deps = ['@fonderie/auth', '@fonderie/workspaces'];\n\n\tconstructor(private readonly store: IStoreAdapter) {}\n\n\tinstall(app: IFonderieApp): void {\n\t\tfor (const [method, path, ...handlers] of buildAuditRoutes(this.store)) {\n\t\t\tapp.addRoute(method, path, ...handlers);\n\t\t}\n\t}\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/routes.ts","../src/dtos/audit.ts","../src/models/event.model.ts","../src/module.ts"],"sourcesContent":["import type { Middleware } from '@fonderie/core';\nimport { setApiResponse, HTTP } from '@fonderie/core';\nimport { requireAuth } from '@fonderie/core/middlewares';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { AuditEventModel } from './models/event.model';\nimport { toAuditEventDTO, encodeCursor } from './dtos/audit';\n\ntype Route = [string, string, ...Middleware[]];\n\nexport function buildAuditRoutes(store: IStoreAdapter): Route[] {\n\treturn [\n\t\t[\n\t\t\t'GET',\n\t\t\t'/audit',\n\t\t\trequireAuth,\n\t\t\tasync (ctx) => {\n\t\t\t\tif (!ctx.workspace)\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t\t'MISSING_WORKSPACE',\n\t\t\t\t\t\t'Workspace context required',\n\t\t\t\t\t);\n\n\t\t\t\tconst url = new URL(ctx.request.url);\n\t\t\t\tconst params = url.searchParams;\n\n\t\t\t\tconst rawLimit = Number(params.get('limit') ?? 50);\n\t\t\t\tconst limit = Number.isFinite(rawLimit)\n\t\t\t\t\t? Math.min(Math.max(Math.trunc(rawLimit), 1), 200)\n\t\t\t\t\t: 50;\n\t\t\t\tconst type = params.get('type') ?? undefined;\n\t\t\t\tconst actor = params.get('actorId') ?? undefined;\n\t\t\t\tconst from = params.get('from') ? new Date(params.get('from')!) : undefined;\n\t\t\t\tconst to = params.get('to') ? new Date(params.get('to')!) : undefined;\n\t\t\t\tconst cursor = params.get('cursor') ?? undefined;\n\n\t\t\t\tconst query: Parameters<AuditEventModel['list']>[0] = {\n\t\t\t\t\tworkspaceId: ctx.workspace.id,\n\t\t\t\t\tlimit,\n\t\t\t\t};\n\t\t\t\tif (type) query.type = type;\n\t\t\t\tif (actor) query.actorId = actor;\n\t\t\t\tif (from) query.from = from;\n\t\t\t\tif (to) query.to = to;\n\t\t\t\tif (cursor) query.cursor = cursor;\n\n\t\t\t\t// The model over-fetches internally to detect a next page; the\n\t\t\t\t// cursor carries the row's created_at::text at full microsecond\n\t\t\t\t// precision so same-millisecond events can't be skipped.\n\t\t\t\tconst { events, hasMore } = await new AuditEventModel(store).list(query);\n\t\t\t\tconst lastEvent = events[events.length - 1];\n\t\t\t\tconst nextCursor =\n\t\t\t\t\thasMore && lastEvent\n\t\t\t\t\t\t? encodeCursor(lastEvent.createdAtRaw ?? lastEvent.createdAt, lastEvent.id)\n\t\t\t\t\t\t: null;\n\n\t\t\t\treturn setApiResponse(HTTP.OK, 'AUDIT_FETCHED', 'Audit events retrieved.', {\n\t\t\t\t\tevents: events.map(toAuditEventDTO),\n\t\t\t\t\tnextCursor,\n\t\t\t\t});\n\t\t\t},\n\t\t],\n\t];\n}\n","import type { IAuditEvent } from '../types';\n\nexport interface IAuditEventDTO {\n\tid: string;\n\ttype: string;\n\tactorId: string | null;\n\trequestId: string | null;\n\tpayload: Record<string, unknown>;\n\tcreatedAt: string;\n}\n\nexport interface IAuditPageDTO {\n\tevents: IAuditEventDTO[];\n\tnextCursor: string | null;\n}\n\nexport function toAuditEventDTO(e: IAuditEvent): IAuditEventDTO {\n\treturn {\n\t\tid: e.id,\n\t\ttype: e.type,\n\t\tactorId: (e.payload['userId'] as string | undefined) ?? null,\n\t\trequestId: (e.meta['requestId'] as string | undefined) ?? null,\n\t\tpayload: e.payload,\n\t\tcreatedAt: e.createdAt.toISOString(),\n\t};\n}\n\n// Cursors accept ISO timestamps and Postgres' own text format (microsecond\n// precision, e.g. '2026-09-05 00:38:31.123456+00') — the keyset carries the\n// latter, because round-tripping through a millisecond JS Date truncates\n// sub-millisecond digits and silently skips same-millisecond events (rows\n// created in one transaction all share now() exactly) between pages.\nconst CURSOR_TS_RE = /^\\d{4}-\\d{2}-\\d{2}[T ]\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,6})?(Z|[+-]\\d{2}(:?\\d{2})?)?$/;\nconst CURSOR_ID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;\n\nexport function encodeCursor(createdAt: Date | string, id: string): string {\n\tconst ts = typeof createdAt === 'string' ? createdAt : createdAt.toISOString();\n\treturn Buffer.from(`${ts},${id}`).toString('base64');\n}\n\nexport function decodeCursor(cursor: string): { createdAt: string; id: string } | null {\n\tif (cursor.length > 256) return null;\n\ttry {\n\t\tconst raw = Buffer.from(cursor, 'base64').toString();\n\t\tconst commaIdx = raw.indexOf(',');\n\t\tif (commaIdx === -1) return null;\n\t\tconst createdAt = raw.slice(0, commaIdx);\n\t\tconst id = raw.slice(commaIdx + 1);\n\t\t// Both halves feed ::timestamptz / ::uuid casts — validate here so a\n\t\t// crafted cursor yields an empty page, not a Postgres cast error.\n\t\tif (!CURSOR_TS_RE.test(createdAt) || !CURSOR_ID_RE.test(id)) return null;\n\t\treturn { createdAt, id };\n\t} catch {\n\t\treturn null;\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IAuditEvent, IAuditQuery } from '../types';\nimport { decodeCursor } from '../dtos/audit';\n\nconst MAX_LIMIT = 200;\n\nexport type IAuditEventRow = IAuditEvent & {\n\t// created_at::text — full microsecond precision for the keyset cursor\n\t// (node-pg parses timestamptz into a millisecond Date, which would make\n\t// the cursor skip same-millisecond rows between pages).\n\tcreatedAtRaw: string;\n};\n\nexport interface IAuditEventPage {\n\tevents: IAuditEventRow[];\n\thasMore: boolean;\n}\n\nexport class AuditEventModel {\n\tconstructor(private readonly store: IStoreAdapter) {}\n\n\t// `query.limit` is the CLIENT-facing page size. The +1 over-fetch that\n\t// detects a next page happens inside this method — keeping it here means\n\t// no outer clamp can shave it off (which previously made nextCursor\n\t// unreachable at the max page size).\n\tasync list(query: IAuditQuery): Promise<IAuditEventPage> {\n\t\tconst limit = Math.min(query.limit ?? 50, MAX_LIMIT);\n\t\tconst params: unknown[] = [query.workspaceId];\n\t\tconst where: string[] = [`payload->>'workspaceId' = $1`];\n\n\t\tif (query.type) {\n\t\t\tparams.push(query.type);\n\t\t\twhere.push(`type = $${params.length}`);\n\t\t}\n\n\t\tif (query.actorId) {\n\t\t\tparams.push(query.actorId);\n\t\t\twhere.push(`payload->>'userId' = $${params.length}`);\n\t\t}\n\n\t\tif (query.from) {\n\t\t\tparams.push(query.from);\n\t\t\twhere.push(`created_at >= $${params.length}`);\n\t\t}\n\n\t\tif (query.to) {\n\t\t\tparams.push(query.to);\n\t\t\twhere.push(`created_at <= $${params.length}`);\n\t\t}\n\n\t\tif (query.cursor) {\n\t\t\tconst decoded = decodeCursor(query.cursor);\n\t\t\tif (decoded) {\n\t\t\t\tparams.push(decoded.createdAt, decoded.id);\n\t\t\t\twhere.push(\n\t\t\t\t\t`(created_at, id) < ($${params.length - 1}::timestamptz, $${params.length}::uuid)`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tparams.push(limit + 1);\n\n\t\tconst rows = await this.store.query<IAuditEventRow>(\n\t\t\t`SELECT id, type, payload, meta, created_at as \"createdAt\",\n\t\t\t created_at::text as \"createdAtRaw\"\n\t\t\t FROM fonderie_events\n\t\t\t WHERE ${where.join(' AND ')}\n\t\t\t ORDER BY created_at DESC, id DESC\n\t\t\t LIMIT $${params.length}`,\n\t\t\tparams,\n\t\t);\n\n\t\treturn { events: rows.slice(0, limit), hasMore: rows.length > limit };\n\t}\n}\n","import type { IFonderieModule, IFonderieApp } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { buildAuditRoutes } from './routes';\n\nexport class AuditModule implements IFonderieModule {\n\treadonly name = '@fonderie/audit';\n\treadonly deps = ['@fonderie/auth', '@fonderie/workspaces'];\n\n\tconstructor(private readonly store: IStoreAdapter) {}\n\n\tinstall(app: IFonderieApp): void {\n\t\tfor (const [method, path, ...handlers] of buildAuditRoutes(this.store)) {\n\t\t\tapp.addRoute(method, path, ...handlers);\n\t\t}\n\t}\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fonderie/audit",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.1",
|
|
4
4
|
"description": "Workspace-scoped audit log — query fonderie_events for a human-readable activity trail.",
|
|
5
5
|
"keywords": [
|
|
6
|
-
"
|
|
6
|
+
"fonderiejs",
|
|
7
7
|
"audit",
|
|
8
8
|
"events",
|
|
9
9
|
"saas",
|
|
@@ -40,9 +40,9 @@
|
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@fonderie/core": "../core",
|
|
42
42
|
"@fonderie/store": "../store",
|
|
43
|
-
"@types/node": "^
|
|
43
|
+
"@types/node": "^26.4.0",
|
|
44
44
|
"tsup": "^8.5.1",
|
|
45
|
-
"tsx": "^4.
|
|
45
|
+
"tsx": "^4.23.12",
|
|
46
46
|
"typescript": "^6.0.3"
|
|
47
47
|
},
|
|
48
48
|
"publishConfig": {
|
|
@@ -56,11 +56,11 @@
|
|
|
56
56
|
],
|
|
57
57
|
"repository": {
|
|
58
58
|
"type": "git",
|
|
59
|
-
"url": "git+https://github.com/fonderiejs/
|
|
59
|
+
"url": "git+https://github.com/fonderiejs/fonderie.git",
|
|
60
60
|
"directory": "packages/audit"
|
|
61
61
|
},
|
|
62
|
-
"homepage": "https://github.com/fonderiejs/
|
|
62
|
+
"homepage": "https://github.com/fonderiejs/fonderie/tree/main/packages/audit#readme",
|
|
63
63
|
"bugs": {
|
|
64
|
-
"url": "https://github.com/fonderiejs/
|
|
64
|
+
"url": "https://github.com/fonderiejs/fonderie/issues"
|
|
65
65
|
}
|
|
66
66
|
}
|