@axiom-lattice/pg-stores 3.1.8 → 3.1.10

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.
@@ -0,0 +1,143 @@
1
+ import { Pool } from "pg";
2
+ import type { PoolConfig } from "pg";
3
+ import type {
4
+ OpenAuditStore,
5
+ OpenAuditAppendInput,
6
+ OpenAuditQuery,
7
+ OpenAuditRecord,
8
+ } from "@axiom-lattice/protocols";
9
+ import { MigrationManager } from "../migrations/migration";
10
+ import { createOpenAuditTable } from "../migrations/create_open_audit_table";
11
+
12
+ export interface PostgreSQLOpenAuditStoreOptions {
13
+ pool?: Pool;
14
+ poolConfig?: string | PoolConfig;
15
+ autoMigrate?: boolean;
16
+ }
17
+
18
+ /** Append-only PostgreSQL audit store for the Open API surface. */
19
+ export class PostgreSQLOpenAuditStore implements OpenAuditStore {
20
+ private pool: Pool;
21
+ private migrationManager!: MigrationManager;
22
+ private initialized = false;
23
+ private ownsPool = true;
24
+ private initPromise: Promise<void> | null = null;
25
+
26
+ constructor(options: PostgreSQLOpenAuditStoreOptions) {
27
+ if (options.pool) {
28
+ this.pool = options.pool;
29
+ this.ownsPool = false;
30
+ this.initialized = true;
31
+ return;
32
+ }
33
+ this.pool =
34
+ typeof options.poolConfig === "string"
35
+ ? new Pool({ connectionString: options.poolConfig })
36
+ : options.poolConfig
37
+ ? new Pool(options.poolConfig as PoolConfig)
38
+ : (() => {
39
+ throw new Error("Either pool or poolConfig must be provided");
40
+ })();
41
+ this.migrationManager = new MigrationManager(this.pool);
42
+ this.migrationManager.register(createOpenAuditTable);
43
+ if (options.autoMigrate !== false) {
44
+ this.initialize().catch((error) => {
45
+ console.error("Failed to initialize PostgreSQLOpenAuditStore:", error);
46
+ throw error;
47
+ });
48
+ }
49
+ }
50
+
51
+ async initialize(): Promise<void> {
52
+ if (this.initialized) return;
53
+ if (this.initPromise) return this.initPromise;
54
+ this.initPromise = (async () => {
55
+ try {
56
+ await this.migrationManager.migrate();
57
+ this.initialized = true;
58
+ } finally {
59
+ this.initPromise = null;
60
+ }
61
+ })();
62
+ return this.initPromise;
63
+ }
64
+
65
+ async dispose(): Promise<void> {
66
+ if (this.ownsPool && this.pool) await this.pool.end();
67
+ }
68
+
69
+ private async ensureInitialized(): Promise<void> {
70
+ if (!this.initialized) await this.initialize();
71
+ }
72
+
73
+ async query(params: OpenAuditQuery): Promise<OpenAuditRecord[]> {
74
+ await this.ensureInitialized();
75
+ const clauses = ["tenant_id = $1"];
76
+ const values: unknown[] = [params.tenantId];
77
+ if (params.credentialId) {
78
+ values.push(params.credentialId);
79
+ clauses.push(`credential_id = $${values.length}`);
80
+ }
81
+ if (params.domain) {
82
+ values.push(params.domain);
83
+ clauses.push(`domain = $${values.length}`);
84
+ }
85
+ if (params.status) {
86
+ values.push(params.status);
87
+ clauses.push(`status = $${values.length}`);
88
+ }
89
+ if (params.from) {
90
+ values.push(params.from);
91
+ clauses.push(`created_at >= $${values.length}`);
92
+ }
93
+ if (params.to) {
94
+ values.push(params.to);
95
+ clauses.push(`created_at < $${values.length}`);
96
+ }
97
+ const limit = params.limit ?? 100;
98
+ const offset = params.offset ?? 0;
99
+ values.push(limit, offset);
100
+ const { rows } = await this.pool.query(
101
+ `SELECT * FROM lattice_open_audit
102
+ WHERE ${clauses.join(" AND ")}
103
+ ORDER BY created_at DESC
104
+ LIMIT $${values.length - 1} OFFSET $${values.length}`,
105
+ values,
106
+ );
107
+ return rows.map((row) => ({
108
+ id: row.id,
109
+ tenantId: row.tenant_id,
110
+ credentialId: row.credential_id,
111
+ credentialKind: row.credential_kind,
112
+ projectId: row.project_id,
113
+ domain: row.domain,
114
+ action: row.action,
115
+ args: row.args,
116
+ status: row.status,
117
+ errorCode: row.error_code ?? undefined,
118
+ durationMs: row.duration_ms,
119
+ createdAt: row.created_at,
120
+ }));
121
+ }
122
+
123
+ async append(record: OpenAuditAppendInput): Promise<void> {
124
+ await this.ensureInitialized();
125
+ await this.pool.query(
126
+ `INSERT INTO lattice_open_audit
127
+ (tenant_id, credential_id, credential_kind, project_id, domain, action, args, status, error_code, duration_ms)
128
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)`,
129
+ [
130
+ record.tenantId,
131
+ record.credentialId,
132
+ record.credentialKind,
133
+ record.projectId,
134
+ record.domain,
135
+ record.action,
136
+ record.args === undefined ? null : JSON.stringify(record.args),
137
+ record.status,
138
+ record.errorCode ?? null,
139
+ Math.max(0, Math.round(record.durationMs)),
140
+ ],
141
+ );
142
+ }
143
+ }
@@ -83,6 +83,25 @@ function mapRow(row: MessageRow): ProjectRoomMessage {
83
83
  ...(row.idempotency_key === null ? {} : { idempotencyKey: row.idempotency_key }), createdAt: row.created_at,
84
84
  };
85
85
  }
86
+ function mapRowsTolerant(
87
+ rows: MessageRow[],
88
+ scope: { tenantId: string; roomId: string },
89
+ ): ProjectRoomMessage[] {
90
+ const messages: ProjectRoomMessage[] = [];
91
+ for (const row of rows) {
92
+ try {
93
+ messages.push(mapRow(row));
94
+ } catch (error) {
95
+ console.warn("[ProjectRoomMessageStore] Skipping invalid message row", {
96
+ tenantId: scope.tenantId,
97
+ roomId: scope.roomId,
98
+ rowId: typeof row.id === "string" ? row.id : undefined,
99
+ reason: error instanceof Error ? error.message : String(error),
100
+ });
101
+ }
102
+ }
103
+ return messages;
104
+ }
86
105
  function isUniqueViolation(error: unknown): error is PostgreSQLError {
87
106
  return typeof error === "object" && error !== null && "code" in error
88
107
  && (error as PostgreSQLError).code === "23505";
@@ -159,7 +178,7 @@ export class PostgreSQLProjectRoomMessageStore implements ProjectRoomMessageStor
159
178
  WHERE tenant_id = $1 AND room_id = $2 ORDER BY created_at DESC, id DESC LIMIT $3`,
160
179
  [input.tenantId, input.roomId, limit],
161
180
  );
162
- return result.rows.map(mapRow);
181
+ return mapRowsTolerant(result.rows, { tenantId: input.tenantId, roomId: input.roomId });
163
182
  }
164
183
 
165
184
  /** Finds a message by tenant-scoped ID. */
@@ -25,6 +25,7 @@ type ShareRow = {
25
25
  password_hash: string | null;
26
26
  expires_at: string | null;
27
27
  max_access: number | null;
28
+ open_grants: unknown[] | null;
28
29
  access_count: number;
29
30
  revoked: boolean;
30
31
  created_at: string;
@@ -106,8 +107,8 @@ export class PostgresSharedResourceStore implements SharedResourceStore {
106
107
  `INSERT INTO lattice_shared_resources
107
108
  (token, address, title, created_by, tenant_id, workspace_id, project_id,
108
109
  assistant_id, vm_isolation, visibility, password_hash,
109
- expires_at, max_access, revoked)
110
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)
110
+ expires_at, max_access, open_grants, revoked)
111
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15)
111
112
  RETURNING *`,
112
113
  [
113
114
  record.token,
@@ -123,6 +124,7 @@ export class PostgresSharedResourceStore implements SharedResourceStore {
123
124
  record.passwordHash,
124
125
  record.expiresAt ? record.expiresAt.toISOString() : null,
125
126
  record.maxAccess,
127
+ record.openGrants ? JSON.stringify(record.openGrants) : null,
126
128
  record.revoked,
127
129
  ],
128
130
  );
@@ -229,6 +231,7 @@ export class PostgresSharedResourceStore implements SharedResourceStore {
229
231
  passwordHash: row.password_hash,
230
232
  expiresAt: row.expires_at ? new Date(row.expires_at) : null,
231
233
  maxAccess: row.max_access,
234
+ openGrants: (row.open_grants as ShareRecord["openGrants"]) ?? undefined,
232
235
  accessCount: row.access_count ?? 0,
233
236
  revoked: row.revoked ?? false,
234
237
  createdAt: new Date(row.created_at),