@bernierllc/database-adapter-postgresql 1.5.1 → 1.5.3

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,212 +0,0 @@
1
- "use strict";
2
- /*
3
- Copyright (c) 2025 Bernier LLC
4
-
5
- This file is licensed to the client under a limited-use license.
6
- The client may use and modify this code *only within the scope of the project it was delivered for*.
7
- Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
8
- */
9
- Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.RLSPolicyGenerator = void 0;
11
- /**
12
- * RLS policy generator for Supabase
13
- * Creates policies for multi-tenant security with Clerk integration
14
- */
15
- class RLSPolicyGenerator {
16
- /**
17
- * Generate RLS policy for user-scoped access
18
- */
19
- static generateUserPolicy(table) {
20
- return `
21
- -- Enable RLS
22
- ALTER TABLE ${table} ENABLE ROW LEVEL SECURITY;
23
-
24
- -- Users can view own records
25
- CREATE POLICY "Users can view own records"
26
- ON ${table}
27
- FOR SELECT
28
- USING (
29
- user_id = (
30
- SELECT id FROM users
31
- WHERE clerk_id = current_setting('request.jwt.claims', true)::json->>'sub'
32
- )
33
- );
34
-
35
- -- Users can insert own records
36
- CREATE POLICY "Users can insert own records"
37
- ON ${table}
38
- FOR INSERT
39
- WITH CHECK (
40
- user_id = (
41
- SELECT id FROM users
42
- WHERE clerk_id = current_setting('request.jwt.claims', true)::json->>'sub'
43
- )
44
- );
45
-
46
- -- Users can update own records
47
- CREATE POLICY "Users can update own records"
48
- ON ${table}
49
- FOR UPDATE
50
- USING (
51
- user_id = (
52
- SELECT id FROM users
53
- WHERE clerk_id = current_setting('request.jwt.claims', true)::json->>'sub'
54
- )
55
- );
56
-
57
- -- Users can delete own records (soft delete only)
58
- CREATE POLICY "Users can delete own records"
59
- ON ${table}
60
- FOR UPDATE
61
- USING (
62
- user_id = (
63
- SELECT id FROM users
64
- WHERE clerk_id = current_setting('request.jwt.claims', true)::json->>'sub'
65
- )
66
- )
67
- WITH CHECK (deleted_at IS NOT NULL);
68
- `;
69
- }
70
- /**
71
- * Generate admin bypass policy
72
- */
73
- static generateAdminPolicy(table) {
74
- return `
75
- -- Admins can do anything
76
- CREATE POLICY "Admins have full access"
77
- ON ${table}
78
- FOR ALL
79
- USING (
80
- (SELECT role FROM users
81
- WHERE clerk_id = current_setting('request.jwt.claims', true)::json->>'sub'
82
- ) = 'admin'
83
- );
84
- `;
85
- }
86
- /**
87
- * Generate multi-tenant policy
88
- */
89
- static generateTenantPolicy(table) {
90
- return `
91
- -- Tenant isolation
92
- CREATE POLICY "Tenant isolation"
93
- ON ${table}
94
- FOR ALL
95
- USING (
96
- tenant_id = current_setting('app.tenant_id', true)::uuid
97
- );
98
- `;
99
- }
100
- /**
101
- * Generate read-only policy for public data
102
- */
103
- static generatePublicReadPolicy(table, condition) {
104
- const whereClause = condition ? `AND ${condition}` : '';
105
- return `
106
- -- Public read access
107
- CREATE POLICY "Public can view published records"
108
- ON ${table}
109
- FOR SELECT
110
- USING (
111
- status = 'published'
112
- AND deleted_at IS NULL
113
- ${whereClause}
114
- );
115
- `;
116
- }
117
- /**
118
- * Generate policy for shared resources
119
- */
120
- static generateSharedResourcePolicy(table, sharingTable = 'shared_access') {
121
- return `
122
- -- Shared resource access
123
- CREATE POLICY "Users can access shared resources"
124
- ON ${table}
125
- FOR SELECT
126
- USING (
127
- -- Owner access
128
- user_id = (
129
- SELECT id FROM users
130
- WHERE clerk_id = current_setting('request.jwt.claims', true)::json->>'sub'
131
- )
132
- OR
133
- -- Shared access
134
- id IN (
135
- SELECT resource_id FROM ${sharingTable}
136
- WHERE shared_with_user_id = (
137
- SELECT id FROM users
138
- WHERE clerk_id = current_setting('request.jwt.claims', true)::json->>'sub'
139
- )
140
- )
141
- );
142
- `;
143
- }
144
- /**
145
- * Generate policy for role-based access
146
- */
147
- static generateRoleBasedPolicy(table, allowedRoles) {
148
- const rolesArray = allowedRoles.map(r => `'${r}'`).join(', ');
149
- return `
150
- -- Role-based access
151
- CREATE POLICY "Role-based access"
152
- ON ${table}
153
- FOR ALL
154
- USING (
155
- (SELECT role FROM users
156
- WHERE clerk_id = current_setting('request.jwt.claims', true)::json->>'sub'
157
- ) IN (${rolesArray})
158
- );
159
- `;
160
- }
161
- /**
162
- * Disable RLS on a table (use with caution)
163
- */
164
- static disableRLS(table) {
165
- return `
166
- -- Disable RLS (use with caution)
167
- ALTER TABLE ${table} DISABLE ROW LEVEL SECURITY;
168
-
169
- -- Drop all policies
170
- DO $$
171
- DECLARE
172
- pol RECORD;
173
- BEGIN
174
- FOR pol IN
175
- SELECT policyname
176
- FROM pg_policies
177
- WHERE tablename = '${table}'
178
- LOOP
179
- EXECUTE format('DROP POLICY IF EXISTS %I ON ${table}', pol.policyname);
180
- END LOOP;
181
- END $$;
182
- `;
183
- }
184
- /**
185
- * Generate policy for time-based access (e.g., scheduled content)
186
- */
187
- static generateTimeBasedPolicy(table) {
188
- return `
189
- -- Time-based access for scheduled content
190
- CREATE POLICY "Scheduled content visibility"
191
- ON ${table}
192
- FOR SELECT
193
- USING (
194
- (
195
- -- Owner can always see
196
- user_id = (
197
- SELECT id FROM users
198
- WHERE clerk_id = current_setting('request.jwt.claims', true)::json->>'sub'
199
- )
200
- )
201
- OR
202
- (
203
- -- Public can see if published and scheduled time has passed
204
- status = 'published'
205
- AND deleted_at IS NULL
206
- AND (scheduled_for IS NULL OR scheduled_for <= NOW())
207
- )
208
- );
209
- `;
210
- }
211
- }
212
- exports.RLSPolicyGenerator = RLSPolicyGenerator;
@@ -1,61 +0,0 @@
1
- import { PostgreSQLAdapter } from '../PostgreSQLAdapter';
2
- /**
3
- * Search result with similarity scores
4
- */
5
- export interface SearchResult {
6
- id: string;
7
- title: string;
8
- excerpt?: string;
9
- body?: string;
10
- titleSimilarity: number;
11
- bodySimilarity: number;
12
- totalSimilarity: number;
13
- [key: string]: unknown;
14
- }
15
- /**
16
- * Search options
17
- */
18
- export interface SearchOptions {
19
- limit?: number;
20
- offset?: number;
21
- minSimilarity?: number;
22
- fields?: string[];
23
- orderBy?: 'relevance' | 'date' | 'title';
24
- }
25
- /**
26
- * PostgreSQL full-text search with pg_trgm
27
- * Enables fuzzy matching and typo-tolerance
28
- */
29
- export declare class FullTextSearchManager {
30
- private adapter;
31
- private logger?;
32
- constructor(adapter: PostgreSQLAdapter);
33
- /**
34
- * Search content with full-text search and fuzzy matching
35
- */
36
- searchContent(query: string, options?: SearchOptions): Promise<SearchResult[]>;
37
- /**
38
- * Search using PostgreSQL full-text search vectors (ts_vector)
39
- */
40
- searchWithTsVector(query: string, options?: SearchOptions): Promise<SearchResult[]>;
41
- /**
42
- * Combined search using both pg_trgm fuzzy matching and ts_vector
43
- */
44
- searchCombined(query: string, options?: SearchOptions): Promise<SearchResult[]>;
45
- /**
46
- * Create full-text search indexes
47
- */
48
- createIndexes(): Promise<void>;
49
- /**
50
- * Drop full-text search indexes
51
- */
52
- dropIndexes(): Promise<void>;
53
- /**
54
- * Get search statistics
55
- */
56
- getSearchStats(): Promise<{
57
- indexesExist: boolean;
58
- extensionEnabled: boolean;
59
- searchVectorExists: boolean;
60
- }>;
61
- }
@@ -1,267 +0,0 @@
1
- "use strict";
2
- /*
3
- Copyright (c) 2025 Bernier LLC
4
-
5
- This file is licensed to the client under a limited-use license.
6
- The client may use and modify this code *only within the scope of the project it was delivered for*.
7
- Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
8
- */
9
- Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.FullTextSearchManager = void 0;
11
- /**
12
- * PostgreSQL full-text search with pg_trgm
13
- * Enables fuzzy matching and typo-tolerance
14
- */
15
- class FullTextSearchManager {
16
- adapter;
17
- logger;
18
- constructor(adapter) {
19
- this.adapter = adapter;
20
- // Access logger from adapter (it's a public property but typed generically)
21
- this.logger = adapter.logger;
22
- }
23
- /**
24
- * Search content with full-text search and fuzzy matching
25
- */
26
- async searchContent(query, options = {}) {
27
- const { limit = 20, offset = 0, minSimilarity = 0.3, fields = ['title', 'body', 'excerpt'], orderBy = 'relevance' } = options;
28
- if (this.logger) {
29
- this.logger.debug('Performing full-text search', {
30
- query,
31
- limit,
32
- offset,
33
- minSimilarity,
34
- orderBy
35
- });
36
- }
37
- // Build field similarity calculations
38
- const titleSimilarity = fields.includes('title')
39
- ? 'similarity(title, $1)'
40
- : '0';
41
- const bodySimilarity = fields.includes('body')
42
- ? 'similarity(body, $1)'
43
- : '0';
44
- const excerptSimilarity = fields.includes('excerpt')
45
- ? 'similarity(excerpt, $1)'
46
- : '0';
47
- // Calculate total similarity with weights (title > excerpt > body)
48
- const totalSimilarity = `(
49
- (${titleSimilarity} * 3) +
50
- (${excerptSimilarity} * 2) +
51
- (${bodySimilarity} * 1)
52
- ) / 6`;
53
- // Build WHERE clause
54
- const whereConditions = [];
55
- if (fields.includes('title')) {
56
- whereConditions.push(`${titleSimilarity} > $2`);
57
- }
58
- if (fields.includes('body')) {
59
- whereConditions.push(`${bodySimilarity} > $2`);
60
- }
61
- if (fields.includes('excerpt')) {
62
- whereConditions.push(`${excerptSimilarity} > $2`);
63
- }
64
- const whereClause = whereConditions.length > 0
65
- ? `WHERE (${whereConditions.join(' OR ')})`
66
- : '';
67
- // Build ORDER BY clause
68
- let orderByClause = '';
69
- switch (orderBy) {
70
- case 'relevance':
71
- orderByClause = 'ORDER BY total_similarity DESC';
72
- break;
73
- case 'date':
74
- orderByClause = 'ORDER BY created_at DESC';
75
- break;
76
- case 'title':
77
- orderByClause = 'ORDER BY title ASC';
78
- break;
79
- }
80
- const sql = `
81
- SELECT
82
- id,
83
- title,
84
- excerpt,
85
- ${titleSimilarity} as title_similarity,
86
- ${bodySimilarity} as body_similarity,
87
- ${totalSimilarity} as total_similarity,
88
- created_at,
89
- updated_at
90
- FROM content
91
- ${whereClause}
92
- ${orderByClause}
93
- LIMIT $3 OFFSET $4
94
- `;
95
- const result = await this.adapter.query(sql, [query, minSimilarity, limit, offset]);
96
- if (this.logger) {
97
- this.logger.debug('Full-text search completed', {
98
- query,
99
- resultsCount: result.rowCount
100
- });
101
- }
102
- return result.rows;
103
- }
104
- /**
105
- * Search using PostgreSQL full-text search vectors (ts_vector)
106
- */
107
- async searchWithTsVector(query, options = {}) {
108
- const { limit = 20, offset = 0 } = options;
109
- if (this.logger) {
110
- this.logger.debug('Performing ts_vector search', { query, limit, offset });
111
- }
112
- const sql = `
113
- SELECT
114
- id,
115
- title,
116
- excerpt,
117
- ts_rank(search_vector, plainto_tsquery('english', $1)) as rank
118
- FROM content
119
- WHERE search_vector @@ plainto_tsquery('english', $1)
120
- AND deleted_at IS NULL
121
- ORDER BY rank DESC
122
- LIMIT $2 OFFSET $3
123
- `;
124
- const result = await this.adapter.query(sql, [query, limit, offset]);
125
- if (this.logger) {
126
- this.logger.debug('ts_vector search completed', {
127
- query,
128
- resultsCount: result.rowCount
129
- });
130
- }
131
- return result.rows;
132
- }
133
- /**
134
- * Combined search using both pg_trgm fuzzy matching and ts_vector
135
- */
136
- async searchCombined(query, options = {}) {
137
- const { limit = 20, offset = 0, minSimilarity = 0.2 } = options;
138
- if (this.logger) {
139
- this.logger.debug('Performing combined search', { query, limit, offset });
140
- }
141
- const sql = `
142
- SELECT DISTINCT
143
- id,
144
- title,
145
- excerpt,
146
- similarity(title, $1) as title_similarity,
147
- similarity(body, $1) as body_similarity,
148
- COALESCE(ts_rank(search_vector, plainto_tsquery('english', $1)), 0) as ts_rank,
149
- (
150
- (similarity(title, $1) * 3) +
151
- (similarity(body, $1) * 1) +
152
- (COALESCE(ts_rank(search_vector, plainto_tsquery('english', $1)), 0) * 2)
153
- ) / 6 as total_similarity,
154
- created_at,
155
- updated_at
156
- FROM content
157
- WHERE
158
- (
159
- similarity(title, $1) > $2
160
- OR similarity(body, $1) > $2
161
- OR search_vector @@ plainto_tsquery('english', $1)
162
- )
163
- AND deleted_at IS NULL
164
- ORDER BY total_similarity DESC
165
- LIMIT $3 OFFSET $4
166
- `;
167
- const result = await this.adapter.query(sql, [query, minSimilarity, limit, offset]);
168
- if (this.logger) {
169
- this.logger.debug('Combined search completed', {
170
- query,
171
- resultsCount: result.rowCount
172
- });
173
- }
174
- return result.rows;
175
- }
176
- /**
177
- * Create full-text search indexes
178
- */
179
- async createIndexes() {
180
- if (this.logger) {
181
- this.logger.info('Creating full-text search indexes');
182
- }
183
- await this.adapter.query(`
184
- -- Enable pg_trgm extension
185
- CREATE EXTENSION IF NOT EXISTS pg_trgm;
186
-
187
- -- Create GIN indexes for fuzzy matching
188
- CREATE INDEX IF NOT EXISTS idx_content_title_trgm
189
- ON content USING gin (title gin_trgm_ops);
190
-
191
- CREATE INDEX IF NOT EXISTS idx_content_body_trgm
192
- ON content USING gin (body gin_trgm_ops);
193
-
194
- CREATE INDEX IF NOT EXISTS idx_content_excerpt_trgm
195
- ON content USING gin (excerpt gin_trgm_ops);
196
-
197
- -- Create full-text search vector if not exists
198
- DO $$
199
- BEGIN
200
- IF NOT EXISTS (
201
- SELECT 1 FROM information_schema.columns
202
- WHERE table_name = 'content' AND column_name = 'search_vector'
203
- ) THEN
204
- ALTER TABLE content ADD COLUMN search_vector tsvector
205
- GENERATED ALWAYS AS (
206
- setweight(to_tsvector('english', coalesce(title, '')), 'A') ||
207
- setweight(to_tsvector('english', coalesce(excerpt, '')), 'B') ||
208
- setweight(to_tsvector('english', coalesce(body, '')), 'C')
209
- ) STORED;
210
- END IF;
211
- END $$;
212
-
213
- -- Create GIN index on search vector
214
- CREATE INDEX IF NOT EXISTS idx_content_search_vector
215
- ON content USING gin (search_vector);
216
- `);
217
- if (this.logger) {
218
- this.logger.info('Full-text search indexes created successfully');
219
- }
220
- }
221
- /**
222
- * Drop full-text search indexes
223
- */
224
- async dropIndexes() {
225
- if (this.logger) {
226
- this.logger.info('Dropping full-text search indexes');
227
- }
228
- await this.adapter.query(`
229
- DROP INDEX IF EXISTS idx_content_title_trgm;
230
- DROP INDEX IF EXISTS idx_content_body_trgm;
231
- DROP INDEX IF EXISTS idx_content_excerpt_trgm;
232
- DROP INDEX IF EXISTS idx_content_search_vector;
233
-
234
- -- Drop search_vector column
235
- ALTER TABLE content DROP COLUMN IF EXISTS search_vector;
236
- `);
237
- if (this.logger) {
238
- this.logger.info('Full-text search indexes dropped successfully');
239
- }
240
- }
241
- /**
242
- * Get search statistics
243
- */
244
- async getSearchStats() {
245
- const result = await this.adapter.query(`
246
- SELECT
247
- EXISTS (
248
- SELECT 1 FROM pg_extension WHERE extname = 'pg_trgm'
249
- ) as extension_enabled,
250
- EXISTS (
251
- SELECT 1 FROM pg_indexes
252
- WHERE indexname IN ('idx_content_title_trgm', 'idx_content_search_vector')
253
- ) as indexes_exist,
254
- EXISTS (
255
- SELECT 1 FROM information_schema.columns
256
- WHERE table_name = 'content' AND column_name = 'search_vector'
257
- ) as search_vector_exists
258
- `);
259
- const row = result.rows[0];
260
- return {
261
- extensionEnabled: row.extension_enabled,
262
- indexesExist: row.indexes_exist,
263
- searchVectorExists: row.search_vector_exists
264
- };
265
- }
266
- }
267
- exports.FullTextSearchManager = FullTextSearchManager;