@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.
- package/dist/kysely/pagination.cjs.map +1 -1
- package/dist/kysely/pagination.d.cts +1 -0
- package/dist/kysely/pagination.d.cts.map +1 -0
- package/dist/kysely/pagination.d.mts +1 -0
- package/dist/kysely/pagination.d.mts.map +1 -0
- package/dist/kysely/pagination.mjs.map +1 -1
- package/dist/kysely-8WPSKCZG.cjs.map +1 -1
- package/dist/{kysely-0FOi6ZdO.d.cts → kysely-D3eeuId4.d.cts} +3 -1
- package/dist/kysely-D3eeuId4.d.cts.map +1 -0
- package/dist/{kysely-Di1LVvL2.d.mts → kysely-DUdsB0BP.d.mts} +3 -1
- package/dist/kysely-DUdsB0BP.d.mts.map +1 -0
- package/dist/kysely-DmfA94RY.mjs.map +1 -1
- package/dist/kysely.d.cts +1 -1
- package/dist/kysely.d.mts +1 -1
- package/dist/rls.cjs.map +1 -1
- package/dist/rls.d.cts +2 -1
- package/dist/rls.d.cts.map +1 -0
- package/dist/rls.d.mts +2 -1
- package/dist/rls.d.mts.map +1 -0
- package/dist/rls.mjs.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/kysely.integration.spec.ts +729 -729
- package/src/__tests__/kysely.spec.ts +388 -388
- package/src/__tests__/rls.spec.ts +399 -399
- package/src/kysely/__tests__/pagination.integration.spec.ts +327 -327
- package/src/kysely/pagination.ts +84 -84
- package/src/kysely.ts +19 -19
- package/src/rls.ts +31 -31
- package/tsconfig.json +9 -0
package/src/kysely/pagination.ts
CHANGED
|
@@ -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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
28
|
-
|
|
27
|
+
TRow,
|
|
28
|
+
TMapRow extends (row: TRow) => unknown,
|
|
29
29
|
> {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
65
|
-
|
|
64
|
+
TRow extends Record<string, unknown>,
|
|
65
|
+
TMapRow extends (row: TRow) => unknown,
|
|
66
66
|
>({
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
67
|
+
query,
|
|
68
|
+
cursor,
|
|
69
|
+
limit = 20,
|
|
70
|
+
mapRow,
|
|
71
|
+
cursorField = 'id',
|
|
72
|
+
cursorDirection = Direction.Asc,
|
|
73
73
|
}: PaginatedSearchOptions<TRow, TMapRow>): Promise<
|
|
74
|
-
|
|
74
|
+
PaginationResult<Awaited<ReturnType<TMapRow>>>
|
|
75
75
|
> {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
83
|
+
const count = countResult.count;
|
|
84
84
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
const items = (await Promise.all(rows.map(mapRow))) as Awaited<
|
|
109
|
+
ReturnType<TMapRow>
|
|
110
|
+
>[];
|
|
111
111
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
138
|
+
try {
|
|
139
|
+
const json = Buffer.from(cursor, 'base64url').toString('utf-8');
|
|
140
|
+
const payload = JSON.parse(json);
|
|
141
141
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
142
|
+
if (payload.t === 'date') {
|
|
143
|
+
return new Date(payload.v);
|
|
144
|
+
}
|
|
145
145
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
ControlledTransaction,
|
|
3
|
+
IsolationLevel,
|
|
4
|
+
Kysely,
|
|
5
|
+
Transaction,
|
|
6
6
|
} from 'kysely';
|
|
7
7
|
|
|
8
8
|
export interface TransactionSettings {
|
|
9
|
-
|
|
9
|
+
isolationLevel?: IsolationLevel;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export function withTransaction<DB, T>(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
db: DatabaseConnection<DB>,
|
|
14
|
+
cb: (trx: Transaction<DB>) => Promise<T>,
|
|
15
|
+
settings?: TransactionSettings,
|
|
16
16
|
): Promise<T> {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
if (db.isTransaction) {
|
|
18
|
+
return cb(db as Transaction<DB>);
|
|
19
|
+
}
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
const builder = db.transaction();
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
if (settings?.isolationLevel) {
|
|
24
|
+
return builder.setIsolationLevel(settings.isolationLevel).execute(cb);
|
|
25
|
+
}
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
return builder.execute(cb);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export type DatabaseConnection<T> =
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
db: DatabaseConnection<DB>,
|
|
53
|
+
context: RlsContext,
|
|
54
|
+
callback: (trx: Transaction<DB>) => Promise<T>,
|
|
55
|
+
options?: WithRlsContextOptions,
|
|
56
56
|
): Promise<T> {
|
|
57
|
-
|
|
57
|
+
const prefix = options?.prefix ?? 'app';
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
67
|
-
|
|
66
|
+
const settingName = `${prefix}.${key}`;
|
|
67
|
+
const settingValue = String(value);
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
return callback(trx);
|
|
77
|
+
},
|
|
78
|
+
options?.settings,
|
|
79
|
+
);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
/**
|