@pgpm/utils 0.4.0 → 0.5.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/Makefile CHANGED
@@ -1,5 +1,5 @@
1
1
  EXTENSION = launchql-utils
2
- DATA = sql/launchql-utils--0.4.6.sql
2
+ DATA = sql/launchql-utils--0.4.0.sql
3
3
 
4
4
  PG_CONFIG = pg_config
5
5
  PGXS := $(shell $(PG_CONFIG) --pgxs)
package/README.md CHANGED
@@ -1,5 +1,468 @@
1
1
  # @pgpm/utils
2
2
 
3
- General utility functions for PostgreSQL extensions.
3
+ General utility functions for PostgreSQL extensions
4
4
 
5
- Provides common utility functions, helper procedures, and shared functionality used across LaunchQL extension modules.
5
+ ## Overview
6
+
7
+ `@pgpm/utils` provides essential utility functions and helper procedures used across LaunchQL extension modules. This package includes functions for error handling, data masking, and singleton pattern enforcement. These utilities form the foundation for building robust PostgreSQL extensions with consistent error handling and data protection patterns.
8
+
9
+ ## Features
10
+
11
+ - **Error Handling**: Throw custom exceptions with detailed messages
12
+ - **Data Masking**: Mask sensitive data with padding characters
13
+ - **Singleton Enforcement**: Ensure tables contain exactly one row
14
+ - **Reusable Utilities**: Common functions used across multiple extensions
15
+ - **Pure plpgsql**: No external dependencies required
16
+
17
+ ## Installation
18
+
19
+ If you have `pgpm` installed:
20
+
21
+ ```bash
22
+ pgpm install @pgpm/utils
23
+ pgpm deploy
24
+ ```
25
+
26
+ This is a quick way to get started. The sections below provide more detailed installation options.
27
+
28
+ ### Prerequisites
29
+
30
+ ```bash
31
+ # Install pgpm globally
32
+ npm install -g pgpm
33
+
34
+ # Start PostgreSQL
35
+ pgpm docker start
36
+
37
+ # Set environment variables
38
+ eval "$(pgpm env)"
39
+ ```
40
+
41
+ ### Deploy
42
+
43
+ #### Option 1: Deploy by installing with pgpm
44
+
45
+ ```bash
46
+ pgpm install @pgpm/utils
47
+ pgpm deploy
48
+ ```
49
+
50
+ #### Option 2: Deploy from Package Directory
51
+
52
+ ```bash
53
+ cd packages/utils/utils
54
+ pgpm deploy --createdb
55
+ ```
56
+
57
+ #### Option 3: Deploy from Workspace Root
58
+
59
+ ```bash
60
+ # Install workspace dependencies
61
+ pgpm install
62
+
63
+ # Deploy with dependencies
64
+ pgpm deploy mydb1 --yes --createdb
65
+ ```
66
+
67
+ ## Core Functions
68
+
69
+ ### utils.throw(message text)
70
+
71
+ Throw a custom exception with a specific message.
72
+
73
+ **Signature:**
74
+ ```sql
75
+ utils.throw(message text) RETURNS void
76
+ ```
77
+
78
+ **Usage:**
79
+ ```sql
80
+ -- Throw an error
81
+ SELECT utils.throw('Invalid user ID provided');
82
+ -- ERROR: Invalid user ID provided
83
+
84
+ -- Use in conditional logic
85
+ DO $$
86
+ BEGIN
87
+ IF NOT EXISTS (SELECT 1 FROM users WHERE id = 'some-uuid') THEN
88
+ PERFORM utils.throw('User not found');
89
+ END IF;
90
+ END $$;
91
+
92
+ -- Use in functions
93
+ CREATE FUNCTION validate_email(email text)
94
+ RETURNS boolean AS $$
95
+ BEGIN
96
+ IF email !~ '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}$' THEN
97
+ PERFORM utils.throw('Invalid email format: ' || email);
98
+ END IF;
99
+ RETURN true;
100
+ END;
101
+ $$ LANGUAGE plpgsql;
102
+ ```
103
+
104
+ ### utils.mask_pad(value text, visible_chars int, pad_char text)
105
+
106
+ Mask sensitive data by showing only a specified number of characters and padding the rest.
107
+
108
+ **Signature:**
109
+ ```sql
110
+ utils.mask_pad(
111
+ value text,
112
+ visible_chars int DEFAULT 4,
113
+ pad_char text DEFAULT '*'
114
+ ) RETURNS text
115
+ ```
116
+
117
+ **Parameters:**
118
+ - `value`: The text to mask
119
+ - `visible_chars`: Number of characters to show at the end (default: 4)
120
+ - `pad_char`: Character to use for masking (default: '*')
121
+
122
+ **Usage:**
123
+ ```sql
124
+ -- Mask credit card number
125
+ SELECT utils.mask_pad('4532123456789012', 4, '*');
126
+ -- ************9012
127
+
128
+ -- Mask email
129
+ SELECT utils.mask_pad('user@example.com', 6, 'x');
130
+ -- xxxxxxxxple.com
131
+
132
+ -- Mask phone number
133
+ SELECT utils.mask_pad('+1-555-123-4567', 4, '#');
134
+ -- ###########4567
135
+
136
+ -- Use in queries
137
+ SELECT
138
+ user_id,
139
+ utils.mask_pad(credit_card, 4, '*') as masked_card,
140
+ utils.mask_pad(ssn, 4, 'X') as masked_ssn
141
+ FROM sensitive_data;
142
+ ```
143
+
144
+ ### utils.ensure_singleton()
145
+
146
+ Trigger function to ensure a table contains exactly one row.
147
+
148
+ **Signature:**
149
+ ```sql
150
+ utils.ensure_singleton() RETURNS trigger
151
+ ```
152
+
153
+ **Usage:**
154
+ ```sql
155
+ -- Create a settings table that should only have one row
156
+ CREATE TABLE app_settings (
157
+ id int PRIMARY KEY DEFAULT 1,
158
+ app_name text NOT NULL,
159
+ max_users int DEFAULT 100,
160
+ maintenance_mode boolean DEFAULT false,
161
+ updated_at timestamptz DEFAULT now(),
162
+ CONSTRAINT singleton_check CHECK (id = 1)
163
+ );
164
+
165
+ -- Add trigger to enforce singleton pattern
166
+ CREATE TRIGGER ensure_singleton_trigger
167
+ BEFORE INSERT OR UPDATE ON app_settings
168
+ FOR EACH ROW
169
+ EXECUTE FUNCTION utils.ensure_singleton();
170
+
171
+ -- First insert works
172
+ INSERT INTO app_settings (app_name) VALUES ('My App');
173
+
174
+ -- Second insert fails
175
+ INSERT INTO app_settings (app_name) VALUES ('Another App');
176
+ -- ERROR: Table app_settings can only contain one row
177
+
178
+ -- Updates work fine
179
+ UPDATE app_settings SET max_users = 200;
180
+ ```
181
+
182
+ ## Usage Examples
183
+
184
+ ### Error Handling in Business Logic
185
+
186
+ Use `utils.throw()` for consistent error handling:
187
+
188
+ ```sql
189
+ CREATE FUNCTION process_payment(
190
+ user_id uuid,
191
+ amount numeric
192
+ ) RETURNS void AS $$
193
+ DECLARE
194
+ user_balance numeric;
195
+ BEGIN
196
+ -- Check if user exists
197
+ IF NOT EXISTS (SELECT 1 FROM users WHERE id = user_id) THEN
198
+ PERFORM utils.throw('User not found: ' || user_id);
199
+ END IF;
200
+
201
+ -- Check balance
202
+ SELECT balance INTO user_balance FROM accounts WHERE user_id = user_id;
203
+
204
+ IF user_balance IS NULL THEN
205
+ PERFORM utils.throw('Account not found for user: ' || user_id);
206
+ END IF;
207
+
208
+ IF user_balance < amount THEN
209
+ PERFORM utils.throw('Insufficient funds. Balance: ' || user_balance || ', Required: ' || amount);
210
+ END IF;
211
+
212
+ -- Process payment
213
+ UPDATE accounts SET balance = balance - amount WHERE user_id = user_id;
214
+ END;
215
+ $$ LANGUAGE plpgsql;
216
+ ```
217
+
218
+ ### Data Masking for Privacy
219
+
220
+ Protect sensitive data in logs and reports:
221
+
222
+ ```sql
223
+ -- Create view with masked data for reporting
224
+ CREATE VIEW user_report AS
225
+ SELECT
226
+ id,
227
+ username,
228
+ utils.mask_pad(email, 8, '*') as email,
229
+ utils.mask_pad(phone, 4, 'X') as phone,
230
+ utils.mask_pad(credit_card, 4, '*') as credit_card,
231
+ created_at
232
+ FROM users;
233
+
234
+ -- Audit log with masked sensitive data
235
+ CREATE FUNCTION log_user_access(user_id uuid)
236
+ RETURNS void AS $$
237
+ DECLARE
238
+ user_email text;
239
+ BEGIN
240
+ SELECT email INTO user_email FROM users WHERE id = user_id;
241
+
242
+ INSERT INTO access_logs (user_id, masked_email, accessed_at)
243
+ VALUES (user_id, utils.mask_pad(user_email, 6, '*'), now());
244
+ END;
245
+ $$ LANGUAGE plpgsql;
246
+ ```
247
+
248
+ ### Singleton Configuration Tables
249
+
250
+ Ensure configuration tables have exactly one row:
251
+
252
+ ```sql
253
+ -- System configuration table
254
+ CREATE TABLE system_config (
255
+ id int PRIMARY KEY DEFAULT 1,
256
+ site_name text NOT NULL,
257
+ admin_email text NOT NULL,
258
+ max_upload_size_mb int DEFAULT 10,
259
+ enable_registration boolean DEFAULT true,
260
+ created_at timestamptz DEFAULT now(),
261
+ updated_at timestamptz DEFAULT now(),
262
+ CONSTRAINT singleton_check CHECK (id = 1)
263
+ );
264
+
265
+ CREATE TRIGGER ensure_singleton_trigger
266
+ BEFORE INSERT OR UPDATE ON system_config
267
+ FOR EACH ROW
268
+ EXECUTE FUNCTION utils.ensure_singleton();
269
+
270
+ -- Initialize with default values
271
+ INSERT INTO system_config (site_name, admin_email)
272
+ VALUES ('My Application', 'admin@example.com');
273
+
274
+ -- Helper function to get config
275
+ CREATE FUNCTION get_config()
276
+ RETURNS system_config AS $$
277
+ SELECT * FROM system_config LIMIT 1;
278
+ $$ LANGUAGE sql STABLE;
279
+
280
+ -- Helper function to update config
281
+ CREATE FUNCTION update_config(
282
+ p_site_name text DEFAULT NULL,
283
+ p_admin_email text DEFAULT NULL,
284
+ p_max_upload_size_mb int DEFAULT NULL
285
+ ) RETURNS void AS $$
286
+ BEGIN
287
+ UPDATE system_config SET
288
+ site_name = COALESCE(p_site_name, site_name),
289
+ admin_email = COALESCE(p_admin_email, admin_email),
290
+ max_upload_size_mb = COALESCE(p_max_upload_size_mb, max_upload_size_mb),
291
+ updated_at = now();
292
+ END;
293
+ $$ LANGUAGE plpgsql;
294
+ ```
295
+
296
+ ## Integration Examples
297
+
298
+ ### With @pgpm/encrypted-secrets
299
+
300
+ Combine error handling with secrets management:
301
+
302
+ ```sql
303
+ CREATE FUNCTION get_api_key(service_name text)
304
+ RETURNS text AS $$
305
+ DECLARE
306
+ api_key text;
307
+ BEGIN
308
+ api_key := encrypted_secrets.secrets_getter(
309
+ current_user_id(),
310
+ service_name
311
+ );
312
+
313
+ IF api_key IS NULL THEN
314
+ PERFORM utils.throw('API key not found for service: ' || service_name);
315
+ END IF;
316
+
317
+ RETURN api_key;
318
+ END;
319
+ $$ LANGUAGE plpgsql;
320
+ ```
321
+
322
+ ### With @pgpm/jobs
323
+
324
+ Use error handling in job processing:
325
+
326
+ ```sql
327
+ CREATE FUNCTION process_job_with_validation(job_id int)
328
+ RETURNS void AS $$
329
+ DECLARE
330
+ job_data jsonb;
331
+ BEGIN
332
+ SELECT payload INTO job_data FROM app_jobs.jobs WHERE id = job_id;
333
+
334
+ IF job_data IS NULL THEN
335
+ PERFORM utils.throw('Job not found: ' || job_id);
336
+ END IF;
337
+
338
+ IF NOT (job_data ? 'email') THEN
339
+ PERFORM utils.throw('Job payload missing required field: email');
340
+ END IF;
341
+
342
+ -- Process job
343
+ -- ...
344
+ END;
345
+ $$ LANGUAGE plpgsql;
346
+ ```
347
+
348
+ ### With @pgpm/achievements
349
+
350
+ Mask user data in achievement displays:
351
+
352
+ ```sql
353
+ CREATE VIEW public_achievements AS
354
+ SELECT
355
+ utils.mask_pad(u.email, 8, '*') as masked_email,
356
+ a.achievement_name,
357
+ a.count,
358
+ a.achieved_at
359
+ FROM user_achievements a
360
+ JOIN users u ON u.id = a.user_id
361
+ WHERE a.achieved_at IS NOT NULL;
362
+ ```
363
+
364
+ ## Use Cases
365
+
366
+ - **Error Handling**: Consistent exception throwing across extensions
367
+ - **Data Privacy**: Mask sensitive information in logs and reports
368
+ - **Configuration Management**: Singleton tables for application settings
369
+ - **Validation**: Input validation with clear error messages
370
+ - **Audit Logging**: Log events with masked sensitive data
371
+ - **API Responses**: Return masked data to clients
372
+ - **Development**: Shared utilities for extension development
373
+
374
+ ## Testing
375
+
376
+ ```bash
377
+ pnpm test
378
+ ```
379
+
380
+ ## Dependencies
381
+
382
+ - `@pgpm/verify`: Verification utilities
383
+
384
+ ## Development
385
+
386
+ See the [Development](#development-1) section below for information on working with this package.
387
+
388
+ ---
389
+
390
+ ## Development
391
+
392
+ ### **Before You Begin**
393
+
394
+ ```bash
395
+ # 1. Install pgpm
396
+ npm install -g pgpm
397
+
398
+ # 2. Start Postgres (Docker or local)
399
+ pgpm docker start
400
+
401
+ # 3. Load PG* environment variables (PGHOST, PGUSER, ...)
402
+ eval "$(pgpm env)"
403
+ ```
404
+
405
+ ---
406
+
407
+ ### **Starting a New Project**
408
+
409
+ ```bash
410
+ # 1. Create a workspace
411
+ pgpm init --workspace
412
+ cd my-app
413
+
414
+ # 2. Create your first module
415
+ pgpm init
416
+
417
+ # 3. Add a migration
418
+ pgpm add some_change
419
+
420
+ # 4. Deploy (auto-creates database)
421
+ pgpm deploy --createdb
422
+ ```
423
+
424
+ ---
425
+
426
+ ### **Working With an Existing Project**
427
+
428
+ ```bash
429
+ # 1. Clone and enter the project
430
+ git clone <repo> && cd <project>
431
+
432
+ # 2. Install dependencies
433
+ pnpm install
434
+
435
+ # 3. Deploy locally
436
+ pgpm deploy --createdb
437
+ ```
438
+
439
+ ---
440
+
441
+ ### **Testing a Module Inside a Workspace**
442
+
443
+ ```bash
444
+ # 1. Install workspace deps
445
+ pnpm install
446
+
447
+ # 2. Enter the module directory
448
+ cd packages/<some-module>
449
+
450
+ # 3. Run tests in watch mode
451
+ pnpm test:watch
452
+ ```
453
+
454
+ ## Related Tooling
455
+
456
+ * [pgpm](https://github.com/launchql/launchql/tree/main/packages/pgpm): **🖥️ PostgreSQL Package Manager** for modular Postgres development. Works with database workspaces, scaffolding, migrations, seeding, and installing database packages.
457
+ * [pgsql-test](https://github.com/launchql/launchql/tree/main/packages/pgsql-test): **📊 Isolated testing environments** with per-test transaction rollbacks—ideal for integration tests, complex migrations, and RLS simulation.
458
+ * [supabase-test](https://github.com/launchql/launchql/tree/main/packages/supabase-test): **🧪 Supabase-native test harness** preconfigured for the local Supabase stack—per-test rollbacks, JWT/role context helpers, and CI/GitHub Actions ready.
459
+ * [graphile-test](https://github.com/launchql/launchql/tree/main/packages/graphile-test): **🔐 Authentication mocking** for Graphile-focused test helpers and emulating row-level security contexts.
460
+ * [pgsql-parser](https://github.com/launchql/pgsql-parser): **🔄 SQL conversion engine** that interprets and converts PostgreSQL syntax.
461
+ * [libpg-query-node](https://github.com/launchql/libpg-query-node): **🌉 Node.js bindings** for `libpg_query`, converting SQL into parse trees.
462
+ * [pg-proto-parser](https://github.com/launchql/pg-proto-parser): **📦 Protobuf parser** for parsing PostgreSQL Protocol Buffers definitions to generate TypeScript interfaces, utility functions, and JSON mappings for enums.
463
+
464
+ ## Disclaimer
465
+
466
+ AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND.
467
+
468
+ No developer or entity involved in creating this software will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of the code, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value.
@@ -1,6 +1,6 @@
1
1
  # launchql-utils extension
2
2
  comment = 'launchql-utils extension'
3
- default_version = '0.4.6'
3
+ default_version = '0.4.0'
4
4
  module_pathname = '$libdir/launchql-utils'
5
5
  requires = 'plpgsql,launchql-verify'
6
6
  relocatable = false
package/package.json CHANGED
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "name": "@pgpm/utils",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "General utility functions for PostgreSQL extensions",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
8
8
  "scripts": {
9
- "bundle": "lql package",
9
+ "bundle": "pgpm package",
10
10
  "test": "jest",
11
11
  "test:watch": "jest --watch"
12
12
  },
13
13
  "dependencies": {
14
- "@pgpm/verify": "0.4.0"
14
+ "@pgpm/verify": "0.5.0"
15
15
  },
16
16
  "devDependencies": {
17
- "@launchql/cli": "^4.9.0"
17
+ "pgpm": "^0.2.0"
18
18
  },
19
19
  "repository": {
20
20
  "type": "git",
@@ -24,5 +24,5 @@
24
24
  "bugs": {
25
25
  "url": "https://github.com/launchql/extensions/issues"
26
26
  },
27
- "gitHead": "cc9f52a335caa6e21ee7751b04b77c84ce6cb809"
27
+ "gitHead": "d8eedbb24ad22a106634bc3b919bfb8d41976c16"
28
28
  }
File without changes