@mastra/pg 0.14.5 → 0.14.6-alpha.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.
@@ -1,377 +0,0 @@
1
- import { createSampleThread } from '@internal/storage-test-utils';
2
- import type { StorageColumn, TABLE_NAMES } from '@mastra/core/storage';
3
- import pgPromise from 'pg-promise';
4
- import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
5
- import { PostgresStore } from '.';
6
- import type { PostgresConfig } from '.';
7
-
8
- export const TEST_CONFIG: PostgresConfig = {
9
- host: process.env.POSTGRES_HOST || 'localhost',
10
- port: Number(process.env.POSTGRES_PORT) || 5434,
11
- database: process.env.POSTGRES_DB || 'postgres',
12
- user: process.env.POSTGRES_USER || 'postgres',
13
- password: process.env.POSTGRES_PASSWORD || 'postgres',
14
- };
15
-
16
- export const connectionString = `postgresql://${TEST_CONFIG.user}:${TEST_CONFIG.password}@${TEST_CONFIG.host}:${TEST_CONFIG.port}/${TEST_CONFIG.database}`;
17
-
18
- export function pgTests() {
19
- let store: PostgresStore;
20
-
21
- describe('PG specific tests', () => {
22
- beforeAll(async () => {
23
- store = new PostgresStore(TEST_CONFIG);
24
- await store.init();
25
- });
26
- afterAll(async () => {
27
- try {
28
- await store.close();
29
- } catch {}
30
- });
31
-
32
- describe('Public Fields Access', () => {
33
- it('should expose db field as public', () => {
34
- expect(store.db).toBeDefined();
35
- expect(typeof store.db).toBe('object');
36
- expect(store.db.query).toBeDefined();
37
- expect(typeof store.db.query).toBe('function');
38
- });
39
-
40
- it('should expose pgp field as public', () => {
41
- expect(store.pgp).toBeDefined();
42
- expect(typeof store.pgp).toBe('function');
43
- expect(store.pgp.end).toBeDefined();
44
- expect(typeof store.pgp.end).toBe('function');
45
- });
46
-
47
- it('should allow direct database queries via public db field', async () => {
48
- const result = await store.db.one('SELECT 1 as test');
49
- expect(result.test).toBe(1);
50
- });
51
-
52
- it('should allow access to pgp utilities via public pgp field', () => {
53
- const helpers = store.pgp.helpers;
54
- expect(helpers).toBeDefined();
55
- expect(helpers.insert).toBeDefined();
56
- expect(helpers.update).toBeDefined();
57
- });
58
-
59
- it('should maintain connection state through public db field', async () => {
60
- // Test multiple queries to ensure connection state
61
- const result1 = await store.db.one('SELECT NOW() as timestamp1');
62
- const result2 = await store.db.one('SELECT NOW() as timestamp2');
63
-
64
- expect(result1.timestamp1).toBeDefined();
65
- expect(result2.timestamp2).toBeDefined();
66
- expect(new Date(result2.timestamp2).getTime()).toBeGreaterThanOrEqual(new Date(result1.timestamp1).getTime());
67
- });
68
-
69
- it('should throw error when pool is used after disconnect', async () => {
70
- await store.close();
71
- await expect(store.db.connect()).rejects.toThrow();
72
- store = new PostgresStore(TEST_CONFIG);
73
- await store.init();
74
- });
75
- });
76
-
77
- describe('PgStorage Table Name Quoting', () => {
78
- const camelCaseTable = 'TestCamelCaseTable';
79
- const snakeCaseTable = 'test_snake_case_table';
80
- const BASE_SCHEMA = {
81
- id: { type: 'integer', primaryKey: true, nullable: false },
82
- name: { type: 'text', nullable: true },
83
- createdAt: { type: 'timestamp', nullable: false },
84
- updatedAt: { type: 'timestamp', nullable: false },
85
- } as Record<string, StorageColumn>;
86
-
87
- beforeEach(async () => {
88
- // Only clear tables if store is initialized
89
- try {
90
- // Clear tables before each test
91
- await store.clearTable({ tableName: camelCaseTable as TABLE_NAMES });
92
- await store.clearTable({ tableName: snakeCaseTable as TABLE_NAMES });
93
- } catch (error) {
94
- // Ignore errors during table clearing
95
- console.warn('Error clearing tables:', error);
96
- }
97
- });
98
-
99
- afterEach(async () => {
100
- // Only clear tables if store is initialized
101
- try {
102
- // Clear tables before each test
103
- await store.clearTable({ tableName: camelCaseTable as TABLE_NAMES });
104
- await store.clearTable({ tableName: snakeCaseTable as TABLE_NAMES });
105
- } catch (error) {
106
- // Ignore errors during table clearing
107
- console.warn('Error clearing tables:', error);
108
- }
109
- });
110
-
111
- it('should create and upsert to a camelCase table without quoting errors', async () => {
112
- await expect(
113
- store.createTable({
114
- tableName: camelCaseTable as TABLE_NAMES,
115
- schema: BASE_SCHEMA,
116
- }),
117
- ).resolves.not.toThrow();
118
-
119
- await store.insert({
120
- tableName: camelCaseTable as TABLE_NAMES,
121
- record: { id: '1', name: 'Alice', createdAt: new Date(), updatedAt: new Date() },
122
- });
123
-
124
- const row: any = await store.load({
125
- tableName: camelCaseTable as TABLE_NAMES,
126
- keys: { id: '1' },
127
- });
128
- expect(row?.name).toBe('Alice');
129
- });
130
-
131
- it('should create and upsert to a snake_case table without quoting errors', async () => {
132
- await expect(
133
- store.createTable({
134
- tableName: snakeCaseTable as TABLE_NAMES,
135
- schema: BASE_SCHEMA,
136
- }),
137
- ).resolves.not.toThrow();
138
-
139
- await store.insert({
140
- tableName: snakeCaseTable as TABLE_NAMES,
141
- record: { id: '2', name: 'Bob', createdAt: new Date(), updatedAt: new Date() },
142
- });
143
-
144
- const row: any = await store.load({
145
- tableName: snakeCaseTable as TABLE_NAMES,
146
- keys: { id: '2' },
147
- });
148
- expect(row?.name).toBe('Bob');
149
- });
150
- });
151
-
152
- describe('Permission Handling', () => {
153
- const schemaRestrictedUser = 'mastra_schema_restricted_storage';
154
- const restrictedPassword = 'test123';
155
- const testSchema = 'testSchema';
156
- let adminDb: pgPromise.IDatabase<{}>;
157
- let pgpAdmin: pgPromise.IMain;
158
-
159
- beforeAll(async () => {
160
- // Re-initialize the main store for subsequent tests
161
-
162
- await store.init();
163
-
164
- // Create a separate pg-promise instance for admin operations
165
- pgpAdmin = pgPromise();
166
- adminDb = pgpAdmin(connectionString);
167
- try {
168
- await adminDb.tx(async t => {
169
- // Drop the test schema if it exists from previous runs
170
- await t.none(`DROP SCHEMA IF EXISTS ${testSchema} CASCADE`);
171
-
172
- // Create schema restricted user with minimal permissions
173
- await t.none(`
174
- DO $$
175
- BEGIN
176
- IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = '${schemaRestrictedUser}') THEN
177
- CREATE USER ${schemaRestrictedUser} WITH PASSWORD '${restrictedPassword}' NOCREATEDB;
178
- END IF;
179
- END
180
- $$;`);
181
-
182
- // Grant only connect and usage to schema restricted user
183
- await t.none(`
184
- REVOKE ALL ON DATABASE ${(TEST_CONFIG as any).database} FROM ${schemaRestrictedUser};
185
- GRANT CONNECT ON DATABASE ${(TEST_CONFIG as any).database} TO ${schemaRestrictedUser};
186
- REVOKE ALL ON SCHEMA public FROM ${schemaRestrictedUser};
187
- GRANT USAGE ON SCHEMA public TO ${schemaRestrictedUser};
188
- `);
189
- });
190
- } catch (error) {
191
- // Clean up the database connection on error
192
- pgpAdmin.end();
193
- throw error;
194
- }
195
- });
196
-
197
- afterAll(async () => {
198
- try {
199
- // Then clean up test user in admin connection
200
- await adminDb.tx(async t => {
201
- await t.none(`
202
- REASSIGN OWNED BY ${schemaRestrictedUser} TO postgres;
203
- DROP OWNED BY ${schemaRestrictedUser};
204
- DROP USER IF EXISTS ${schemaRestrictedUser};
205
- `);
206
- });
207
-
208
- // Finally clean up admin connection
209
- if (pgpAdmin) {
210
- pgpAdmin.end();
211
- }
212
- } catch (error) {
213
- console.error('Error cleaning up test user:', error);
214
- if (pgpAdmin) pgpAdmin.end();
215
- }
216
- });
217
-
218
- describe('Schema Creation', () => {
219
- beforeEach(async () => {
220
- // Create a fresh connection for each test
221
- const tempPgp = pgPromise();
222
- const tempDb = tempPgp(connectionString);
223
-
224
- try {
225
- // Ensure schema doesn't exist before each test
226
- await tempDb.none(`DROP SCHEMA IF EXISTS ${testSchema} CASCADE`);
227
-
228
- // Ensure no active connections from restricted user
229
- await tempDb.none(`
230
- SELECT pg_terminate_backend(pid)
231
- FROM pg_stat_activity
232
- WHERE usename = '${schemaRestrictedUser}'
233
- `);
234
- } finally {
235
- tempPgp.end(); // Always clean up the connection
236
- }
237
- });
238
-
239
- afterEach(async () => {
240
- // Create a fresh connection for cleanup
241
- const tempPgp = pgPromise();
242
- const tempDb = tempPgp(connectionString);
243
-
244
- try {
245
- // Clean up any connections from the restricted user and drop schema
246
- await tempDb.none(`
247
- DO $$
248
- BEGIN
249
- -- Terminate connections
250
- PERFORM pg_terminate_backend(pid)
251
- FROM pg_stat_activity
252
- WHERE usename = '${schemaRestrictedUser}';
253
-
254
- -- Drop schema
255
- DROP SCHEMA IF EXISTS ${testSchema} CASCADE;
256
- END $$;
257
- `);
258
- } catch (error) {
259
- console.error('Error in afterEach cleanup:', error);
260
- } finally {
261
- tempPgp.end(); // Always clean up the connection
262
- }
263
- });
264
-
265
- it('should fail when user lacks CREATE privilege', async () => {
266
- const restrictedDB = new PostgresStore({
267
- ...TEST_CONFIG,
268
- user: schemaRestrictedUser,
269
- password: restrictedPassword,
270
- schemaName: testSchema,
271
- });
272
-
273
- // Create a fresh connection for verification
274
- const tempPgp = pgPromise();
275
- const tempDb = tempPgp(connectionString);
276
-
277
- try {
278
- // Test schema creation by initializing the store
279
- await expect(async () => {
280
- await restrictedDB.init();
281
- }).rejects.toThrow(
282
- `Unable to create schema "${testSchema}". This requires CREATE privilege on the database.`,
283
- );
284
-
285
- // Verify schema was not created
286
- const exists = await tempDb.oneOrNone(
287
- `SELECT EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = $1)`,
288
- [testSchema],
289
- );
290
- expect(exists?.exists).toBe(false);
291
- } finally {
292
- await restrictedDB.close();
293
- tempPgp.end(); // Clean up the verification connection
294
- }
295
- });
296
-
297
- it('should fail with schema creation error when saving thread', async () => {
298
- const restrictedDB = new PostgresStore({
299
- ...TEST_CONFIG,
300
- user: schemaRestrictedUser,
301
- password: restrictedPassword,
302
- schemaName: testSchema,
303
- });
304
-
305
- // Create a fresh connection for verification
306
- const tempPgp = pgPromise();
307
- const tempDb = tempPgp(connectionString);
308
-
309
- try {
310
- await expect(async () => {
311
- await restrictedDB.init();
312
- const thread = createSampleThread();
313
- await restrictedDB.saveThread({ thread });
314
- }).rejects.toThrow(
315
- `Unable to create schema "${testSchema}". This requires CREATE privilege on the database.`,
316
- );
317
-
318
- // Verify schema was not created
319
- const exists = await tempDb.oneOrNone(
320
- `SELECT EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = $1)`,
321
- [testSchema],
322
- );
323
- expect(exists?.exists).toBe(false);
324
- } finally {
325
- await restrictedDB.close();
326
- tempPgp.end(); // Clean up the verification connection
327
- }
328
- });
329
- });
330
- });
331
-
332
- describe('Validation', () => {
333
- const validConfig = TEST_CONFIG as any;
334
- it('throws if connectionString is empty', () => {
335
- expect(() => new PostgresStore({ connectionString: '' })).toThrow();
336
- expect(() => new PostgresStore({ ...validConfig, connectionString: '' })).toThrow();
337
- });
338
- it('throws if host is missing or empty', () => {
339
- expect(() => new PostgresStore({ ...validConfig, host: '' })).toThrow();
340
- const { host, ...rest } = validConfig;
341
- expect(() => new PostgresStore(rest as any)).toThrow();
342
- });
343
- it('throws if connectionString is empty', () => {
344
- expect(() => new PostgresStore({ connectionString: '' })).toThrow();
345
- const { database, ...rest } = validConfig;
346
- expect(() => new PostgresStore(rest as any)).toThrow();
347
- });
348
- it('throws if user is missing or empty', () => {
349
- expect(() => new PostgresStore({ ...validConfig, user: '' })).toThrow();
350
- const { user, ...rest } = validConfig;
351
- expect(() => new PostgresStore(rest as any)).toThrow();
352
- });
353
- it('throws if database is missing or empty', () => {
354
- expect(() => new PostgresStore({ ...validConfig, database: '' })).toThrow();
355
- const { database, ...rest } = validConfig;
356
- expect(() => new PostgresStore(rest as any)).toThrow();
357
- });
358
- it('throws if password is missing or empty', () => {
359
- expect(() => new PostgresStore({ ...validConfig, password: '' })).toThrow();
360
- const { password, ...rest } = validConfig;
361
- expect(() => new PostgresStore(rest as any)).toThrow();
362
- });
363
- it('does not throw on valid config (host-based)', () => {
364
- expect(() => new PostgresStore(validConfig)).not.toThrow();
365
- });
366
- it('does not throw on non-empty connection string', () => {
367
- expect(() => new PostgresStore({ connectionString })).not.toThrow();
368
- });
369
- it('throws if store is not initialized', () => {
370
- expect(() => new PostgresStore(validConfig).db.any('SELECT 1')).toThrow(
371
- /PostgresStore: Store is not initialized/,
372
- );
373
- expect(() => new PostgresStore(validConfig).pgp).toThrow(/PostgresStore: Store is not initialized/);
374
- });
375
- });
376
- });
377
- }