@conquext/pg 0.1.0

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Rasheed Alabi
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.
@@ -0,0 +1,51 @@
1
+ import { StorageBackend, UsageEvent, UsageQuery } from '@conquext/core';
2
+
3
+ interface PgConfig {
4
+ connectionString: string;
5
+ schema?: string;
6
+ }
7
+ interface PgRow {
8
+ id: string;
9
+ timestamp: Date;
10
+ model: string;
11
+ provider: string;
12
+ requested_model: string | null;
13
+ input_tokens: number;
14
+ output_tokens: number;
15
+ total_tokens: number;
16
+ cost: number;
17
+ latency_ms: number;
18
+ status: string;
19
+ error: string | null;
20
+ streaming: boolean | null;
21
+ time_to_first_token_ms: number | null;
22
+ cache_read_tokens: number | null;
23
+ cache_write_tokens: number | null;
24
+ pricing_miss: boolean | null;
25
+ user_id: string | null;
26
+ user_name: string | null;
27
+ user_email: string | null;
28
+ user_team: string | null;
29
+ user_plan: string | null;
30
+ task_id: string | null;
31
+ task_title: string | null;
32
+ task_type: string | null;
33
+ task_priority: string | null;
34
+ session_id: string | null;
35
+ conversation_id: string | null;
36
+ parent_call_id: string | null;
37
+ labels: Record<string, string>;
38
+ }
39
+ declare class PgBackend implements StorageBackend {
40
+ private sql;
41
+ private readonly schema;
42
+ private readonly connectionString;
43
+ constructor(config: PgConfig);
44
+ private initSql;
45
+ static eventToRow(event: UsageEvent): PgRow;
46
+ write(events: UsageEvent[]): Promise<void>;
47
+ query(query: UsageQuery): Promise<UsageEvent[]>;
48
+ close(): Promise<void>;
49
+ }
50
+
51
+ export { PgBackend, type PgConfig };
package/dist/index.js ADDED
@@ -0,0 +1,125 @@
1
+ // src/pg-backend.ts
2
+ var PgBackend = class _PgBackend {
3
+ sql;
4
+ schema;
5
+ connectionString;
6
+ constructor(config) {
7
+ this.schema = config.schema ?? "observatory";
8
+ this.connectionString = config.connectionString;
9
+ this.sql = null;
10
+ }
11
+ async initSql() {
12
+ if (this.sql) return;
13
+ const { default: postgres } = await import("postgres");
14
+ this.sql = postgres(this.connectionString);
15
+ }
16
+ static eventToRow(event) {
17
+ return {
18
+ id: event.id,
19
+ timestamp: event.timestamp,
20
+ model: event.model,
21
+ provider: event.provider,
22
+ requested_model: event.requested_model ?? null,
23
+ input_tokens: event.input_tokens,
24
+ output_tokens: event.output_tokens,
25
+ total_tokens: event.total_tokens,
26
+ cost: event.cost,
27
+ latency_ms: event.latency_ms,
28
+ status: event.status,
29
+ error: event.error ?? null,
30
+ streaming: event.streaming ?? null,
31
+ time_to_first_token_ms: event.time_to_first_token_ms ?? null,
32
+ cache_read_tokens: event.cache_read_tokens ?? null,
33
+ cache_write_tokens: event.cache_write_tokens ?? null,
34
+ pricing_miss: event.pricing_miss ?? null,
35
+ user_id: event.context?.user?.id ?? null,
36
+ user_name: event.context?.user?.name ?? null,
37
+ user_email: event.context?.user?.email ?? null,
38
+ user_team: event.context?.user?.team ?? null,
39
+ user_plan: event.context?.user?.plan ?? null,
40
+ task_id: event.context?.task?.id ?? null,
41
+ task_title: event.context?.task?.title ?? null,
42
+ task_type: event.context?.task?.type ?? null,
43
+ task_priority: event.context?.task?.priority ?? null,
44
+ session_id: event.context?.session?.id ?? null,
45
+ conversation_id: event.context?.session?.conversationId ?? null,
46
+ parent_call_id: event.context?.session?.parentCallId ?? null,
47
+ labels: event.context?.labels ?? {}
48
+ };
49
+ }
50
+ async write(events) {
51
+ await this.initSql();
52
+ const sql = this.sql;
53
+ const rows = events.map(_PgBackend.eventToRow);
54
+ await sql`
55
+ INSERT INTO ${sql(this.schema)}.usage_events ${sql(rows)}
56
+ `;
57
+ }
58
+ async query(query) {
59
+ await this.initSql();
60
+ const sql = this.sql;
61
+ const rows = await sql`
62
+ SELECT * FROM ${sql(this.schema)}.usage_events
63
+ WHERE timestamp >= ${query.from} AND timestamp <= ${query.to}
64
+ ${query.user ? sql`AND user_id = ${query.user}` : sql``}
65
+ ${query.model ? sql`AND model = ${query.model}` : sql``}
66
+ ${query.provider ? sql`AND provider = ${query.provider}` : sql``}
67
+ ${query.task ? sql`AND task_id = ${query.task}` : sql``}
68
+ ${query.session ? sql`AND session_id = ${query.session}` : sql``}
69
+ ORDER BY timestamp DESC
70
+ LIMIT 1000
71
+ `;
72
+ return rows.map(rowToEvent);
73
+ }
74
+ async close() {
75
+ if (!this.sql) return;
76
+ const sql = this.sql;
77
+ await sql.end();
78
+ }
79
+ };
80
+ function rowToEvent(row) {
81
+ return {
82
+ id: row.id,
83
+ timestamp: row.timestamp,
84
+ model: row.model,
85
+ provider: row.provider,
86
+ requested_model: row.requested_model || void 0,
87
+ input_tokens: row.input_tokens,
88
+ output_tokens: row.output_tokens,
89
+ total_tokens: row.total_tokens,
90
+ cost: Number(row.cost),
91
+ latency_ms: Number(row.latency_ms),
92
+ status: row.status,
93
+ error: row.error || void 0,
94
+ streaming: row.streaming || void 0,
95
+ time_to_first_token_ms: row.time_to_first_token_ms ? Number(row.time_to_first_token_ms) : void 0,
96
+ cache_read_tokens: row.cache_read_tokens || void 0,
97
+ cache_write_tokens: row.cache_write_tokens || void 0,
98
+ pricing_miss: row.pricing_miss || void 0,
99
+ context: {
100
+ user: row.user_id ? {
101
+ id: row.user_id,
102
+ name: row.user_name || void 0,
103
+ email: row.user_email || void 0,
104
+ team: row.user_team || void 0,
105
+ plan: row.user_plan || void 0
106
+ } : void 0,
107
+ task: row.task_id ? {
108
+ id: row.task_id,
109
+ title: row.task_title || void 0,
110
+ type: row.task_type || void 0,
111
+ priority: row.task_priority || void 0
112
+ } : void 0,
113
+ session: row.session_id ? {
114
+ id: row.session_id,
115
+ conversationId: row.conversation_id || void 0,
116
+ parentCallId: row.parent_call_id || void 0
117
+ } : void 0,
118
+ labels: row.labels || void 0
119
+ }
120
+ };
121
+ }
122
+ export {
123
+ PgBackend
124
+ };
125
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/pg-backend.ts"],"sourcesContent":["import type { StorageBackend, UsageEvent, UsageQuery } from '@conquext/core';\n\nexport interface PgConfig {\n connectionString: string;\n schema?: string;\n}\n\ninterface PgRow {\n id: string;\n timestamp: Date;\n model: string;\n provider: string;\n requested_model: string | null;\n input_tokens: number;\n output_tokens: number;\n total_tokens: number;\n cost: number;\n latency_ms: number;\n status: string;\n error: string | null;\n streaming: boolean | null;\n time_to_first_token_ms: number | null;\n cache_read_tokens: number | null;\n cache_write_tokens: number | null;\n pricing_miss: boolean | null;\n user_id: string | null;\n user_name: string | null;\n user_email: string | null;\n user_team: string | null;\n user_plan: string | null;\n task_id: string | null;\n task_title: string | null;\n task_type: string | null;\n task_priority: string | null;\n session_id: string | null;\n conversation_id: string | null;\n parent_call_id: string | null;\n labels: Record<string, string>;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype Sql = any;\n\nexport class PgBackend implements StorageBackend {\n private sql: Sql | null;\n private readonly schema: string;\n private readonly connectionString: string;\n\n constructor(config: PgConfig) {\n this.schema = config.schema ?? 'observatory';\n this.connectionString = config.connectionString;\n this.sql = null;\n }\n\n private async initSql(): Promise<void> {\n if (this.sql) return;\n const { default: postgres } = await import('postgres');\n this.sql = postgres(this.connectionString);\n }\n\n static eventToRow(event: UsageEvent): PgRow {\n return {\n id: event.id,\n timestamp: event.timestamp,\n model: event.model,\n provider: event.provider,\n requested_model: event.requested_model ?? null,\n input_tokens: event.input_tokens,\n output_tokens: event.output_tokens,\n total_tokens: event.total_tokens,\n cost: event.cost,\n latency_ms: event.latency_ms,\n status: event.status,\n error: event.error ?? null,\n streaming: event.streaming ?? null,\n time_to_first_token_ms: event.time_to_first_token_ms ?? null,\n cache_read_tokens: event.cache_read_tokens ?? null,\n cache_write_tokens: event.cache_write_tokens ?? null,\n pricing_miss: event.pricing_miss ?? null,\n user_id: event.context?.user?.id ?? null,\n user_name: event.context?.user?.name ?? null,\n user_email: event.context?.user?.email ?? null,\n user_team: event.context?.user?.team ?? null,\n user_plan: event.context?.user?.plan ?? null,\n task_id: event.context?.task?.id ?? null,\n task_title: event.context?.task?.title ?? null,\n task_type: event.context?.task?.type ?? null,\n task_priority: event.context?.task?.priority ?? null,\n session_id: event.context?.session?.id ?? null,\n conversation_id: event.context?.session?.conversationId ?? null,\n parent_call_id: event.context?.session?.parentCallId ?? null,\n labels: event.context?.labels ?? {},\n };\n }\n\n async write(events: UsageEvent[]): Promise<void> {\n await this.initSql();\n const sql = this.sql!;\n const rows = events.map(PgBackend.eventToRow);\n\n await sql`\n INSERT INTO ${sql(this.schema)}.usage_events ${sql(rows)}\n `;\n }\n\n async query(query: UsageQuery): Promise<UsageEvent[]> {\n await this.initSql();\n const sql = this.sql!;\n\n const rows = await sql`\n SELECT * FROM ${sql(this.schema)}.usage_events\n WHERE timestamp >= ${query.from} AND timestamp <= ${query.to}\n ${query.user ? sql`AND user_id = ${query.user}` : sql``}\n ${query.model ? sql`AND model = ${query.model}` : sql``}\n ${query.provider ? sql`AND provider = ${query.provider}` : sql``}\n ${query.task ? sql`AND task_id = ${query.task}` : sql``}\n ${query.session ? sql`AND session_id = ${query.session}` : sql``}\n ORDER BY timestamp DESC\n LIMIT 1000\n `;\n\n return rows.map(rowToEvent);\n }\n\n async close(): Promise<void> {\n if (!this.sql) return;\n const sql = this.sql;\n await sql.end();\n }\n}\n\nfunction rowToEvent(row: Record<string, unknown>): UsageEvent {\n return {\n id: row.id as string,\n timestamp: row.timestamp as Date,\n model: row.model as string,\n provider: row.provider as string,\n requested_model: (row.requested_model as string) || undefined,\n input_tokens: row.input_tokens as number,\n output_tokens: row.output_tokens as number,\n total_tokens: row.total_tokens as number,\n cost: Number(row.cost),\n latency_ms: Number(row.latency_ms),\n status: row.status as 'success' | 'error',\n error: (row.error as string) || undefined,\n streaming: (row.streaming as boolean) || undefined,\n time_to_first_token_ms: row.time_to_first_token_ms ? Number(row.time_to_first_token_ms) : undefined,\n cache_read_tokens: (row.cache_read_tokens as number) || undefined,\n cache_write_tokens: (row.cache_write_tokens as number) || undefined,\n pricing_miss: (row.pricing_miss as boolean) || undefined,\n context: {\n user: row.user_id ? {\n id: row.user_id as string,\n name: (row.user_name as string) || undefined,\n email: (row.user_email as string) || undefined,\n team: (row.user_team as string) || undefined,\n plan: (row.user_plan as string) || undefined,\n } : undefined,\n task: row.task_id ? {\n id: row.task_id as string,\n title: (row.task_title as string) || undefined,\n type: (row.task_type as string) || undefined,\n priority: (row.task_priority as 'low' | 'medium' | 'high' | 'critical') || undefined,\n } : undefined,\n session: row.session_id ? {\n id: row.session_id as string,\n conversationId: (row.conversation_id as string) || undefined,\n parentCallId: (row.parent_call_id as string) || undefined,\n } : undefined,\n labels: (row.labels as Record<string, string>) || undefined,\n },\n };\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@conquext/pg",
3
+ "version": "0.1.0",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "type": "module",
8
+ "main": "dist/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "src/migrations"
19
+ ],
20
+ "dependencies": {
21
+ "@conquext/core": "0.1.0"
22
+ },
23
+ "peerDependencies": {
24
+ "postgres": ">=3.0.0"
25
+ },
26
+ "peerDependenciesMeta": {
27
+ "postgres": {
28
+ "optional": true
29
+ }
30
+ },
31
+ "scripts": {
32
+ "build": "tsup",
33
+ "typecheck": "tsc --noEmit",
34
+ "test": "vitest run"
35
+ }
36
+ }
@@ -0,0 +1,57 @@
1
+ CREATE SCHEMA IF NOT EXISTS observatory;
2
+
3
+ CREATE TABLE IF NOT EXISTS observatory.usage_events (
4
+ id TEXT PRIMARY KEY,
5
+ timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
6
+ model TEXT NOT NULL,
7
+ provider TEXT NOT NULL,
8
+ requested_model TEXT,
9
+ input_tokens INTEGER NOT NULL,
10
+ output_tokens INTEGER NOT NULL,
11
+ total_tokens INTEGER NOT NULL,
12
+ cost NUMERIC(12, 6) NOT NULL DEFAULT 0,
13
+ latency_ms NUMERIC(10, 2) NOT NULL,
14
+ status TEXT NOT NULL CHECK (status IN ('success', 'error')),
15
+ error TEXT,
16
+ streaming BOOLEAN,
17
+ time_to_first_token_ms NUMERIC(10, 2),
18
+ cache_read_tokens INTEGER,
19
+ cache_write_tokens INTEGER,
20
+ pricing_miss BOOLEAN,
21
+ user_id TEXT,
22
+ user_name TEXT,
23
+ user_email TEXT,
24
+ user_team TEXT,
25
+ user_plan TEXT,
26
+ task_id TEXT,
27
+ task_title TEXT,
28
+ task_type TEXT,
29
+ task_priority TEXT,
30
+ session_id TEXT,
31
+ conversation_id TEXT,
32
+ parent_call_id TEXT,
33
+ labels JSONB DEFAULT '{}'::jsonb,
34
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
35
+ ) PARTITION BY RANGE (timestamp);
36
+
37
+ -- Create initial monthly partitions (3 months)
38
+ DO $$
39
+ DECLARE
40
+ start_date DATE := DATE_TRUNC('month', CURRENT_DATE);
41
+ BEGIN
42
+ FOR i IN 0..2 LOOP
43
+ EXECUTE format(
44
+ 'CREATE TABLE IF NOT EXISTS observatory.usage_events_%s PARTITION OF observatory.usage_events FOR VALUES FROM (%L) TO (%L)',
45
+ TO_CHAR(start_date + (i || ' months')::INTERVAL, 'YYYY_MM'),
46
+ start_date + (i || ' months')::INTERVAL,
47
+ start_date + ((i + 1) || ' months')::INTERVAL
48
+ );
49
+ END LOOP;
50
+ END $$;
51
+
52
+ -- Indexes for common queries
53
+ CREATE INDEX IF NOT EXISTS idx_usage_events_user_id ON observatory.usage_events (user_id, timestamp);
54
+ CREATE INDEX IF NOT EXISTS idx_usage_events_model ON observatory.usage_events (model, timestamp);
55
+ CREATE INDEX IF NOT EXISTS idx_usage_events_provider ON observatory.usage_events (provider, timestamp);
56
+ CREATE INDEX IF NOT EXISTS idx_usage_events_task_id ON observatory.usage_events (task_id, timestamp);
57
+ CREATE INDEX IF NOT EXISTS idx_usage_events_team ON observatory.usage_events (user_team, timestamp);