Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,68 @@ const config = {
};
```

#### PostgreSQL Schema Configuration

Nuvex uses strongly branded table and column names by default (`nuvex_storage`, `nuvex_key`, `nuvex_data`). For backwards compatibility with existing applications, you can customize these names:

```typescript
// Default configuration (recommended for new apps)
const storage = await NuvexClient.initialize({
postgres: {
host: 'localhost',
port: 5432,
database: 'myapp',
user: 'postgres',
password: 'password'
// Uses: table 'nuvex_storage', columns 'nuvex_key' and 'nuvex_data'
}
});

// Custom configuration for Telegram bot compatibility
const storage = await NuvexClient.initialize({
postgres: {
host: 'localhost',
port: 5432,
database: 'telegram_bot',
user: 'postgres',
password: 'password',
schema: {
tableName: 'storage_cache',
columns: {
key: 'key',
value: 'value'
}
}
}
});

// Custom configuration for Discord bot compatibility
const storage = await NuvexClient.initialize({
postgres: {
host: 'localhost',
port: 5432,
database: 'discord_bot',
user: 'postgres',
password: 'password',
schema: {
tableName: 'storage_cache',
columns: {
key: 'cache_key',
value: 'data'
}
}
}
});
```

**Schema Configuration Reference:**

| App | Table | Key Column | Data Column |
|---------------|-----------------|---------------|---------------|
| **Nuvex** (default) | nuvex_storage | nuvex_key | nuvex_data |
| Telegram Bot | storage_cache | key | value |
| Discord Bot | storage_cache | cache_key | data |

### API Reference

```typescript
Expand Down
110 changes: 108 additions & 2 deletions src/__tests__/unit/database.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ describe('Database Utilities', () => {
it('should contain the complete schema definition', () => {
expect(NUVEX_SCHEMA_SQL).toContain('CREATE TABLE IF NOT EXISTS nuvex_storage');
expect(NUVEX_SCHEMA_SQL).toContain('id SERIAL PRIMARY KEY');
expect(NUVEX_SCHEMA_SQL).toContain('key VARCHAR(512) NOT NULL UNIQUE');
expect(NUVEX_SCHEMA_SQL).toContain('value JSONB NOT NULL');
expect(NUVEX_SCHEMA_SQL).toContain('nuvex_key VARCHAR(512) NOT NULL UNIQUE');
expect(NUVEX_SCHEMA_SQL).toContain('nuvex_data JSONB NOT NULL');
expect(NUVEX_SCHEMA_SQL).toContain('expires_at TIMESTAMP WITH TIME ZONE');
expect(NUVEX_SCHEMA_SQL).toContain('idx_nuvex_storage_expires_at');
expect(NUVEX_SCHEMA_SQL).toContain('update_nuvex_storage_updated_at');
Expand Down Expand Up @@ -103,6 +103,73 @@ describe('Database Utilities', () => {
expect.arrayContaining([expect.stringMatching(/nuvex-cleanup-\d+/)])
);
});

it('should setup schema with custom table name', async () => {
const querySpy = jest.spyOn(mockDb, 'query');
const options: SchemaSetupOptions = {
schema: {
tableName: 'storage_cache'
}
};

await setupNuvexSchema(mockDb, options);

// Should call query with custom schema SQL
const queryCalls = querySpy.mock.calls;
const schemaCall = queryCalls.find((call: any[]) =>
typeof call[0] === 'string' && call[0].includes('CREATE TABLE IF NOT EXISTS storage_cache')
);
expect(schemaCall).toBeDefined();
expect(console.log).toHaveBeenCalledWith('Nuvex database schema setup completed successfully');
});

it('should setup schema with custom column names', async () => {
const querySpy = jest.spyOn(mockDb, 'query');
const options: SchemaSetupOptions = {
schema: {
columns: {
key: 'cache_key',
value: 'data'
}
}
};

await setupNuvexSchema(mockDb, options);

// Should call query with custom column names
const queryCalls = querySpy.mock.calls;
const schemaCall = queryCalls.find((call: any[]) =>
typeof call[0] === 'string' &&
call[0].includes('cache_key VARCHAR(512)') &&
call[0].includes('data JSONB')
);
expect(schemaCall).toBeDefined();
});

it('should setup schema with custom table and column names for Telegram bot', async () => {
const querySpy = jest.spyOn(mockDb, 'query');
const options: SchemaSetupOptions = {
schema: {
tableName: 'storage_cache',
columns: {
key: 'key',
value: 'value'
}
}
};

await setupNuvexSchema(mockDb, options);

// Should call query with custom schema SQL
const queryCalls = querySpy.mock.calls;
const schemaCall = queryCalls.find((call: any[]) =>
typeof call[0] === 'string' &&
call[0].includes('CREATE TABLE IF NOT EXISTS storage_cache') &&
call[0].includes('key VARCHAR(512)') &&
call[0].includes('value JSONB')
);
expect(schemaCall).toBeDefined();
});
});

describe('cleanupExpiredEntries', () => {
Expand Down Expand Up @@ -141,6 +208,24 @@ describe('Database Utilities', () => {
await expect(cleanupExpiredEntries(mockDb)).rejects.toThrow('Cleanup failed');
expect(console.error).toHaveBeenCalledWith('Failed to cleanup expired entries:', error);
});

it('should use custom table name when provided', async () => {
const mockResult = { rows: [{ deleted_count: 3 }] };
jest.spyOn(mockDb, 'query').mockResolvedValue(mockResult);

const deletedCount = await cleanupExpiredEntries(mockDb, {
tableName: 'storage_cache'
});

expect(deletedCount).toBe(3);
expect(mockDb.query).toHaveBeenCalledWith('SELECT cleanup_expired_storage_cache() as deleted_count;');
});

it('should validate custom table name', async () => {
await expect(cleanupExpiredEntries(mockDb, {
tableName: 'invalid; DROP TABLE users; --'
})).rejects.toThrow('Invalid table name');
});
});

describe('dropNuvexSchema', () => {
Expand All @@ -162,5 +247,26 @@ describe('Database Utilities', () => {
await expect(dropNuvexSchema(mockDb)).rejects.toThrow('Drop schema failed');
expect(console.error).toHaveBeenCalledWith('Failed to drop Nuvex database schema:', error);
});

it('should drop custom table schema', async () => {
const querySpy = jest.spyOn(mockDb, 'query');

await dropNuvexSchema(mockDb, {
tableName: 'storage_cache'
});

// Should use custom table name in drop statements
const queryCall = querySpy.mock.calls[0][0] as string;
expect(queryCall).toContain('DROP TRIGGER IF EXISTS trigger_update_storage_cache_updated_at ON storage_cache');
expect(queryCall).toContain('DROP FUNCTION IF EXISTS update_storage_cache_updated_at()');
expect(queryCall).toContain('DROP FUNCTION IF EXISTS cleanup_expired_storage_cache()');
expect(queryCall).toContain('DROP TABLE IF EXISTS storage_cache CASCADE');
});

it('should validate custom table name before dropping', async () => {
await expect(dropNuvexSchema(mockDb, {
tableName: 'invalid; DROP TABLE users; --'
})).rejects.toThrow('Invalid table name');
});
});
});
Loading