@geekmidas/db 0.2.0 → 0.3.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.
@@ -4,41 +4,41 @@ import type { SelectQueryBuilder } from 'kysely';
4
4
  * Sort direction for cursor-based pagination.
5
5
  */
6
6
  export enum Direction {
7
- Asc = 'asc',
8
- Desc = 'desc',
7
+ Asc = 'asc',
8
+ Desc = 'desc',
9
9
  }
10
10
 
11
11
  /**
12
12
  * Result of a paginated query.
13
13
  */
14
14
  export interface PaginationResult<TItem> {
15
- items: TItem[];
16
- pagination: {
17
- total: number;
18
- hasMore: boolean;
19
- cursor?: string;
20
- };
15
+ items: TItem[];
16
+ pagination: {
17
+ total: number;
18
+ hasMore: boolean;
19
+ cursor?: string;
20
+ };
21
21
  }
22
22
 
23
23
  /**
24
24
  * Options for paginated search.
25
25
  */
26
26
  export interface PaginatedSearchOptions<
27
- TRow,
28
- TMapRow extends (row: TRow) => unknown,
27
+ TRow,
28
+ TMapRow extends (row: TRow) => unknown,
29
29
  > {
30
- /** The base Kysely query to paginate */
31
- query: SelectQueryBuilder<any, any, TRow>;
32
- /** Cursor value for pagination (value of cursorField from previous page) */
33
- cursor?: string;
34
- /** Maximum number of items per page (default: 20) */
35
- limit?: number;
36
- /** Function to transform each row to the output format */
37
- mapRow: TMapRow;
38
- /** Field to use for cursor pagination (default: 'id') */
39
- cursorField?: string;
40
- /** Sort direction for cursor field (default: Direction.Asc) */
41
- cursorDirection?: Direction;
30
+ /** The base Kysely query to paginate */
31
+ query: SelectQueryBuilder<any, any, TRow>;
32
+ /** Cursor value for pagination (value of cursorField from previous page) */
33
+ cursor?: string;
34
+ /** Maximum number of items per page (default: 20) */
35
+ limit?: number;
36
+ /** Function to transform each row to the output format */
37
+ mapRow: TMapRow;
38
+ /** Field to use for cursor pagination (default: 'id') */
39
+ cursorField?: string;
40
+ /** Sort direction for cursor field (default: Direction.Asc) */
41
+ cursorDirection?: Direction;
42
42
  }
43
43
 
44
44
  /**
@@ -61,62 +61,62 @@ export interface PaginatedSearchOptions<
61
61
  * ```
62
62
  */
63
63
  export async function paginatedSearch<
64
- TRow extends Record<string, unknown>,
65
- TMapRow extends (row: TRow) => unknown,
64
+ TRow extends Record<string, unknown>,
65
+ TMapRow extends (row: TRow) => unknown,
66
66
  >({
67
- query,
68
- cursor,
69
- limit = 20,
70
- mapRow,
71
- cursorField = 'id',
72
- cursorDirection = Direction.Asc,
67
+ query,
68
+ cursor,
69
+ limit = 20,
70
+ mapRow,
71
+ cursorField = 'id',
72
+ cursorDirection = Direction.Asc,
73
73
  }: PaginatedSearchOptions<TRow, TMapRow>): Promise<
74
- PaginationResult<Awaited<ReturnType<TMapRow>>>
74
+ PaginationResult<Awaited<ReturnType<TMapRow>>>
75
75
  > {
76
- // Get total count (without cursor)
77
- const countResult = await query
78
- .clearSelect()
79
- .clearOrderBy()
80
- .select((eb) => eb.fn.countAll().as('count'))
81
- .executeTakeFirstOrThrow();
76
+ // Get total count (without cursor)
77
+ const countResult = await query
78
+ .clearSelect()
79
+ .clearOrderBy()
80
+ .select((eb) => eb.fn.countAll().as('count'))
81
+ .executeTakeFirstOrThrow();
82
82
 
83
- const count = countResult.count;
83
+ const count = countResult.count;
84
84
 
85
- // Apply cursor if provided
86
- let paginatedQuery = query;
87
- if (cursor) {
88
- const operator = cursorDirection === Direction.Asc ? '>' : '<';
89
- paginatedQuery = paginatedQuery.where(
90
- cursorField as any,
91
- operator,
92
- cursor,
93
- ) as typeof query;
94
- }
85
+ // Apply cursor if provided
86
+ let paginatedQuery = query;
87
+ if (cursor) {
88
+ const operator = cursorDirection === Direction.Asc ? '>' : '<';
89
+ paginatedQuery = paginatedQuery.where(
90
+ cursorField as any,
91
+ operator,
92
+ cursor,
93
+ ) as typeof query;
94
+ }
95
95
 
96
- // Fetch one extra to determine if there are more results
97
- const data = await paginatedQuery
98
- .orderBy(cursorField as any, cursorDirection)
99
- .limit(limit + 1)
100
- .execute();
96
+ // Fetch one extra to determine if there are more results
97
+ const data = await paginatedQuery
98
+ .orderBy(cursorField as any, cursorDirection)
99
+ .limit(limit + 1)
100
+ .execute();
101
101
 
102
- const hasMore = data.length > limit;
103
- const rows = hasMore ? data.slice(0, limit) : data;
104
- const lastRow = rows[rows.length - 1];
105
- const nextCursor =
106
- hasMore && lastRow ? String(lastRow[cursorField]) : undefined;
102
+ const hasMore = data.length > limit;
103
+ const rows = hasMore ? data.slice(0, limit) : data;
104
+ const lastRow = rows[rows.length - 1];
105
+ const nextCursor =
106
+ hasMore && lastRow ? String(lastRow[cursorField]) : undefined;
107
107
 
108
- const items = (await Promise.all(rows.map(mapRow))) as Awaited<
109
- ReturnType<TMapRow>
110
- >[];
108
+ const items = (await Promise.all(rows.map(mapRow))) as Awaited<
109
+ ReturnType<TMapRow>
110
+ >[];
111
111
 
112
- return {
113
- items,
114
- pagination: {
115
- total: Number(count),
116
- hasMore,
117
- cursor: nextCursor,
118
- },
119
- };
112
+ return {
113
+ items,
114
+ pagination: {
115
+ total: Number(count),
116
+ hasMore,
117
+ cursor: nextCursor,
118
+ },
119
+ };
120
120
  }
121
121
 
122
122
  /**
@@ -124,27 +124,27 @@ export async function paginatedSearch<
124
124
  * Supports various types: string, number, Date, etc.
125
125
  */
126
126
  export function encodeCursor(value: unknown): string {
127
- const payload = {
128
- v: value instanceof Date ? value.toISOString() : value,
129
- t: value instanceof Date ? 'date' : typeof value,
130
- };
131
- return Buffer.from(JSON.stringify(payload)).toString('base64url');
127
+ const payload = {
128
+ v: value instanceof Date ? value.toISOString() : value,
129
+ t: value instanceof Date ? 'date' : typeof value,
130
+ };
131
+ return Buffer.from(JSON.stringify(payload)).toString('base64url');
132
132
  }
133
133
 
134
134
  /**
135
135
  * Decode a cursor string back to its original value.
136
136
  */
137
137
  export function decodeCursor(cursor: string): unknown {
138
- try {
139
- const json = Buffer.from(cursor, 'base64url').toString('utf-8');
140
- const payload = JSON.parse(json);
138
+ try {
139
+ const json = Buffer.from(cursor, 'base64url').toString('utf-8');
140
+ const payload = JSON.parse(json);
141
141
 
142
- if (payload.t === 'date') {
143
- return new Date(payload.v);
144
- }
142
+ if (payload.t === 'date') {
143
+ return new Date(payload.v);
144
+ }
145
145
 
146
- return payload.v;
147
- } catch {
148
- throw new Error('Invalid cursor format');
149
- }
146
+ return payload.v;
147
+ } catch {
148
+ throw new Error('Invalid cursor format');
149
+ }
150
150
  }
package/src/kysely.ts CHANGED
@@ -1,33 +1,33 @@
1
1
  import type {
2
- ControlledTransaction,
3
- IsolationLevel,
4
- Kysely,
5
- Transaction,
2
+ ControlledTransaction,
3
+ IsolationLevel,
4
+ Kysely,
5
+ Transaction,
6
6
  } from 'kysely';
7
7
 
8
8
  export interface TransactionSettings {
9
- isolationLevel?: IsolationLevel;
9
+ isolationLevel?: IsolationLevel;
10
10
  }
11
11
 
12
12
  export function withTransaction<DB, T>(
13
- db: DatabaseConnection<DB>,
14
- cb: (trx: Transaction<DB>) => Promise<T>,
15
- settings?: TransactionSettings,
13
+ db: DatabaseConnection<DB>,
14
+ cb: (trx: Transaction<DB>) => Promise<T>,
15
+ settings?: TransactionSettings,
16
16
  ): Promise<T> {
17
- if (db.isTransaction) {
18
- return cb(db as Transaction<DB>);
19
- }
17
+ if (db.isTransaction) {
18
+ return cb(db as Transaction<DB>);
19
+ }
20
20
 
21
- const builder = db.transaction();
21
+ const builder = db.transaction();
22
22
 
23
- if (settings?.isolationLevel) {
24
- return builder.setIsolationLevel(settings.isolationLevel).execute(cb);
25
- }
23
+ if (settings?.isolationLevel) {
24
+ return builder.setIsolationLevel(settings.isolationLevel).execute(cb);
25
+ }
26
26
 
27
- return builder.execute(cb);
27
+ return builder.execute(cb);
28
28
  }
29
29
 
30
30
  export type DatabaseConnection<T> =
31
- | ControlledTransaction<T>
32
- | Kysely<T>
33
- | Transaction<T>;
31
+ | ControlledTransaction<T>
32
+ | Kysely<T>
33
+ | Transaction<T>;
package/src/rls.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import type { Transaction } from 'kysely';
2
2
  import { sql } from 'kysely';
3
3
  import {
4
- type DatabaseConnection,
5
- type TransactionSettings,
6
- withTransaction,
4
+ type DatabaseConnection,
5
+ type TransactionSettings,
6
+ withTransaction,
7
7
  } from './kysely';
8
8
 
9
9
  /**
@@ -11,17 +11,17 @@ import {
11
11
  * Keys become `prefix.key` (e.g., `app.user_id`).
12
12
  */
13
13
  export interface RlsContext {
14
- [key: string]: string | number | boolean | null | undefined;
14
+ [key: string]: string | number | boolean | null | undefined;
15
15
  }
16
16
 
17
17
  /**
18
18
  * Options for withRlsContext function.
19
19
  */
20
20
  export interface WithRlsContextOptions {
21
- /** Prefix for PostgreSQL session variables (default: 'app') */
22
- prefix?: string;
23
- /** Transaction settings (isolation level) */
24
- settings?: TransactionSettings;
21
+ /** Prefix for PostgreSQL session variables (default: 'app') */
22
+ prefix?: string;
23
+ /** Transaction settings (isolation level) */
24
+ settings?: TransactionSettings;
25
25
  }
26
26
 
27
27
  /**
@@ -49,34 +49,34 @@ export interface WithRlsContextOptions {
49
49
  * @param options - Optional prefix and transaction settings
50
50
  */
51
51
  export async function withRlsContext<DB, T>(
52
- db: DatabaseConnection<DB>,
53
- context: RlsContext,
54
- callback: (trx: Transaction<DB>) => Promise<T>,
55
- options?: WithRlsContextOptions,
52
+ db: DatabaseConnection<DB>,
53
+ context: RlsContext,
54
+ callback: (trx: Transaction<DB>) => Promise<T>,
55
+ options?: WithRlsContextOptions,
56
56
  ): Promise<T> {
57
- const prefix = options?.prefix ?? 'app';
57
+ const prefix = options?.prefix ?? 'app';
58
58
 
59
- return withTransaction(
60
- db,
61
- async (trx) => {
62
- // Set each context variable using SET LOCAL (scoped to transaction)
63
- for (const [key, value] of Object.entries(context)) {
64
- if (value === null || value === undefined) continue;
59
+ return withTransaction(
60
+ db,
61
+ async (trx) => {
62
+ // Set each context variable using SET LOCAL (scoped to transaction)
63
+ for (const [key, value] of Object.entries(context)) {
64
+ if (value === null || value === undefined) continue;
65
65
 
66
- const settingName = `${prefix}.${key}`;
67
- const settingValue = String(value);
66
+ const settingName = `${prefix}.${key}`;
67
+ const settingValue = String(value);
68
68
 
69
- // Use raw SQL for SET LOCAL with proper escaping
70
- // The setting name is an identifier, value is a string literal
71
- await sql`SELECT set_config(${settingName}, ${settingValue}, true)`.execute(
72
- trx,
73
- );
74
- }
69
+ // Use raw SQL for SET LOCAL with proper escaping
70
+ // The setting name is an identifier, value is a string literal
71
+ await sql`SELECT set_config(${settingName}, ${settingValue}, true)`.execute(
72
+ trx,
73
+ );
74
+ }
75
75
 
76
- return callback(trx);
77
- },
78
- options?.settings,
79
- );
76
+ return callback(trx);
77
+ },
78
+ options?.settings,
79
+ );
80
80
  }
81
81
 
82
82
  /**
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "composite": true
7
+ },
8
+ "include": ["src/**/*"]
9
+ }