@chatcops/store-postgres 0.0.0-stage → 0.2.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.
@@ -0,0 +1,98 @@
1
+ import { json, ms, toTime } from './sql.js';
2
+ const DAY_MS = 24 * 60 * 60 * 1000;
3
+ const HOUR_MS = 60 * 60 * 1000;
4
+ /** One upsert per increment, so concurrent increments never lose a count. */
5
+ export class PostgresCounterStore {
6
+ db;
7
+ t;
8
+ now;
9
+ constructor(db, t, now) {
10
+ this.db = db;
11
+ this.t = t;
12
+ this.now = now;
13
+ }
14
+ async increment(key, by, ttlMs) {
15
+ const now = this.now();
16
+ const { rows } = await this.db.query(`INSERT INTO ${this.t.counters} AS k (key, value, expires_at)
17
+ VALUES ($1, $2::float8, ${toTime('$4')})
18
+ ON CONFLICT (key) DO UPDATE SET
19
+ value = CASE WHEN k.expires_at <= ${toTime('$3')} THEN EXCLUDED.value ELSE k.value + EXCLUDED.value END,
20
+ expires_at = CASE WHEN k.expires_at <= ${toTime('$3')} THEN EXCLUDED.expires_at ELSE k.expires_at END
21
+ RETURNING value, ${ms('expires_at')} AS expires_ms`, [key, by, now, now + ttlMs]);
22
+ return { value: Number(rows[0].value), expiresAt: Number(rows[0].expires_ms) };
23
+ }
24
+ async get(key) {
25
+ const { rows } = await this.db.query(`SELECT value, ${ms('expires_at')} AS expires_ms FROM ${this.t.counters}
26
+ WHERE key = $1 AND expires_at > ${toTime('$2')}`, [key, this.now()]);
27
+ return rows[0] ? { value: Number(rows[0].value), expiresAt: Number(rows[0].expires_ms) } : null;
28
+ }
29
+ }
30
+ /** JSON values with an optional expiry. Expired rows are ignored, and cleared by `purge()`. */
31
+ export class PostgresKeyValueStore {
32
+ db;
33
+ t;
34
+ now;
35
+ constructor(db, t, now) {
36
+ this.db = db;
37
+ this.t = t;
38
+ this.now = now;
39
+ }
40
+ expiry(ttlMs) {
41
+ return ttlMs === undefined ? null : this.now() + ttlMs;
42
+ }
43
+ async get(key) {
44
+ const { rows } = await this.db.query(`SELECT value FROM ${this.t.kv}
45
+ WHERE key = $1 AND (expires_at IS NULL OR expires_at > ${toTime('$2')})`, [key, this.now()]);
46
+ return rows[0] ? rows[0].value : null;
47
+ }
48
+ async set(key, value, options = {}) {
49
+ await this.db.query(`INSERT INTO ${this.t.kv} (key, value, expires_at)
50
+ VALUES ($1, $2::text::jsonb, CASE WHEN $3::float8 IS NULL THEN NULL ELSE ${toTime('$3')} END)
51
+ ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, expires_at = EXCLUDED.expires_at`, [key, json(value) ?? 'null', this.expiry(options.ttlMs)]);
52
+ }
53
+ async setIfAbsent(key, value, options = {}) {
54
+ // Inserts, or takes over an expired row. A live row is left alone.
55
+ const { rows } = await this.db.query(`INSERT INTO ${this.t.kv} AS k (key, value, expires_at)
56
+ VALUES ($1, $2::text::jsonb, CASE WHEN $3::float8 IS NULL THEN NULL ELSE ${toTime('$3')} END)
57
+ ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, expires_at = EXCLUDED.expires_at
58
+ WHERE k.expires_at IS NOT NULL AND k.expires_at <= ${toTime('$4')}
59
+ RETURNING 1`, [key, json(value) ?? 'null', this.expiry(options.ttlMs), this.now()]);
60
+ return rows.length > 0;
61
+ }
62
+ async delete(key) {
63
+ await this.db.query(`DELETE FROM ${this.t.kv} WHERE key = $1`, [key]);
64
+ }
65
+ }
66
+ /** Events as rows, counted with SQL. Writes go in batches of up to 500 rows. */
67
+ export class PostgresEventSink {
68
+ db;
69
+ t;
70
+ constructor(db, t) {
71
+ this.db = db;
72
+ this.t = t;
73
+ }
74
+ async write(events) {
75
+ for (let start = 0; start < events.length; start += 500) {
76
+ const batch = events.slice(start, start + 500);
77
+ const params = [];
78
+ const rows = batch.map((event) => {
79
+ const conversationId = typeof event.data?.conversationId === 'string' ? event.data.conversationId : null;
80
+ params.push(event.type, conversationId, json(event.data), event.timestamp);
81
+ const n = params.length;
82
+ return `($${n - 3}::text, $${n - 2}::text, $${n - 1}::text::jsonb, ${toTime(`$${n}`)})`;
83
+ });
84
+ await this.db.query(`INSERT INTO ${this.t.events} (type, conversation_id, data, created_at) VALUES ${rows.join(', ')}`, params);
85
+ }
86
+ }
87
+ async query(query) {
88
+ const size = query.groupBy === 'hour' ? HOUR_MS : DAY_MS;
89
+ const { rows } = await this.db.query(`SELECT (floor(${ms('created_at')} / $3::float8) * $3::float8)::float8 AS bucket, type, count(*)::int AS count
90
+ FROM ${this.t.events}
91
+ WHERE created_at >= ${toTime('$1')} AND created_at < ${toTime('$2')}
92
+ AND ($4::text IS NULL OR type = $4::text)
93
+ GROUP BY 1, 2
94
+ ORDER BY bucket, type COLLATE "C"`, [query.from, query.to, size, query.type ?? null]);
95
+ return rows.map((row) => ({ bucket: Number(row.bucket), type: row.type, count: Number(row.count) }));
96
+ }
97
+ }
98
+ //# sourceMappingURL=counters.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"counters.js","sourceRoot":"","sources":["../src/counters.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ import type { ChatCopsStorage } from '@chatcops/core';
2
+ import type { Queryable } from './queryable.js';
3
+ import { type ExtraColumns, type RowHookFunction } from './row-hook.js';
4
+ export { fromNeon, fromPostgresJs, type NeonSql, type PostgresJsSql, type Queryable } from './queryable.js';
5
+ export { migrate, migrateVectors, MIGRATIONS, tableNames, type MigrateOptions, type MigrateVectorsOptions, type TableNames } from './schema.js';
6
+ export { PostgresConversationStore } from './conversations.js';
7
+ export type { ExtraColumns, RowHookContext, RowHookFunction } from './row-hook.js';
8
+ export { PostgresCounterStore, PostgresEventSink, PostgresKeyValueStore } from './counters.js';
9
+ export { PostgresVectorStore } from './vectors.js';
10
+ export interface PostgresStorageOptions {
11
+ /** A `pg` `Pool`, or a wrapped postgres.js or Neon client (`fromPostgresJs`, `fromNeon`). */
12
+ db: Queryable;
13
+ /** Default `chatcops_`. Lets several apps share one database. */
14
+ tablePrefix?: string;
15
+ /** Set this to expose pgvector storage; call `migrateVectors` separately. */
16
+ dimensions?: number;
17
+ /**
18
+ * Columns you've added to the conversations and messages tables (with your
19
+ * own migration), filled in by `rowHook` when a row is created. Names, or
20
+ * names and SQL types: `{ workspace_id: 'uuid' }`.
21
+ */
22
+ extraColumns?: ExtraColumns;
23
+ /**
24
+ * Values for `extraColumns` on each new conversation and message row. Runs
25
+ * on insert only, never on update. Returning a key that isn't declared in
26
+ * `extraColumns` throws. Values are sent as query parameters, never put
27
+ * into the SQL.
28
+ */
29
+ rowHook?: RowHookFunction;
30
+ /** Clock, for tests. */
31
+ now?: () => number;
32
+ }
33
+ /**
34
+ * Everything ChatCops stores, in Postgres: conversations, rate limit
35
+ * counters, keys (turn locks), analytics events and optional pgvector chunks.
36
+ * Run `migrate(db)` before first use and `migrateVectors(db, { dimensions })`
37
+ * separately when vector storage is enabled.
38
+ *
39
+ * Every write is a single statement. Filtered vector searches use a
40
+ * transaction for pgvector's iterative HNSW scan setting.
41
+ */
42
+ export declare function postgresStorage(options: PostgresStorageOptions): ChatCopsStorage;
43
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAGtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAgB,KAAK,YAAY,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAItF,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,KAAK,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC5G,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,UAAU,EAAE,KAAK,cAAc,EAAE,KAAK,qBAAqB,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAChJ,OAAO,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAC/D,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACnF,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAC/F,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAEnD,MAAM,WAAW,sBAAsB;IACrC,6FAA6F;IAC7F,EAAE,EAAE,SAAS,CAAC;IACd,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B;;;;;OAKG;IACH,OAAO,CAAC,EAAE,eAAe,CAAC;IAC1B,wBAAwB;IACxB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,eAAe,CAUhF"}
package/dist/index.js ADDED
@@ -0,0 +1,31 @@
1
+ import { PostgresConversationStore } from './conversations.js';
2
+ import { PostgresCounterStore, PostgresEventSink, PostgresKeyValueStore } from './counters.js';
3
+ import { checkRowHook } from './row-hook.js';
4
+ import { tableNames } from './schema.js';
5
+ import { PostgresVectorStore } from './vectors.js';
6
+ export { fromNeon, fromPostgresJs } from './queryable.js';
7
+ export { migrate, migrateVectors, MIGRATIONS, tableNames } from './schema.js';
8
+ export { PostgresConversationStore } from './conversations.js';
9
+ export { PostgresCounterStore, PostgresEventSink, PostgresKeyValueStore } from './counters.js';
10
+ export { PostgresVectorStore } from './vectors.js';
11
+ /**
12
+ * Everything ChatCops stores, in Postgres: conversations, rate limit
13
+ * counters, keys (turn locks), analytics events and optional pgvector chunks.
14
+ * Run `migrate(db)` before first use and `migrateVectors(db, { dimensions })`
15
+ * separately when vector storage is enabled.
16
+ *
17
+ * Every write is a single statement. Filtered vector searches use a
18
+ * transaction for pgvector's iterative HNSW scan setting.
19
+ */
20
+ export function postgresStorage(options) {
21
+ const t = tableNames(options.tablePrefix);
22
+ const now = options.now ?? Date.now;
23
+ return {
24
+ conversations: new PostgresConversationStore(options.db, t, now, checkRowHook(options.extraColumns, options.rowHook)),
25
+ counters: new PostgresCounterStore(options.db, t, now),
26
+ kv: new PostgresKeyValueStore(options.db, t, now),
27
+ events: new PostgresEventSink(options.db, t),
28
+ ...(options.dimensions !== undefined ? { vectors: new PostgresVectorStore(options.dimensions, options.db, t.vectorChunks) } : {}),
29
+ };
30
+ }
31
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAC/D,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAE/F,OAAO,EAAE,YAAY,EAA2C,MAAM,eAAe,CAAC;AACtF,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAEnD,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAoD,MAAM,gBAAgB,CAAC;AAC5G,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,UAAU,EAAoE,MAAM,aAAa,CAAC;AAChJ,OAAO,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAE/D,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAC/F,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AA0BnD;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAAC,OAA+B;IAC7D,MAAM,CAAC,GAAG,UAAU,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IAC1C,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC;IACpC,OAAO;QACL,aAAa,EAAE,IAAI,yBAAyB,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC,EAAE,GAAG,EAAE,YAAY,CAAC,OAAO,CAAC,YAAY,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;QACrH,QAAQ,EAAE,IAAI,oBAAoB,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC,EAAE,GAAG,CAAC;QACtD,EAAE,EAAE,IAAI,qBAAqB,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC,EAAE,GAAG,CAAC;QACjD,MAAM,EAAE,IAAI,iBAAiB,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC;QAC5C,GAAG,CAAC,OAAO,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,mBAAmB,CAAC,OAAO,CAAC,UAAU,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAClI,CAAC;AACJ,CAAC"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Anything that runs a parameterized query and returns rows. A `pg` `Pool`
3
+ * or `Client` fits as is. Filtered vector searches use an optional transaction
4
+ * callback, or acquire a connection from a `pg` Pool.
5
+ */
6
+ export interface Queryable {
7
+ query<T = Record<string, unknown>>(text: string, params?: unknown[]): Promise<{
8
+ rows: T[];
9
+ }>;
10
+ /** Runs the callback on one transaction-bound connection when supported. */
11
+ transaction?<T>(run: (db: Queryable) => Promise<T>): Promise<T>;
12
+ }
13
+ /** The part of a postgres.js `sql` instance the store uses. */
14
+ export interface PostgresJsSql {
15
+ unsafe(text: string, params?: never[]): PromiseLike<unknown>;
16
+ begin?<T>(run: (sql: PostgresJsSql) => Promise<T>): Promise<T>;
17
+ }
18
+ /** Wraps a postgres.js client: `fromPostgresJs(postgres(url))`. */
19
+ export declare function fromPostgresJs(sql: PostgresJsSql): Queryable;
20
+ /** The part of a Neon serverless `sql` function the store uses. */
21
+ export interface NeonSql {
22
+ query(text: string, params?: unknown[]): Promise<unknown>;
23
+ }
24
+ /** Wraps the Neon serverless driver: `fromNeon(neon(process.env.DATABASE_URL))`. */
25
+ export declare function fromNeon(sql: NeonSql): Queryable;
26
+ //# sourceMappingURL=queryable.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queryable.d.ts","sourceRoot":"","sources":["../src/queryable.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,KAAK,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,CAAC,EAAE,CAAA;KAAE,CAAC,CAAC;IAC7F,4EAA4E;IAC5E,WAAW,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE,SAAS,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CACjE;AAED,+DAA+D;AAC/D,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,KAAK,EAAE,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;IAC7D,KAAK,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,EAAE,aAAa,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CAChE;AAED,mEAAmE;AACnE,wBAAgB,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,SAAS,CAa5D;AAED,mEAAmE;AACnE,MAAM,WAAW,OAAO;IACtB,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC3D;AAED,oFAAoF;AACpF,wBAAgB,QAAQ,CAAC,GAAG,EAAE,OAAO,GAAG,SAAS,CAQhD"}
@@ -0,0 +1,25 @@
1
+ /** Wraps a postgres.js client: `fromPostgresJs(postgres(url))`. */
2
+ export function fromPostgresJs(sql) {
3
+ const db = {
4
+ async query(text, params = []) {
5
+ const rows = (await sql.unsafe(text, params));
6
+ return { rows: [...rows] };
7
+ },
8
+ };
9
+ const begin = sql.begin;
10
+ if (begin) {
11
+ db.transaction = async (run) => (await begin.call(sql, (tx) => run(fromPostgresJs(tx))));
12
+ }
13
+ return db;
14
+ }
15
+ /** Wraps the Neon serverless driver: `fromNeon(neon(process.env.DATABASE_URL))`. */
16
+ export function fromNeon(sql) {
17
+ return {
18
+ async query(text, params = []) {
19
+ const result = await sql.query(text, params);
20
+ const rows = Array.isArray(result) ? result : (result.rows ?? []);
21
+ return { rows: rows };
22
+ },
23
+ };
24
+ }
25
+ //# sourceMappingURL=queryable.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queryable.js","sourceRoot":"","sources":["../src/queryable.ts"],"names":[],"mappings":"AAiBA,mEAAmE;AACnE,MAAM,UAAU,cAAc,CAAC,GAAkB;IAC/C,MAAM,EAAE,GAAc;QACpB,KAAK,CAAC,KAAK,CAAI,IAAY,EAAE,SAAoB,EAAE;YACjD,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,MAAiB,CAAC,CAAQ,CAAC;YAChE,OAAO,EAAE,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC;QAC7B,CAAC;KACF,CAAC;IACF,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC;IACxB,IAAI,KAAK,EAAE,CAAC;QACV,EAAE,CAAC,WAAW,GAAG,KAAK,EAAK,GAAkC,EAAE,EAAE,CAC/D,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC,CAAC,CAAM,CAAC;IAClE,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAOD,oFAAoF;AACpF,MAAM,UAAU,QAAQ,CAAC,GAAY;IACnC,OAAO;QACL,KAAK,CAAC,KAAK,CAAI,IAAY,EAAE,SAAoB,EAAE;YACjD,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;YAC7C,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAE,MAAyB,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;YACtF,OAAO,EAAE,IAAI,EAAE,IAAW,EAAE,CAAC;QAC/B,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,26 @@
1
+ import type { ChatMessage } from '@chatcops/core';
2
+ /** What the hook gets for each new row. */
3
+ export interface RowHookContext {
4
+ table: 'conversations' | 'messages';
5
+ conversationId: string;
6
+ /** The metadata passed with this write (`append` options or the saved conversation). */
7
+ metadata?: Record<string, unknown>;
8
+ /** The message being inserted, for the messages table. */
9
+ message?: ChatMessage;
10
+ }
11
+ export type RowHookFunction = (context: RowHookContext) => Record<string, unknown>;
12
+ /**
13
+ * Columns you've added to the conversations and messages tables, by name, or
14
+ * by name and SQL type (default `text`), for example
15
+ * `{ workspace_id: 'uuid', site_id: 'uuid' }`.
16
+ */
17
+ export type ExtraColumns = string[] | Record<string, string>;
18
+ /** A checked hook: the columns it may set, and a function that refuses any others. */
19
+ export interface RowHook {
20
+ columns: Array<[name: string, type: string]>;
21
+ /** `, a, b` for an INSERT column list. */
22
+ columnList: string;
23
+ values(context: RowHookContext): Record<string, unknown>;
24
+ }
25
+ export declare function checkRowHook(extraColumns: ExtraColumns | undefined, rowHook: RowHookFunction | undefined): RowHook | undefined;
26
+ //# sourceMappingURL=row-hook.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"row-hook.d.ts","sourceRoot":"","sources":["../src/row-hook.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD,2CAA2C;AAC3C,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,eAAe,GAAG,UAAU,CAAC;IACpC,cAAc,EAAE,MAAM,CAAC;IACvB,wFAAwF;IACxF,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,0DAA0D;IAC1D,OAAO,CAAC,EAAE,WAAW,CAAC;CACvB;AAED,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,EAAE,cAAc,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEnF;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAS7D,sFAAsF;AACtF,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;IAC7C,0CAA0C;IAC1C,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC1D;AAED,wBAAgB,YAAY,CAAC,YAAY,EAAE,YAAY,GAAG,SAAS,EAAE,OAAO,EAAE,eAAe,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,CA2B9H"}
@@ -0,0 +1,38 @@
1
+ const IDENTIFIER = /^[a-z_][a-z0-9_]{0,62}$/;
2
+ const SQL_TYPE = /^[a-z][a-z0-9_ ]{0,40}(\[\])?$/;
3
+ const RESERVED = new Set([
4
+ 'id', 'metadata', 'version', 'next_seq', 'created_at', 'updated_at',
5
+ 'conversation_id', 'seq', 'role', 'content', 'parts', 'status', 'usage', 'ai_generated', 'extra', 'sent_at',
6
+ ]);
7
+ export function checkRowHook(extraColumns, rowHook) {
8
+ if (!rowHook && !extraColumns)
9
+ return undefined;
10
+ if (!rowHook || !extraColumns)
11
+ throw new Error('postgresStorage needs both extraColumns and rowHook, or neither.');
12
+ const columns = Array.isArray(extraColumns)
13
+ ? extraColumns.map((name) => [name, 'text'])
14
+ : Object.entries(extraColumns);
15
+ for (const [name, type] of columns) {
16
+ if (!IDENTIFIER.test(name))
17
+ throw new Error(`extraColumns: "${name}" isn't a plain lowercase column name.`);
18
+ if (RESERVED.has(name))
19
+ throw new Error(`extraColumns: "${name}" is one of the store's own columns.`);
20
+ if (!SQL_TYPE.test(type))
21
+ throw new Error(`extraColumns: "${type}" isn't a SQL type name.`);
22
+ }
23
+ const allowed = new Set(columns.map(([name]) => name));
24
+ return {
25
+ columns,
26
+ columnList: columns.map(([name]) => `, ${name}`).join(''),
27
+ values(context) {
28
+ const row = rowHook(context) ?? {};
29
+ for (const key of Object.keys(row)) {
30
+ if (!allowed.has(key)) {
31
+ throw new Error(`rowHook returned "${key}", which isn't in extraColumns. Declare it there first.`);
32
+ }
33
+ }
34
+ return row;
35
+ },
36
+ };
37
+ }
38
+ //# sourceMappingURL=row-hook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"row-hook.js","sourceRoot":"","sources":["../src/row-hook.ts"],"names":[],"mappings":"AAqBA,MAAM,UAAU,GAAG,yBAAyB,CAAC;AAC7C,MAAM,QAAQ,GAAG,gCAAgC,CAAC;AAClD,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC;IACvB,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,YAAY,EAAE,YAAY;IACnE,iBAAiB,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,SAAS;CAC5G,CAAC,CAAC;AAUH,MAAM,UAAU,YAAY,CAAC,YAAsC,EAAE,OAAoC;IACvG,IAAI,CAAC,OAAO,IAAI,CAAC,YAAY;QAAE,OAAO,SAAS,CAAC;IAChD,IAAI,CAAC,OAAO,IAAI,CAAC,YAAY;QAAE,MAAM,IAAI,KAAK,CAAC,kEAAkE,CAAC,CAAC;IAEnH,MAAM,OAAO,GAA4B,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC;QAClE,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAC5C,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;IACjC,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,OAAO,EAAE,CAAC;QACnC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,kBAAkB,IAAI,wCAAwC,CAAC,CAAC;QAC5G,IAAI,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,kBAAkB,IAAI,sCAAsC,CAAC,CAAC;QACtG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,kBAAkB,IAAI,0BAA0B,CAAC,CAAC;IAC9F,CAAC;IACD,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;IAEvD,OAAO;QACL,OAAO;QACP,UAAU,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;QACzD,MAAM,CAAC,OAAO;YACZ,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;YACnC,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;gBACnC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;oBACtB,MAAM,IAAI,KAAK,CAAC,qBAAqB,GAAG,yDAAyD,CAAC,CAAC;gBACrG,CAAC;YACH,CAAC;YACD,OAAO,GAAG,CAAC;QACb,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,44 @@
1
+ import type { Queryable } from './queryable.js';
2
+ export interface TableNames {
3
+ conversations: string;
4
+ messages: string;
5
+ counters: string;
6
+ kv: string;
7
+ events: string;
8
+ migrations: string;
9
+ vectorChunks: string;
10
+ }
11
+ /** Table names for a prefix. The prefix must be a plain lowercase identifier. */
12
+ export declare function tableNames(prefix?: string): TableNames;
13
+ interface Migration {
14
+ id: number;
15
+ name: string;
16
+ statements(t: TableNames): string[];
17
+ }
18
+ /**
19
+ * The schema, one migration at a time. Statements are idempotent, so running
20
+ * a migration twice is harmless. Times are `timestamptz`; the store reads and
21
+ * writes them as milliseconds.
22
+ */
23
+ export declare const MIGRATIONS: Migration[];
24
+ export interface MigrateOptions {
25
+ /** Default `chatcops_`. Use the same prefix as `postgresStorage`. */
26
+ tablePrefix?: string;
27
+ }
28
+ export interface MigrateVectorsOptions {
29
+ /** Embedding length used for the vector column. */
30
+ dimensions: number;
31
+ /** Default `chatcops_`. Use the same prefix as `postgresStorage`. */
32
+ tablePrefix?: string;
33
+ }
34
+ /** Installs pgvector and creates the optional vector index, independently of `migrate`. */
35
+ export declare function migrateVectors(db: Queryable, options: MigrateVectorsOptions): Promise<void>;
36
+ /**
37
+ * Creates or updates the tables. Safe to run on every deploy: migrations
38
+ * already applied are skipped. Returns the ids it applied.
39
+ */
40
+ export declare function migrate(db: Queryable, options?: MigrateOptions): Promise<{
41
+ applied: number[];
42
+ }>;
43
+ export {};
44
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,MAAM,WAAW,UAAU;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;CACtB;AAID,iFAAiF;AACjF,wBAAgB,UAAU,CAAC,MAAM,SAAc,GAAG,UAAU,CAa3D;AAED,UAAU,SAAS;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,CAAC,EAAE,UAAU,GAAG,MAAM,EAAE,CAAC;CACrC;AAED;;;;GAIG;AACH,eAAO,MAAM,UAAU,EAAE,SAAS,EAwDjC,CAAC;AAEF,MAAM,WAAW,cAAc;IAC7B,qEAAqE;IACrE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,qBAAqB;IACpC,mDAAmD;IACnD,UAAU,EAAE,MAAM,CAAC;IACnB,qEAAqE;IACrE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,2FAA2F;AAC3F,wBAAsB,cAAc,CAAC,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAkBjG;AAED;;;GAGG;AACH,wBAAsB,OAAO,CAAC,EAAE,EAAE,SAAS,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAuBzG"}
package/dist/schema.js ADDED
@@ -0,0 +1,126 @@
1
+ const IDENTIFIER = /^[a-z_][a-z0-9_]{0,40}$/;
2
+ /** Table names for a prefix. The prefix must be a plain lowercase identifier. */
3
+ export function tableNames(prefix = 'chatcops_') {
4
+ if (!IDENTIFIER.test(prefix)) {
5
+ throw new Error(`tablePrefix must be lowercase letters, digits and underscores, starting with a letter or underscore. Got "${prefix}".`);
6
+ }
7
+ return {
8
+ conversations: `${prefix}conversations`,
9
+ messages: `${prefix}messages`,
10
+ counters: `${prefix}counters`,
11
+ kv: `${prefix}kv`,
12
+ events: `${prefix}events`,
13
+ migrations: `${prefix}migrations`,
14
+ vectorChunks: `${prefix}vector_chunks`,
15
+ };
16
+ }
17
+ /**
18
+ * The schema, one migration at a time. Statements are idempotent, so running
19
+ * a migration twice is harmless. Times are `timestamptz`; the store reads and
20
+ * writes them as milliseconds.
21
+ */
22
+ export const MIGRATIONS = [
23
+ {
24
+ id: 1,
25
+ name: 'create tables',
26
+ statements: (t) => [
27
+ `CREATE TABLE IF NOT EXISTS ${t.conversations} (
28
+ id text PRIMARY KEY,
29
+ metadata jsonb,
30
+ version integer NOT NULL DEFAULT 0,
31
+ -- Sequence numbers handed out to messages so far. Only ever grows, so
32
+ -- concurrent appends never pick the same number.
33
+ next_seq integer NOT NULL DEFAULT 0,
34
+ created_at timestamptz NOT NULL,
35
+ updated_at timestamptz NOT NULL
36
+ )`,
37
+ `CREATE INDEX IF NOT EXISTS ${t.conversations}_updated_at ON ${t.conversations} (updated_at DESC, id)`,
38
+ `CREATE TABLE IF NOT EXISTS ${t.messages} (
39
+ conversation_id text NOT NULL REFERENCES ${t.conversations} (id) ON DELETE CASCADE,
40
+ seq integer NOT NULL,
41
+ id text NOT NULL,
42
+ role text NOT NULL,
43
+ content text NOT NULL,
44
+ parts jsonb,
45
+ status text,
46
+ usage jsonb,
47
+ ai_generated boolean,
48
+ metadata jsonb,
49
+ -- Message fields this version of the store doesn't know about.
50
+ extra jsonb,
51
+ sent_at timestamptz NOT NULL,
52
+ created_at timestamptz NOT NULL DEFAULT now(),
53
+ PRIMARY KEY (conversation_id, seq)
54
+ )`,
55
+ `CREATE INDEX IF NOT EXISTS ${t.messages}_message_id ON ${t.messages} (conversation_id, id)`,
56
+ `CREATE TABLE IF NOT EXISTS ${t.counters} (
57
+ key text PRIMARY KEY,
58
+ value double precision NOT NULL,
59
+ expires_at timestamptz NOT NULL
60
+ )`,
61
+ `CREATE INDEX IF NOT EXISTS ${t.counters}_expires_at ON ${t.counters} (expires_at)`,
62
+ `CREATE TABLE IF NOT EXISTS ${t.kv} (
63
+ key text PRIMARY KEY,
64
+ value jsonb NOT NULL,
65
+ expires_at timestamptz
66
+ )`,
67
+ `CREATE INDEX IF NOT EXISTS ${t.kv}_expires_at ON ${t.kv} (expires_at) WHERE expires_at IS NOT NULL`,
68
+ `CREATE TABLE IF NOT EXISTS ${t.events} (
69
+ id bigserial PRIMARY KEY,
70
+ type text NOT NULL,
71
+ conversation_id text,
72
+ data jsonb,
73
+ created_at timestamptz NOT NULL
74
+ )`,
75
+ `CREATE INDEX IF NOT EXISTS ${t.events}_type_time ON ${t.events} (type, created_at)`,
76
+ ],
77
+ },
78
+ ];
79
+ /** Installs pgvector and creates the optional vector index, independently of `migrate`. */
80
+ export async function migrateVectors(db, options) {
81
+ if (!Number.isInteger(options.dimensions) || options.dimensions < 1 || options.dimensions > 16_000) {
82
+ throw new Error(`dimensions must be a whole number from 1 to 16000; got ${options.dimensions}.`);
83
+ }
84
+ const t = tableNames(options.tablePrefix);
85
+ await db.query('CREATE EXTENSION IF NOT EXISTS vector');
86
+ await db.query(`CREATE TABLE IF NOT EXISTS ${t.vectorChunks} (
87
+ id text PRIMARY KEY,
88
+ source_id text NOT NULL,
89
+ source jsonb NOT NULL,
90
+ text text NOT NULL,
91
+ metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
92
+ embedding vector(${options.dimensions}) NOT NULL,
93
+ keyword_vector tsvector NOT NULL
94
+ )`);
95
+ await db.query(`CREATE INDEX IF NOT EXISTS ${t.vectorChunks}_hnsw ON ${t.vectorChunks} USING hnsw (embedding vector_cosine_ops)`);
96
+ await db.query(`CREATE INDEX IF NOT EXISTS ${t.vectorChunks}_src ON ${t.vectorChunks} (source_id)`);
97
+ await db.query(`CREATE INDEX IF NOT EXISTS ${t.vectorChunks}_fts ON ${t.vectorChunks} USING gin (keyword_vector)`);
98
+ }
99
+ /**
100
+ * Creates or updates the tables. Safe to run on every deploy: migrations
101
+ * already applied are skipped. Returns the ids it applied.
102
+ */
103
+ export async function migrate(db, options = {}) {
104
+ const t = tableNames(options.tablePrefix);
105
+ await db.query(`CREATE TABLE IF NOT EXISTS ${t.migrations} (
106
+ id integer PRIMARY KEY,
107
+ name text NOT NULL,
108
+ applied_at timestamptz NOT NULL DEFAULT now()
109
+ )`);
110
+ const { rows } = await db.query(`SELECT id FROM ${t.migrations}`);
111
+ const done = new Set(rows.map((row) => Number(row.id)));
112
+ const applied = [];
113
+ for (const migration of MIGRATIONS) {
114
+ if (done.has(migration.id))
115
+ continue;
116
+ for (const statement of migration.statements(t))
117
+ await db.query(statement);
118
+ await db.query(`INSERT INTO ${t.migrations} (id, name) VALUES ($1, $2) ON CONFLICT (id) DO NOTHING`, [
119
+ migration.id,
120
+ migration.name,
121
+ ]);
122
+ applied.push(migration.id);
123
+ }
124
+ return { applied };
125
+ }
126
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAYA,MAAM,UAAU,GAAG,yBAAyB,CAAC;AAE7C,iFAAiF;AACjF,MAAM,UAAU,UAAU,CAAC,MAAM,GAAG,WAAW;IAC7C,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CAAC,6GAA6G,MAAM,IAAI,CAAC,CAAC;IAC3I,CAAC;IACD,OAAO;QACL,aAAa,EAAE,GAAG,MAAM,eAAe;QACvC,QAAQ,EAAE,GAAG,MAAM,UAAU;QAC7B,QAAQ,EAAE,GAAG,MAAM,UAAU;QAC7B,EAAE,EAAE,GAAG,MAAM,IAAI;QACjB,MAAM,EAAE,GAAG,MAAM,QAAQ;QACzB,UAAU,EAAE,GAAG,MAAM,YAAY;QACjC,YAAY,EAAE,GAAG,MAAM,eAAe;KACvC,CAAC;AACJ,CAAC;AAQD;;;;GAIG;AACH,MAAM,CAAC,MAAM,UAAU,GAAgB;IACrC;QACE,EAAE,EAAE,CAAC;QACL,IAAI,EAAE,eAAe;QACrB,UAAU,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC;YACjB,8BAA8B,CAAC,CAAC,aAAa;;;;;;;;;QAS3C;YACF,8BAA8B,CAAC,CAAC,aAAa,kBAAkB,CAAC,CAAC,aAAa,wBAAwB;YACtG,8BAA8B,CAAC,CAAC,QAAQ;mDACK,CAAC,CAAC,aAAa;;;;;;;;;;;;;;;QAe1D;YACF,8BAA8B,CAAC,CAAC,QAAQ,kBAAkB,CAAC,CAAC,QAAQ,wBAAwB;YAC5F,8BAA8B,CAAC,CAAC,QAAQ;;;;QAItC;YACF,8BAA8B,CAAC,CAAC,QAAQ,kBAAkB,CAAC,CAAC,QAAQ,eAAe;YACnF,8BAA8B,CAAC,CAAC,EAAE;;;;QAIhC;YACF,8BAA8B,CAAC,CAAC,EAAE,kBAAkB,CAAC,CAAC,EAAE,4CAA4C;YACpG,8BAA8B,CAAC,CAAC,MAAM;;;;;;QAMpC;YACF,8BAA8B,CAAC,CAAC,MAAM,iBAAiB,CAAC,CAAC,MAAM,qBAAqB;SACrF;KACF;CACF,CAAC;AAcF,2FAA2F;AAC3F,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,EAAa,EAAE,OAA8B;IAChF,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,OAAO,CAAC,UAAU,GAAG,CAAC,IAAI,OAAO,CAAC,UAAU,GAAG,MAAM,EAAE,CAAC;QACnG,MAAM,IAAI,KAAK,CAAC,0DAA0D,OAAO,CAAC,UAAU,GAAG,CAAC,CAAC;IACnG,CAAC;IACD,MAAM,CAAC,GAAG,UAAU,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IAC1C,MAAM,EAAE,CAAC,KAAK,CAAC,uCAAuC,CAAC,CAAC;IACxD,MAAM,EAAE,CAAC,KAAK,CAAC,8BAA8B,CAAC,CAAC,YAAY;;;;;;uBAMtC,OAAO,CAAC,UAAU;;IAErC,CAAC,CAAC;IACJ,MAAM,EAAE,CAAC,KAAK,CAAC,8BAA8B,CAAC,CAAC,YAAY,YAAY,CAAC,CAAC,YAAY,2CAA2C,CAAC,CAAC;IAClI,MAAM,EAAE,CAAC,KAAK,CAAC,8BAA8B,CAAC,CAAC,YAAY,WAAW,CAAC,CAAC,YAAY,cAAc,CAAC,CAAC;IACpG,MAAM,EAAE,CAAC,KAAK,CAAC,8BAA8B,CAAC,CAAC,YAAY,WAAW,CAAC,CAAC,YAAY,6BAA6B,CAAC,CAAC;AACrH,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,EAAa,EAAE,UAA0B,EAAE;IACvE,MAAM,CAAC,GAAG,UAAU,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IAC1C,MAAM,EAAE,CAAC,KAAK,CACZ,8BAA8B,CAAC,CAAC,UAAU;;;;MAIxC,CACH,CAAC;IACF,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,EAAE,CAAC,KAAK,CAAiB,kBAAkB,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC;IAClF,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAExD,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,IAAI,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;YAAE,SAAS;QACrC,KAAK,MAAM,SAAS,IAAI,SAAS,CAAC,UAAU,CAAC,CAAC,CAAC;YAAE,MAAM,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QAC3E,MAAM,EAAE,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC,UAAU,yDAAyD,EAAE;YACnG,SAAS,CAAC,EAAE;YACZ,SAAS,CAAC,IAAI;SACf,CAAC,CAAC;QACH,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;IAC7B,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,CAAC;AACrB,CAAC"}
package/dist/sql.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ /** A `timestamptz` column read back as epoch milliseconds (a float8, which is exact for ms). */
2
+ export declare const ms: (column: string) => string;
3
+ /** A milliseconds parameter written as `timestamptz`. */
4
+ export declare const toTime: (placeholder: string) => string;
5
+ /** Drivers differ on `undefined`; postgres.js rejects it. Send `null`. */
6
+ export declare const param: (value: unknown) => {} | null;
7
+ /** A value for a `jsonb` parameter, or `null`. */
8
+ export declare const json: (value: unknown) => string | null;
9
+ //# sourceMappingURL=sql.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sql.d.ts","sourceRoot":"","sources":["../src/sql.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,eAAO,MAAM,EAAE,GAAI,QAAQ,MAAM,WAAqD,CAAC;AAEvF,yDAAyD;AACzD,eAAO,MAAM,MAAM,GAAI,aAAa,MAAM,WAAkD,CAAC;AAE7F,0EAA0E;AAC1E,eAAO,MAAM,KAAK,GAAI,OAAO,OAAO,cAAyC,CAAC;AAE9E,kDAAkD;AAClD,eAAO,MAAM,IAAI,GAAI,OAAO,OAAO,kBAA2E,CAAC"}
package/dist/sql.js ADDED
@@ -0,0 +1,9 @@
1
+ /** A `timestamptz` column read back as epoch milliseconds (a float8, which is exact for ms). */
2
+ export const ms = (column) => `(extract(epoch from ${column}) * 1000)::float8`;
3
+ /** A milliseconds parameter written as `timestamptz`. */
4
+ export const toTime = (placeholder) => `to_timestamp(${placeholder}::float8 / 1000)`;
5
+ /** Drivers differ on `undefined`; postgres.js rejects it. Send `null`. */
6
+ export const param = (value) => (value === undefined ? null : value);
7
+ /** A value for a `jsonb` parameter, or `null`. */
8
+ export const json = (value) => (value === undefined || value === null ? null : JSON.stringify(value));
9
+ //# sourceMappingURL=sql.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sql.js","sourceRoot":"","sources":["../src/sql.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,MAAc,EAAE,EAAE,CAAC,uBAAuB,MAAM,mBAAmB,CAAC;AAEvF,yDAAyD;AACzD,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,WAAmB,EAAE,EAAE,CAAC,gBAAgB,WAAW,kBAAkB,CAAC;AAE7F,0EAA0E;AAC1E,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,KAAc,EAAE,EAAE,CAAC,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;AAE9E,kDAAkD;AAClD,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,KAAc,EAAE,EAAE,CAAC,CAAC,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC"}
@@ -0,0 +1,15 @@
1
+ import type { KeywordQuery, ScoredRecord, VectorQuery, VectorRecord, VectorStore } from '@chatcops/core';
2
+ import type { Queryable } from './queryable.js';
3
+ /** Postgres pgvector-backed storage for embedded page chunks. */
4
+ export declare class PostgresVectorStore implements VectorStore {
5
+ readonly dimensions: number;
6
+ private readonly db;
7
+ private readonly table;
8
+ constructor(dimensions: number, db: Queryable, table: string);
9
+ upsert(records: VectorRecord[]): Promise<void>;
10
+ query({ vector, topK, filter }: VectorQuery): Promise<ScoredRecord[]>;
11
+ keywordQuery({ text, topK, filter }: KeywordQuery): Promise<ScoredRecord[]>;
12
+ deleteBySource(sourceId: string): Promise<void>;
13
+ private checkVector;
14
+ }
15
+ //# sourceMappingURL=vectors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vectors.d.ts","sourceRoot":"","sources":["../src/vectors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EAEZ,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,WAAW,EACZ,MAAM,gBAAgB,CAAC;AAExB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAmBhD,iEAAiE;AACjE,qBAAa,mBAAoB,YAAW,WAAW;IAEnD,QAAQ,CAAC,UAAU,EAAE,MAAM;IAC3B,OAAO,CAAC,QAAQ,CAAC,EAAE;IACnB,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAFb,UAAU,EAAE,MAAM,EACV,EAAE,EAAE,SAAS,EACb,KAAK,EAAE,MAAM;IAO1B,MAAM,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAe9C,KAAK,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAyBrE,YAAY,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAwB3E,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIrD,OAAO,CAAC,WAAW;CAMpB"}