postgrest-parser 0.1.0 → 0.1.2

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/pkg/client.ts ADDED
@@ -0,0 +1,528 @@
1
+ /**
2
+ * Type-safe TypeScript wrapper for PostgREST Parser
3
+ *
4
+ * This module provides a type-safe, idiomatic TypeScript API on top of
5
+ * the auto-generated WASM bindings, improving developer experience.
6
+ */
7
+
8
+ import type {
9
+ HttpMethod,
10
+ QueryResult,
11
+ RequestHeaders,
12
+ SelectOptions,
13
+ InsertOptions,
14
+ UpdateOptions,
15
+ DeleteOptions,
16
+ RpcOptions,
17
+ PreferOptions,
18
+ } from "./types.js";
19
+
20
+ import {
21
+ parseRequest as wasmParseRequest,
22
+ parseInsert as wasmParseInsert,
23
+ parseUpdate as wasmParseUpdate,
24
+ parseDelete as wasmParseDelete,
25
+ parseRpc as wasmParseRpc,
26
+ parseOnly as wasmParseOnly,
27
+ buildFilterClause as wasmBuildFilterClause,
28
+ type WasmQueryResult,
29
+ } from "./postgrest_parser.js";
30
+
31
+ // Re-export WASM initialization functions
32
+ export { default as init, initSchemaFromDb } from "./postgrest_parser.js";
33
+
34
+ /**
35
+ * Convert WASM result to typed QueryResult
36
+ */
37
+ function toQueryResult(wasmResult: WasmQueryResult): QueryResult {
38
+ return {
39
+ query: wasmResult.query,
40
+ params: wasmResult.params as QueryResult["params"],
41
+ tables: wasmResult.tables as string[],
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Convert headers object to JSON string
47
+ */
48
+ function headersToJson(headers?: RequestHeaders): string | undefined {
49
+ return headers ? JSON.stringify(headers) : undefined;
50
+ }
51
+
52
+ /**
53
+ * Convert PreferOptions to Prefer header value
54
+ */
55
+ function preferToHeader(prefer?: PreferOptions): string | undefined {
56
+ if (!prefer) return undefined;
57
+
58
+ const parts: string[] = [];
59
+ if (prefer.return) parts.push(`return=${prefer.return}`);
60
+ if (prefer.resolution) parts.push(`resolution=${prefer.resolution}`);
61
+ if (prefer.missing) parts.push(`missing=${prefer.missing}`);
62
+ if (prefer.count) parts.push(`count=${prefer.count}`);
63
+
64
+ return parts.length > 0 ? parts.join(",") : undefined;
65
+ }
66
+
67
+ /**
68
+ * Build query string from filters and options
69
+ */
70
+ function buildQueryString(
71
+ filters?: Record<string, string>,
72
+ options?: {
73
+ select?: string | string[];
74
+ order?: string | string[];
75
+ limit?: number;
76
+ offset?: number;
77
+ onConflict?: string | string[];
78
+ returning?: string | string[];
79
+ }
80
+ ): string {
81
+ const parts: string[] = [];
82
+
83
+ // Add filters
84
+ if (filters) {
85
+ for (const [key, value] of Object.entries(filters)) {
86
+ parts.push(`${key}=${value}`);
87
+ }
88
+ }
89
+
90
+ // Add select
91
+ if (options?.select) {
92
+ const select = Array.isArray(options.select)
93
+ ? options.select.join(",")
94
+ : options.select;
95
+ parts.push(`select=${select}`);
96
+ }
97
+
98
+ // Add order
99
+ if (options?.order) {
100
+ const order = Array.isArray(options.order)
101
+ ? options.order.join(",")
102
+ : options.order;
103
+ parts.push(`order=${order}`);
104
+ }
105
+
106
+ // Add limit
107
+ if (options?.limit !== undefined) {
108
+ parts.push(`limit=${options.limit}`);
109
+ }
110
+
111
+ // Add offset
112
+ if (options?.offset !== undefined) {
113
+ parts.push(`offset=${options.offset}`);
114
+ }
115
+
116
+ // Add on_conflict
117
+ if (options?.onConflict) {
118
+ const onConflict = Array.isArray(options.onConflict)
119
+ ? options.onConflict.join(",")
120
+ : options.onConflict;
121
+ parts.push(`on_conflict=${onConflict}`);
122
+ }
123
+
124
+ // Add returning
125
+ if (options?.returning) {
126
+ const returning = Array.isArray(options.returning)
127
+ ? options.returning.join(",")
128
+ : options.returning;
129
+ parts.push(`returning=${returning}`);
130
+ }
131
+
132
+ return parts.join("&");
133
+ }
134
+
135
+ /**
136
+ * Type-safe PostgREST Parser client
137
+ *
138
+ * Provides strongly-typed methods for generating PostgREST-compatible SQL queries.
139
+ *
140
+ * @example
141
+ * ```typescript
142
+ * const client = new PostgRESTParser();
143
+ *
144
+ * // SELECT query
145
+ * const getUsers = client.select("users", {
146
+ * filters: { "age": "gte.18", "status": "eq.active" },
147
+ * order: ["created_at.desc"],
148
+ * limit: 10
149
+ * });
150
+ *
151
+ * // INSERT query
152
+ * const createUser = client.insert("users", {
153
+ * name: "Alice",
154
+ * email: "alice@example.com"
155
+ * }, {
156
+ * returning: "*",
157
+ * prefer: { return: "representation" }
158
+ * });
159
+ *
160
+ * // Execute with your database client
161
+ * const rows = await db.query(getUsers.query, getUsers.params);
162
+ * ```
163
+ */
164
+ export class PostgRESTParser {
165
+ /**
166
+ * Parse a complete HTTP request and generate appropriate SQL
167
+ *
168
+ * This is the universal routing method that handles all HTTP methods.
169
+ *
170
+ * @param method - HTTP method: "GET", "POST", "PUT", "PATCH", "DELETE"
171
+ * @param path - Resource path (table name or "rpc/function_name")
172
+ * @param queryString - URL query string
173
+ * @param body - Request body (object or null)
174
+ * @param headers - Request headers (object or null)
175
+ * @returns Query result with SQL, params, and tables
176
+ *
177
+ * @example
178
+ * ```typescript
179
+ * const result = client.parseRequest("GET", "users", "age=gte.18", null, null);
180
+ * const rows = await db.query(result.query, result.params);
181
+ * ```
182
+ */
183
+ parseRequest(
184
+ method: HttpMethod,
185
+ path: string,
186
+ queryString: string,
187
+ body?: Record<string, unknown> | Record<string, unknown>[] | null,
188
+ headers?: RequestHeaders | null
189
+ ): QueryResult {
190
+ const bodyJson = body ? JSON.stringify(body) : undefined;
191
+ const headersJson = headers ? headersToJson(headers) : undefined;
192
+
193
+ const result = wasmParseRequest(
194
+ method,
195
+ path,
196
+ queryString,
197
+ bodyJson,
198
+ headersJson
199
+ );
200
+
201
+ return toQueryResult(result);
202
+ }
203
+
204
+ /**
205
+ * Generate a SELECT query
206
+ *
207
+ * @param table - Table name to query
208
+ * @param options - Query options (filters, ordering, pagination)
209
+ * @returns Query result with SQL, params, and tables
210
+ *
211
+ * @example
212
+ * ```typescript
213
+ * const result = client.select("users", {
214
+ * filters: { "age": "gte.18", "status": "eq.active" },
215
+ * order: ["created_at.desc"],
216
+ * limit: 10,
217
+ * offset: 0
218
+ * });
219
+ * ```
220
+ */
221
+ select(table: string, options: SelectOptions = {}): QueryResult {
222
+ const queryString = buildQueryString(options.filters, options);
223
+ const headers: RequestHeaders | undefined = options.count
224
+ ? { Prefer: `count=${options.count}` }
225
+ : undefined;
226
+
227
+ const result = wasmParseRequest(
228
+ "GET",
229
+ table,
230
+ queryString,
231
+ undefined,
232
+ headersToJson(headers)
233
+ );
234
+
235
+ return toQueryResult(result);
236
+ }
237
+
238
+ /**
239
+ * Generate an INSERT query
240
+ *
241
+ * @param table - Table name
242
+ * @param data - Data to insert (single object or array of objects)
243
+ * @param options - Insert options (returning, onConflict, prefer)
244
+ * @returns Query result with SQL, params, and tables
245
+ *
246
+ * @example
247
+ * ```typescript
248
+ * const result = client.insert("users", {
249
+ * name: "Alice",
250
+ * email: "alice@example.com"
251
+ * }, {
252
+ * returning: "*",
253
+ * prefer: { return: "representation" }
254
+ * });
255
+ * ```
256
+ */
257
+ insert(
258
+ table: string,
259
+ data: Record<string, unknown> | Record<string, unknown>[],
260
+ options: InsertOptions = {}
261
+ ): QueryResult {
262
+ const queryString = buildQueryString(undefined, {
263
+ onConflict: options.onConflict,
264
+ returning: options.returning,
265
+ });
266
+
267
+ const preferHeader = preferToHeader(options.prefer);
268
+ const headers: RequestHeaders | undefined = preferHeader
269
+ ? { Prefer: preferHeader }
270
+ : undefined;
271
+
272
+ const result = wasmParseInsert(
273
+ table,
274
+ JSON.stringify(data),
275
+ queryString || undefined,
276
+ headersToJson(headers)
277
+ );
278
+
279
+ return toQueryResult(result);
280
+ }
281
+
282
+ /**
283
+ * Generate an UPSERT query (INSERT with ON CONFLICT)
284
+ *
285
+ * PUT method auto-generates ON CONFLICT from filter columns.
286
+ *
287
+ * @param table - Table name
288
+ * @param data - Data to upsert
289
+ * @param conflictColumns - Columns to use for conflict detection
290
+ * @param options - Upsert options (returning, prefer)
291
+ * @returns Query result with SQL, params, and tables
292
+ *
293
+ * @example
294
+ * ```typescript
295
+ * const result = client.upsert("users", {
296
+ * email: "alice@example.com",
297
+ * name: "Alice Updated"
298
+ * }, ["email"], {
299
+ * returning: "*"
300
+ * });
301
+ * ```
302
+ */
303
+ upsert(
304
+ table: string,
305
+ data: Record<string, unknown>,
306
+ conflictColumns: string[],
307
+ options: InsertOptions = {}
308
+ ): QueryResult {
309
+ // Build filters from conflict columns for PUT auto-conflict
310
+ const filters: Record<string, string> = {};
311
+ for (const col of conflictColumns) {
312
+ if (col in data) {
313
+ filters[col] = `eq.${data[col]}`;
314
+ }
315
+ }
316
+
317
+ const queryString = buildQueryString(filters, {
318
+ returning: options.returning,
319
+ });
320
+
321
+ const preferHeader = preferToHeader(options.prefer);
322
+ const headers: RequestHeaders | undefined = preferHeader
323
+ ? { Prefer: preferHeader }
324
+ : undefined;
325
+
326
+ const result = wasmParseRequest(
327
+ "PUT",
328
+ table,
329
+ queryString,
330
+ JSON.stringify(data),
331
+ headersToJson(headers)
332
+ );
333
+
334
+ return toQueryResult(result);
335
+ }
336
+
337
+ /**
338
+ * Generate an UPDATE query
339
+ *
340
+ * @param table - Table name
341
+ * @param data - Data to update
342
+ * @param filters - Filter conditions to match rows
343
+ * @param options - Update options (returning, prefer)
344
+ * @returns Query result with SQL, params, and tables
345
+ *
346
+ * @example
347
+ * ```typescript
348
+ * const result = client.update("users", {
349
+ * status: "active"
350
+ * }, {
351
+ * "id": "eq.123"
352
+ * }, {
353
+ * returning: "id,status"
354
+ * });
355
+ * ```
356
+ */
357
+ update(
358
+ table: string,
359
+ data: Record<string, unknown>,
360
+ filters: Record<string, string>,
361
+ options: UpdateOptions = {}
362
+ ): QueryResult {
363
+ const queryString = buildQueryString(filters, {
364
+ returning: options.returning,
365
+ });
366
+
367
+ const preferHeader = preferToHeader(options.prefer);
368
+ const headers: RequestHeaders | undefined = preferHeader
369
+ ? { Prefer: preferHeader }
370
+ : undefined;
371
+
372
+ const result = wasmParseUpdate(
373
+ table,
374
+ JSON.stringify(data),
375
+ queryString,
376
+ headersToJson(headers)
377
+ );
378
+
379
+ return toQueryResult(result);
380
+ }
381
+
382
+ /**
383
+ * Generate a DELETE query
384
+ *
385
+ * @param table - Table name
386
+ * @param filters - Filter conditions to match rows to delete
387
+ * @param options - Delete options (returning, prefer)
388
+ * @returns Query result with SQL, params, and tables
389
+ *
390
+ * @example
391
+ * ```typescript
392
+ * const result = client.delete("users", {
393
+ * "status": "eq.inactive",
394
+ * "last_login": "lt.2023-01-01"
395
+ * }, {
396
+ * returning: "id"
397
+ * });
398
+ * ```
399
+ */
400
+ delete(
401
+ table: string,
402
+ filters: Record<string, string>,
403
+ options: DeleteOptions = {}
404
+ ): QueryResult {
405
+ const queryString = buildQueryString(filters, {
406
+ returning: options.returning,
407
+ });
408
+
409
+ const preferHeader = preferToHeader(options.prefer);
410
+ const headers: RequestHeaders | undefined = preferHeader
411
+ ? { Prefer: preferHeader }
412
+ : undefined;
413
+
414
+ const result = wasmParseDelete(
415
+ table,
416
+ queryString,
417
+ headersToJson(headers)
418
+ );
419
+
420
+ return toQueryResult(result);
421
+ }
422
+
423
+ /**
424
+ * Generate an RPC (stored procedure/function) call
425
+ *
426
+ * @param functionName - Function name (can include schema)
427
+ * @param args - Function arguments as object
428
+ * @param options - RPC options (select, filters, ordering)
429
+ * @returns Query result with SQL, params, and tables
430
+ *
431
+ * @example
432
+ * ```typescript
433
+ * const result = client.rpc("calculate_total", {
434
+ * order_id: 123,
435
+ * tax_rate: 0.08
436
+ * }, {
437
+ * select: ["total", "tax"],
438
+ * limit: 1
439
+ * });
440
+ * ```
441
+ */
442
+ rpc(
443
+ functionName: string,
444
+ args: Record<string, unknown> = {},
445
+ options: RpcOptions = {}
446
+ ): QueryResult {
447
+ const queryString = buildQueryString(options.filters, options);
448
+
449
+ const result = wasmParseRpc(
450
+ functionName,
451
+ JSON.stringify(args),
452
+ queryString || undefined,
453
+ undefined
454
+ );
455
+
456
+ return toQueryResult(result);
457
+ }
458
+
459
+ /**
460
+ * Parse a query string without generating SQL
461
+ *
462
+ * Useful for inspecting the parsed structure before generating SQL.
463
+ *
464
+ * @param queryString - PostgREST query string
465
+ * @returns Parsed query parameters as object
466
+ *
467
+ * @example
468
+ * ```typescript
469
+ * const parsed = client.parseOnly("age=gte.18&status=eq.active&order=created_at.desc");
470
+ * console.log(parsed); // { filters: [...], order: [...] }
471
+ * ```
472
+ */
473
+ parseOnly(queryString: string): unknown {
474
+ return wasmParseOnly(queryString);
475
+ }
476
+
477
+ /**
478
+ * Build a WHERE clause from filter conditions
479
+ *
480
+ * @param filters - Filter conditions as object
481
+ * @returns Object with clause (SQL string) and params (array of values)
482
+ *
483
+ * @example
484
+ * ```typescript
485
+ * const filters = [
486
+ * { column: "age", operator: "gte", value: "18" },
487
+ * { column: "status", operator: "eq", value: "active" }
488
+ * ];
489
+ * const result = client.buildFilterClause(filters);
490
+ * console.log(result.clause); // "age >= $1 AND status = $2"
491
+ * console.log(result.params); // ["18", "active"]
492
+ * ```
493
+ */
494
+ buildFilterClause(filters: unknown): unknown {
495
+ return wasmBuildFilterClause(filters);
496
+ }
497
+ }
498
+
499
+ /**
500
+ * Create a new PostgREST Parser client instance
501
+ *
502
+ * @returns New PostgRESTParser instance
503
+ *
504
+ * @example
505
+ * ```typescript
506
+ * import { createClient } from './pkg/client.js';
507
+ *
508
+ * const client = createClient();
509
+ * const result = client.select("users", { limit: 10 });
510
+ * ```
511
+ */
512
+ export function createClient(): PostgRESTParser {
513
+ return new PostgRESTParser();
514
+ }
515
+
516
+ // Re-export types for convenience
517
+ export type {
518
+ HttpMethod,
519
+ QueryResult,
520
+ RequestHeaders,
521
+ SelectOptions,
522
+ InsertOptions,
523
+ UpdateOptions,
524
+ DeleteOptions,
525
+ RpcOptions,
526
+ PreferOptions,
527
+ PostgRESTParserError,
528
+ } from "./types.js";
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "postgrest-parser",
3
+ "type": "module",
4
+ "collaborators": [
5
+ "Your Name"
6
+ ],
7
+ "description": "PostgREST URL-to-SQL parser for Rust and WASM",
8
+ "version": "0.1.2",
9
+ "license": "MIT",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/your-org/postgrest-parser-rust"
13
+ },
14
+ "files": [
15
+ "postgrest_parser_bg.wasm",
16
+ "postgrest_parser.js",
17
+ "postgrest_parser.d.ts"
18
+ ],
19
+ "main": "postgrest_parser.js",
20
+ "types": "postgrest_parser.d.ts",
21
+ "sideEffects": [
22
+ "./snippets/*"
23
+ ],
24
+ "keywords": [
25
+ "postgrest",
26
+ "sql",
27
+ "parser",
28
+ "wasm",
29
+ "postgresql"
30
+ ]
31
+ }