@sandbox-agent/persist-postgres 0.3.0 → 0.3.2

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/dist/index.js CHANGED
@@ -88,10 +88,9 @@ var PostgresSessionPersistDriver = class {
88
88
  LIMIT $2 OFFSET $3`,
89
89
  [request.sessionId, limit, offset]
90
90
  );
91
- const countResult = await this.pool.query(
92
- `SELECT COUNT(*) AS count FROM ${this.table("events")} WHERE session_id = $1`,
93
- [request.sessionId]
94
- );
91
+ const countResult = await this.pool.query(`SELECT COUNT(*) AS count FROM ${this.table("events")} WHERE session_id = $1`, [
92
+ request.sessionId
93
+ ]);
95
94
  const total = parseInteger(countResult.rows[0]?.count ?? "0");
96
95
  const nextOffset = offset + rowsResult.rows.length;
97
96
  return {
@@ -112,15 +111,7 @@ var PostgresSessionPersistDriver = class {
112
111
  connection_id = EXCLUDED.connection_id,
113
112
  sender = EXCLUDED.sender,
114
113
  payload_json = EXCLUDED.payload_json`,
115
- [
116
- event.id,
117
- event.eventIndex,
118
- event.sessionId,
119
- event.createdAt,
120
- event.connectionId,
121
- event.sender,
122
- event.payload
123
- ]
114
+ [event.id, event.eventIndex, event.sessionId, event.createdAt, event.connectionId, event.sender, event.payload]
124
115
  );
125
116
  }
126
117
  async close() {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { Pool, type PoolConfig } from \"pg\";\nimport type {\n ListEventsRequest,\n ListPage,\n ListPageRequest,\n SessionEvent,\n SessionPersistDriver,\n SessionRecord,\n} from \"sandbox-agent\";\n\nconst DEFAULT_LIST_LIMIT = 100;\n\nexport interface PostgresSessionPersistDriverOptions {\n connectionString?: string;\n pool?: Pool;\n poolConfig?: PoolConfig;\n schema?: string;\n}\n\nexport class PostgresSessionPersistDriver implements SessionPersistDriver {\n private readonly pool: Pool;\n private readonly ownsPool: boolean;\n private readonly schema: string;\n private readonly initialized: Promise<void>;\n\n constructor(options: PostgresSessionPersistDriverOptions = {}) {\n this.schema = normalizeSchema(options.schema ?? \"public\");\n\n if (options.pool) {\n this.pool = options.pool;\n this.ownsPool = false;\n } else {\n this.pool = new Pool({\n connectionString: options.connectionString,\n ...options.poolConfig,\n });\n this.ownsPool = true;\n }\n\n this.initialized = this.initialize();\n }\n\n async getSession(id: string): Promise<SessionRecord | null> {\n await this.ready();\n\n const result = await this.pool.query<SessionRow>(\n `SELECT id, agent, agent_session_id, last_connection_id, created_at, destroyed_at, session_init_json\n FROM ${this.table(\"sessions\")}\n WHERE id = $1`,\n [id],\n );\n\n if (result.rows.length === 0) {\n return null;\n }\n\n return decodeSessionRow(result.rows[0]);\n }\n\n async listSessions(request: ListPageRequest = {}): Promise<ListPage<SessionRecord>> {\n await this.ready();\n\n const offset = parseCursor(request.cursor);\n const limit = normalizeLimit(request.limit);\n\n const rowsResult = await this.pool.query<SessionRow>(\n `SELECT id, agent, agent_session_id, last_connection_id, created_at, destroyed_at, session_init_json\n FROM ${this.table(\"sessions\")}\n ORDER BY created_at ASC, id ASC\n LIMIT $1 OFFSET $2`,\n [limit, offset],\n );\n\n const countResult = await this.pool.query<{ count: string }>(`SELECT COUNT(*) AS count FROM ${this.table(\"sessions\")}`);\n const total = parseInteger(countResult.rows[0]?.count ?? \"0\");\n const nextOffset = offset + rowsResult.rows.length;\n\n return {\n items: rowsResult.rows.map(decodeSessionRow),\n nextCursor: nextOffset < total ? String(nextOffset) : undefined,\n };\n }\n\n async updateSession(session: SessionRecord): Promise<void> {\n await this.ready();\n\n await this.pool.query(\n `INSERT INTO ${this.table(\"sessions\")} (\n id, agent, agent_session_id, last_connection_id, created_at, destroyed_at, session_init_json\n ) VALUES ($1, $2, $3, $4, $5, $6, $7)\n ON CONFLICT(id) DO UPDATE SET\n agent = EXCLUDED.agent,\n agent_session_id = EXCLUDED.agent_session_id,\n last_connection_id = EXCLUDED.last_connection_id,\n created_at = EXCLUDED.created_at,\n destroyed_at = EXCLUDED.destroyed_at,\n session_init_json = EXCLUDED.session_init_json`,\n [\n session.id,\n session.agent,\n session.agentSessionId,\n session.lastConnectionId,\n session.createdAt,\n session.destroyedAt ?? null,\n session.sessionInit ?? null,\n ],\n );\n }\n\n async listEvents(request: ListEventsRequest): Promise<ListPage<SessionEvent>> {\n await this.ready();\n\n const offset = parseCursor(request.cursor);\n const limit = normalizeLimit(request.limit);\n\n const rowsResult = await this.pool.query<EventRow>(\n `SELECT id, event_index, session_id, created_at, connection_id, sender, payload_json\n FROM ${this.table(\"events\")}\n WHERE session_id = $1\n ORDER BY event_index ASC, id ASC\n LIMIT $2 OFFSET $3`,\n [request.sessionId, limit, offset],\n );\n\n const countResult = await this.pool.query<{ count: string }>(\n `SELECT COUNT(*) AS count FROM ${this.table(\"events\")} WHERE session_id = $1`,\n [request.sessionId],\n );\n const total = parseInteger(countResult.rows[0]?.count ?? \"0\");\n const nextOffset = offset + rowsResult.rows.length;\n\n return {\n items: rowsResult.rows.map(decodeEventRow),\n nextCursor: nextOffset < total ? String(nextOffset) : undefined,\n };\n }\n\n async insertEvent(event: SessionEvent): Promise<void> {\n await this.ready();\n\n await this.pool.query(\n `INSERT INTO ${this.table(\"events\")} (\n id, event_index, session_id, created_at, connection_id, sender, payload_json\n ) VALUES ($1, $2, $3, $4, $5, $6, $7)\n ON CONFLICT(id) DO UPDATE SET\n event_index = EXCLUDED.event_index,\n session_id = EXCLUDED.session_id,\n created_at = EXCLUDED.created_at,\n connection_id = EXCLUDED.connection_id,\n sender = EXCLUDED.sender,\n payload_json = EXCLUDED.payload_json`,\n [\n event.id,\n event.eventIndex,\n event.sessionId,\n event.createdAt,\n event.connectionId,\n event.sender,\n event.payload,\n ],\n );\n }\n\n async close(): Promise<void> {\n if (!this.ownsPool) {\n return;\n }\n await this.pool.end();\n }\n\n private async ready(): Promise<void> {\n await this.initialized;\n }\n\n private table(name: \"sessions\" | \"events\"): string {\n return `\"${this.schema}\".\"${name}\"`;\n }\n\n private async initialize(): Promise<void> {\n await this.pool.query(`CREATE SCHEMA IF NOT EXISTS \"${this.schema}\"`);\n\n await this.pool.query(`\n CREATE TABLE IF NOT EXISTS ${this.table(\"sessions\")} (\n id TEXT PRIMARY KEY,\n agent TEXT NOT NULL,\n agent_session_id TEXT NOT NULL,\n last_connection_id TEXT NOT NULL,\n created_at BIGINT NOT NULL,\n destroyed_at BIGINT,\n session_init_json JSONB\n )\n `);\n\n await this.pool.query(`\n CREATE TABLE IF NOT EXISTS ${this.table(\"events\")} (\n id TEXT PRIMARY KEY,\n event_index BIGINT NOT NULL,\n session_id TEXT NOT NULL,\n created_at BIGINT NOT NULL,\n connection_id TEXT NOT NULL,\n sender TEXT NOT NULL,\n payload_json JSONB NOT NULL\n )\n `);\n\n await this.pool.query(`\n ALTER TABLE ${this.table(\"events\")}\n ALTER COLUMN id TYPE TEXT USING id::TEXT\n `);\n\n await this.pool.query(`\n ALTER TABLE ${this.table(\"events\")}\n ADD COLUMN IF NOT EXISTS event_index BIGINT\n `);\n\n await this.pool.query(`\n WITH ranked AS (\n SELECT id, ROW_NUMBER() OVER (PARTITION BY session_id ORDER BY created_at ASC, id ASC) AS ranked_index\n FROM ${this.table(\"events\")}\n )\n UPDATE ${this.table(\"events\")} AS current_events\n SET event_index = ranked.ranked_index\n FROM ranked\n WHERE current_events.id = ranked.id\n AND current_events.event_index IS NULL\n `);\n\n await this.pool.query(`\n ALTER TABLE ${this.table(\"events\")}\n ALTER COLUMN event_index SET NOT NULL\n `);\n\n await this.pool.query(`\n CREATE INDEX IF NOT EXISTS idx_events_session_order\n ON ${this.table(\"events\")}(session_id, event_index, id)\n `);\n }\n}\n\ntype SessionRow = {\n id: string;\n agent: string;\n agent_session_id: string;\n last_connection_id: string;\n created_at: string | number;\n destroyed_at: string | number | null;\n session_init_json: unknown | null;\n};\n\ntype EventRow = {\n id: string | number;\n event_index: string | number;\n session_id: string;\n created_at: string | number;\n connection_id: string;\n sender: string;\n payload_json: unknown;\n};\n\nfunction decodeSessionRow(row: SessionRow): SessionRecord {\n return {\n id: row.id,\n agent: row.agent,\n agentSessionId: row.agent_session_id,\n lastConnectionId: row.last_connection_id,\n createdAt: parseInteger(row.created_at),\n destroyedAt: row.destroyed_at === null ? undefined : parseInteger(row.destroyed_at),\n sessionInit: row.session_init_json ? (row.session_init_json as SessionRecord[\"sessionInit\"]) : undefined,\n };\n}\n\nfunction decodeEventRow(row: EventRow): SessionEvent {\n return {\n id: String(row.id),\n eventIndex: parseInteger(row.event_index),\n sessionId: row.session_id,\n createdAt: parseInteger(row.created_at),\n connectionId: row.connection_id,\n sender: parseSender(row.sender),\n payload: row.payload_json as SessionEvent[\"payload\"],\n };\n}\n\nfunction normalizeLimit(limit: number | undefined): number {\n if (!Number.isFinite(limit) || (limit ?? 0) < 1) {\n return DEFAULT_LIST_LIMIT;\n }\n return Math.floor(limit as number);\n}\n\nfunction parseCursor(cursor: string | undefined): number {\n if (!cursor) {\n return 0;\n }\n const parsed = Number.parseInt(cursor, 10);\n if (!Number.isFinite(parsed) || parsed < 0) {\n return 0;\n }\n return parsed;\n}\n\nfunction parseInteger(value: string | number): number {\n const parsed = typeof value === \"number\" ? value : Number.parseInt(value, 10);\n if (!Number.isFinite(parsed)) {\n throw new Error(`Invalid integer value returned by postgres: ${String(value)}`);\n }\n return parsed;\n}\n\nfunction parseSender(value: string): SessionEvent[\"sender\"] {\n if (value === \"agent\" || value === \"client\") {\n return value;\n }\n throw new Error(`Invalid sender value returned by postgres: ${value}`);\n}\n\nfunction normalizeSchema(schema: string): string {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(schema)) {\n throw new Error(`Invalid schema name '${schema}'. Use letters, numbers, and underscores only.`);\n }\n return schema;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { Pool, type PoolConfig } from \"pg\";\nimport type { ListEventsRequest, ListPage, ListPageRequest, SessionEvent, SessionPersistDriver, SessionRecord } from \"sandbox-agent\";\n\nconst DEFAULT_LIST_LIMIT = 100;\n\nexport interface PostgresSessionPersistDriverOptions {\n connectionString?: string;\n pool?: Pool;\n poolConfig?: PoolConfig;\n schema?: string;\n}\n\nexport class PostgresSessionPersistDriver implements SessionPersistDriver {\n private readonly pool: Pool;\n private readonly ownsPool: boolean;\n private readonly schema: string;\n private readonly initialized: Promise<void>;\n\n constructor(options: PostgresSessionPersistDriverOptions = {}) {\n this.schema = normalizeSchema(options.schema ?? \"public\");\n\n if (options.pool) {\n this.pool = options.pool;\n this.ownsPool = false;\n } else {\n this.pool = new Pool({\n connectionString: options.connectionString,\n ...options.poolConfig,\n });\n this.ownsPool = true;\n }\n\n this.initialized = this.initialize();\n }\n\n async getSession(id: string): Promise<SessionRecord | null> {\n await this.ready();\n\n const result = await this.pool.query<SessionRow>(\n `SELECT id, agent, agent_session_id, last_connection_id, created_at, destroyed_at, session_init_json\n FROM ${this.table(\"sessions\")}\n WHERE id = $1`,\n [id],\n );\n\n if (result.rows.length === 0) {\n return null;\n }\n\n return decodeSessionRow(result.rows[0]);\n }\n\n async listSessions(request: ListPageRequest = {}): Promise<ListPage<SessionRecord>> {\n await this.ready();\n\n const offset = parseCursor(request.cursor);\n const limit = normalizeLimit(request.limit);\n\n const rowsResult = await this.pool.query<SessionRow>(\n `SELECT id, agent, agent_session_id, last_connection_id, created_at, destroyed_at, session_init_json\n FROM ${this.table(\"sessions\")}\n ORDER BY created_at ASC, id ASC\n LIMIT $1 OFFSET $2`,\n [limit, offset],\n );\n\n const countResult = await this.pool.query<{ count: string }>(`SELECT COUNT(*) AS count FROM ${this.table(\"sessions\")}`);\n const total = parseInteger(countResult.rows[0]?.count ?? \"0\");\n const nextOffset = offset + rowsResult.rows.length;\n\n return {\n items: rowsResult.rows.map(decodeSessionRow),\n nextCursor: nextOffset < total ? String(nextOffset) : undefined,\n };\n }\n\n async updateSession(session: SessionRecord): Promise<void> {\n await this.ready();\n\n await this.pool.query(\n `INSERT INTO ${this.table(\"sessions\")} (\n id, agent, agent_session_id, last_connection_id, created_at, destroyed_at, session_init_json\n ) VALUES ($1, $2, $3, $4, $5, $6, $7)\n ON CONFLICT(id) DO UPDATE SET\n agent = EXCLUDED.agent,\n agent_session_id = EXCLUDED.agent_session_id,\n last_connection_id = EXCLUDED.last_connection_id,\n created_at = EXCLUDED.created_at,\n destroyed_at = EXCLUDED.destroyed_at,\n session_init_json = EXCLUDED.session_init_json`,\n [\n session.id,\n session.agent,\n session.agentSessionId,\n session.lastConnectionId,\n session.createdAt,\n session.destroyedAt ?? null,\n session.sessionInit ?? null,\n ],\n );\n }\n\n async listEvents(request: ListEventsRequest): Promise<ListPage<SessionEvent>> {\n await this.ready();\n\n const offset = parseCursor(request.cursor);\n const limit = normalizeLimit(request.limit);\n\n const rowsResult = await this.pool.query<EventRow>(\n `SELECT id, event_index, session_id, created_at, connection_id, sender, payload_json\n FROM ${this.table(\"events\")}\n WHERE session_id = $1\n ORDER BY event_index ASC, id ASC\n LIMIT $2 OFFSET $3`,\n [request.sessionId, limit, offset],\n );\n\n const countResult = await this.pool.query<{ count: string }>(`SELECT COUNT(*) AS count FROM ${this.table(\"events\")} WHERE session_id = $1`, [\n request.sessionId,\n ]);\n const total = parseInteger(countResult.rows[0]?.count ?? \"0\");\n const nextOffset = offset + rowsResult.rows.length;\n\n return {\n items: rowsResult.rows.map(decodeEventRow),\n nextCursor: nextOffset < total ? String(nextOffset) : undefined,\n };\n }\n\n async insertEvent(event: SessionEvent): Promise<void> {\n await this.ready();\n\n await this.pool.query(\n `INSERT INTO ${this.table(\"events\")} (\n id, event_index, session_id, created_at, connection_id, sender, payload_json\n ) VALUES ($1, $2, $3, $4, $5, $6, $7)\n ON CONFLICT(id) DO UPDATE SET\n event_index = EXCLUDED.event_index,\n session_id = EXCLUDED.session_id,\n created_at = EXCLUDED.created_at,\n connection_id = EXCLUDED.connection_id,\n sender = EXCLUDED.sender,\n payload_json = EXCLUDED.payload_json`,\n [event.id, event.eventIndex, event.sessionId, event.createdAt, event.connectionId, event.sender, event.payload],\n );\n }\n\n async close(): Promise<void> {\n if (!this.ownsPool) {\n return;\n }\n await this.pool.end();\n }\n\n private async ready(): Promise<void> {\n await this.initialized;\n }\n\n private table(name: \"sessions\" | \"events\"): string {\n return `\"${this.schema}\".\"${name}\"`;\n }\n\n private async initialize(): Promise<void> {\n await this.pool.query(`CREATE SCHEMA IF NOT EXISTS \"${this.schema}\"`);\n\n await this.pool.query(`\n CREATE TABLE IF NOT EXISTS ${this.table(\"sessions\")} (\n id TEXT PRIMARY KEY,\n agent TEXT NOT NULL,\n agent_session_id TEXT NOT NULL,\n last_connection_id TEXT NOT NULL,\n created_at BIGINT NOT NULL,\n destroyed_at BIGINT,\n session_init_json JSONB\n )\n `);\n\n await this.pool.query(`\n CREATE TABLE IF NOT EXISTS ${this.table(\"events\")} (\n id TEXT PRIMARY KEY,\n event_index BIGINT NOT NULL,\n session_id TEXT NOT NULL,\n created_at BIGINT NOT NULL,\n connection_id TEXT NOT NULL,\n sender TEXT NOT NULL,\n payload_json JSONB NOT NULL\n )\n `);\n\n await this.pool.query(`\n ALTER TABLE ${this.table(\"events\")}\n ALTER COLUMN id TYPE TEXT USING id::TEXT\n `);\n\n await this.pool.query(`\n ALTER TABLE ${this.table(\"events\")}\n ADD COLUMN IF NOT EXISTS event_index BIGINT\n `);\n\n await this.pool.query(`\n WITH ranked AS (\n SELECT id, ROW_NUMBER() OVER (PARTITION BY session_id ORDER BY created_at ASC, id ASC) AS ranked_index\n FROM ${this.table(\"events\")}\n )\n UPDATE ${this.table(\"events\")} AS current_events\n SET event_index = ranked.ranked_index\n FROM ranked\n WHERE current_events.id = ranked.id\n AND current_events.event_index IS NULL\n `);\n\n await this.pool.query(`\n ALTER TABLE ${this.table(\"events\")}\n ALTER COLUMN event_index SET NOT NULL\n `);\n\n await this.pool.query(`\n CREATE INDEX IF NOT EXISTS idx_events_session_order\n ON ${this.table(\"events\")}(session_id, event_index, id)\n `);\n }\n}\n\ntype SessionRow = {\n id: string;\n agent: string;\n agent_session_id: string;\n last_connection_id: string;\n created_at: string | number;\n destroyed_at: string | number | null;\n session_init_json: unknown | null;\n};\n\ntype EventRow = {\n id: string | number;\n event_index: string | number;\n session_id: string;\n created_at: string | number;\n connection_id: string;\n sender: string;\n payload_json: unknown;\n};\n\nfunction decodeSessionRow(row: SessionRow): SessionRecord {\n return {\n id: row.id,\n agent: row.agent,\n agentSessionId: row.agent_session_id,\n lastConnectionId: row.last_connection_id,\n createdAt: parseInteger(row.created_at),\n destroyedAt: row.destroyed_at === null ? undefined : parseInteger(row.destroyed_at),\n sessionInit: row.session_init_json ? (row.session_init_json as SessionRecord[\"sessionInit\"]) : undefined,\n };\n}\n\nfunction decodeEventRow(row: EventRow): SessionEvent {\n return {\n id: String(row.id),\n eventIndex: parseInteger(row.event_index),\n sessionId: row.session_id,\n createdAt: parseInteger(row.created_at),\n connectionId: row.connection_id,\n sender: parseSender(row.sender),\n payload: row.payload_json as SessionEvent[\"payload\"],\n };\n}\n\nfunction normalizeLimit(limit: number | undefined): number {\n if (!Number.isFinite(limit) || (limit ?? 0) < 1) {\n return DEFAULT_LIST_LIMIT;\n }\n return Math.floor(limit as number);\n}\n\nfunction parseCursor(cursor: string | undefined): number {\n if (!cursor) {\n return 0;\n }\n const parsed = Number.parseInt(cursor, 10);\n if (!Number.isFinite(parsed) || parsed < 0) {\n return 0;\n }\n return parsed;\n}\n\nfunction parseInteger(value: string | number): number {\n const parsed = typeof value === \"number\" ? value : Number.parseInt(value, 10);\n if (!Number.isFinite(parsed)) {\n throw new Error(`Invalid integer value returned by postgres: ${String(value)}`);\n }\n return parsed;\n}\n\nfunction parseSender(value: string): SessionEvent[\"sender\"] {\n if (value === \"agent\" || value === \"client\") {\n return value;\n }\n throw new Error(`Invalid sender value returned by postgres: ${value}`);\n}\n\nfunction normalizeSchema(schema: string): string {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(schema)) {\n throw new Error(`Invalid schema name '${schema}'. Use letters, numbers, and underscores only.`);\n }\n return schema;\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sandbox-agent/persist-postgres",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "PostgreSQL persistence driver for the Sandbox Agent TypeScript SDK",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -18,7 +18,7 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "pg": "^8.16.3",
21
- "sandbox-agent": "0.3.0"
21
+ "sandbox-agent": "0.3.2"
22
22
  },
23
23
  "files": [
24
24
  "dist"