@geekmidas/studio 0.0.1

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.
Files changed (64) hide show
  1. package/dist/DataBrowser-DQ3-ZxdV.mjs +427 -0
  2. package/dist/DataBrowser-DQ3-ZxdV.mjs.map +1 -0
  3. package/dist/DataBrowser-SOcqmZb2.d.mts +267 -0
  4. package/dist/DataBrowser-c-Gs6PZB.cjs +432 -0
  5. package/dist/DataBrowser-c-Gs6PZB.cjs.map +1 -0
  6. package/dist/DataBrowser-hGwiTffZ.d.cts +267 -0
  7. package/dist/chunk-CUT6urMc.cjs +30 -0
  8. package/dist/data/index.cjs +4 -0
  9. package/dist/data/index.d.cts +2 -0
  10. package/dist/data/index.d.mts +2 -0
  11. package/dist/data/index.mjs +4 -0
  12. package/dist/index.cjs +239 -0
  13. package/dist/index.cjs.map +1 -0
  14. package/dist/index.d.cts +132 -0
  15. package/dist/index.d.mts +132 -0
  16. package/dist/index.mjs +230 -0
  17. package/dist/index.mjs.map +1 -0
  18. package/dist/server/hono.cjs +192 -0
  19. package/dist/server/hono.cjs.map +1 -0
  20. package/dist/server/hono.d.cts +19 -0
  21. package/dist/server/hono.d.mts +19 -0
  22. package/dist/server/hono.mjs +191 -0
  23. package/dist/server/hono.mjs.map +1 -0
  24. package/dist/types-BZv87Ikv.mjs +31 -0
  25. package/dist/types-BZv87Ikv.mjs.map +1 -0
  26. package/dist/types-CMttUZYk.cjs +43 -0
  27. package/dist/types-CMttUZYk.cjs.map +1 -0
  28. package/package.json +54 -0
  29. package/src/Studio.ts +318 -0
  30. package/src/data/DataBrowser.ts +166 -0
  31. package/src/data/__tests__/DataBrowser.integration.spec.ts +418 -0
  32. package/src/data/__tests__/filtering.integration.spec.ts +741 -0
  33. package/src/data/__tests__/introspection.integration.spec.ts +352 -0
  34. package/src/data/filtering.ts +191 -0
  35. package/src/data/index.ts +1 -0
  36. package/src/data/introspection.ts +220 -0
  37. package/src/data/pagination.ts +33 -0
  38. package/src/index.ts +31 -0
  39. package/src/server/__tests__/hono.integration.spec.ts +361 -0
  40. package/src/server/hono.ts +225 -0
  41. package/src/types.ts +278 -0
  42. package/src/ui-assets.ts +40 -0
  43. package/tsdown.config.ts +13 -0
  44. package/ui/index.html +12 -0
  45. package/ui/node_modules/.bin/browserslist +21 -0
  46. package/ui/node_modules/.bin/jiti +21 -0
  47. package/ui/node_modules/.bin/terser +21 -0
  48. package/ui/node_modules/.bin/tsc +21 -0
  49. package/ui/node_modules/.bin/tsserver +21 -0
  50. package/ui/node_modules/.bin/tsx +21 -0
  51. package/ui/node_modules/.bin/vite +21 -0
  52. package/ui/package.json +24 -0
  53. package/ui/src/App.tsx +141 -0
  54. package/ui/src/api.ts +71 -0
  55. package/ui/src/components/RowDetail.tsx +113 -0
  56. package/ui/src/components/TableList.tsx +51 -0
  57. package/ui/src/components/TableView.tsx +219 -0
  58. package/ui/src/main.tsx +10 -0
  59. package/ui/src/styles.css +36 -0
  60. package/ui/src/types.ts +50 -0
  61. package/ui/src/vite-env.d.ts +1 -0
  62. package/ui/tsconfig.json +21 -0
  63. package/ui/tsconfig.tsbuildinfo +1 -0
  64. package/ui/vite.config.ts +12 -0
@@ -0,0 +1,352 @@
1
+ import {
2
+ CamelCasePlugin,
3
+ type Generated,
4
+ Kysely,
5
+ PostgresDialect,
6
+ sql,
7
+ } from 'kysely';
8
+ import pg from 'pg';
9
+ import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest';
10
+ import { TEST_DATABASE_CONFIG } from '../../../../testkit/test/globalSetup';
11
+ import { introspectSchema, introspectTable } from '../introspection';
12
+
13
+ interface TestDatabase {
14
+ studioIntrospectUsers: {
15
+ id: Generated<number>;
16
+ name: string;
17
+ email: string;
18
+ isActive: boolean;
19
+ metadata: Generated<Record<string, unknown> | null>;
20
+ createdAt: Generated<Date>;
21
+ updatedAt: Generated<Date>;
22
+ };
23
+ studioIntrospectPosts: {
24
+ id: Generated<string>;
25
+ userId: number;
26
+ title: string;
27
+ content: string | null;
28
+ viewCount: number;
29
+ publishedAt: Date | null;
30
+ createdAt: Generated<Date>;
31
+ };
32
+ studioIntrospectTags: {
33
+ id: Generated<number>;
34
+ name: string;
35
+ };
36
+ studioIntrospectExcluded: {
37
+ id: Generated<number>;
38
+ value: string;
39
+ };
40
+ }
41
+
42
+ describe('Schema Introspection Integration Tests', () => {
43
+ let db: Kysely<TestDatabase>;
44
+
45
+ beforeAll(async () => {
46
+ db = new Kysely<TestDatabase>({
47
+ dialect: new PostgresDialect({
48
+ pool: new pg.Pool({
49
+ ...TEST_DATABASE_CONFIG,
50
+ database: 'postgres',
51
+ }),
52
+ }),
53
+ plugins: [new CamelCasePlugin()],
54
+ });
55
+
56
+ // Create users table with various column types
57
+ await db.schema
58
+ .createTable('studio_introspect_users')
59
+ .ifNotExists()
60
+ .addColumn('id', 'serial', (col) => col.primaryKey())
61
+ .addColumn('name', 'varchar(255)', (col) => col.notNull())
62
+ .addColumn('email', 'varchar(255)', (col) => col.notNull().unique())
63
+ .addColumn('is_active', 'boolean', (col) => col.notNull().defaultTo(true))
64
+ .addColumn('metadata', 'jsonb')
65
+ .addColumn('created_at', 'timestamptz', (col) =>
66
+ col.defaultTo(sql`now()`).notNull(),
67
+ )
68
+ .addColumn('updated_at', 'timestamptz', (col) =>
69
+ col.defaultTo(sql`now()`).notNull(),
70
+ )
71
+ .execute();
72
+
73
+ // Create posts table with foreign key
74
+ await db.schema
75
+ .createTable('studio_introspect_posts')
76
+ .ifNotExists()
77
+ .addColumn('id', 'uuid', (col) =>
78
+ col.primaryKey().defaultTo(sql`gen_random_uuid()`),
79
+ )
80
+ .addColumn('user_id', 'integer', (col) =>
81
+ col
82
+ .notNull()
83
+ .references('studio_introspect_users.id')
84
+ .onDelete('cascade'),
85
+ )
86
+ .addColumn('title', 'varchar(500)', (col) => col.notNull())
87
+ .addColumn('content', 'text')
88
+ .addColumn('view_count', 'integer', (col) => col.notNull().defaultTo(0))
89
+ .addColumn('published_at', 'timestamp')
90
+ .addColumn('created_at', 'timestamptz', (col) =>
91
+ col.defaultTo(sql`now()`).notNull(),
92
+ )
93
+ .execute();
94
+
95
+ // Create simple tags table
96
+ await db.schema
97
+ .createTable('studio_introspect_tags')
98
+ .ifNotExists()
99
+ .addColumn('id', 'serial', (col) => col.primaryKey())
100
+ .addColumn('name', 'varchar(100)', (col) => col.notNull())
101
+ .execute();
102
+
103
+ // Create excluded table (for exclusion testing)
104
+ await db.schema
105
+ .createTable('studio_introspect_excluded')
106
+ .ifNotExists()
107
+ .addColumn('id', 'serial', (col) => col.primaryKey())
108
+ .addColumn('value', 'varchar(100)', (col) => col.notNull())
109
+ .execute();
110
+ });
111
+
112
+ afterEach(async () => {
113
+ // Clean up data after each test
114
+ await db.deleteFrom('studioIntrospectPosts').execute();
115
+ await db.deleteFrom('studioIntrospectUsers').execute();
116
+ await db.deleteFrom('studioIntrospectTags').execute();
117
+ await db.deleteFrom('studioIntrospectExcluded').execute();
118
+ });
119
+
120
+ afterAll(async () => {
121
+ // Drop tables and close connection
122
+ await db.schema.dropTable('studio_introspect_posts').ifExists().execute();
123
+ await db.schema.dropTable('studio_introspect_users').ifExists().execute();
124
+ await db.schema.dropTable('studio_introspect_tags').ifExists().execute();
125
+ await db.schema
126
+ .dropTable('studio_introspect_excluded')
127
+ .ifExists()
128
+ .execute();
129
+ await db.destroy();
130
+ });
131
+
132
+ describe('introspectSchema', () => {
133
+ it('should discover all tables in the public schema', async () => {
134
+ const schema = await introspectSchema(db, []);
135
+
136
+ // Find our test tables
137
+ const tableNames = schema.tables.map((t) => t.name);
138
+ expect(tableNames).toContain('studio_introspect_users');
139
+ expect(tableNames).toContain('studio_introspect_posts');
140
+ expect(tableNames).toContain('studio_introspect_tags');
141
+ expect(tableNames).toContain('studio_introspect_excluded');
142
+ expect(schema.updatedAt).toBeInstanceOf(Date);
143
+ });
144
+
145
+ it('should exclude specified tables', async () => {
146
+ const schema = await introspectSchema(db, ['studio_introspect_excluded']);
147
+
148
+ const tableNames = schema.tables.map((t) => t.name);
149
+ expect(tableNames).toContain('studio_introspect_users');
150
+ expect(tableNames).toContain('studio_introspect_posts');
151
+ expect(tableNames).not.toContain('studio_introspect_excluded');
152
+ });
153
+
154
+ it('should exclude multiple tables', async () => {
155
+ const schema = await introspectSchema(db, [
156
+ 'studio_introspect_excluded',
157
+ 'studio_introspect_tags',
158
+ ]);
159
+
160
+ const tableNames = schema.tables.map((t) => t.name);
161
+ expect(tableNames).toContain('studio_introspect_users');
162
+ expect(tableNames).toContain('studio_introspect_posts');
163
+ expect(tableNames).not.toContain('studio_introspect_excluded');
164
+ expect(tableNames).not.toContain('studio_introspect_tags');
165
+ });
166
+ });
167
+
168
+ describe('introspectTable', () => {
169
+ it('should return column information for users table', async () => {
170
+ const tableInfo = await introspectTable(db, 'studio_introspect_users');
171
+
172
+ expect(tableInfo.name).toBe('studio_introspect_users');
173
+ expect(tableInfo.schema).toBe('public');
174
+ expect(tableInfo.columns.length).toBe(7);
175
+
176
+ // Find specific columns
177
+ const idCol = tableInfo.columns.find((c) => c.name === 'id');
178
+ const nameCol = tableInfo.columns.find((c) => c.name === 'name');
179
+ const emailCol = tableInfo.columns.find((c) => c.name === 'email');
180
+ const isActiveCol = tableInfo.columns.find((c) => c.name === 'is_active');
181
+ const metadataCol = tableInfo.columns.find((c) => c.name === 'metadata');
182
+ const createdAtCol = tableInfo.columns.find(
183
+ (c) => c.name === 'created_at',
184
+ );
185
+
186
+ // Check id column (primary key, serial)
187
+ expect(idCol).toBeDefined();
188
+ expect(idCol!.type).toBe('number');
189
+ expect(idCol!.rawType).toBe('int4');
190
+ expect(idCol!.isPrimaryKey).toBe(true);
191
+ expect(idCol!.nullable).toBe(false);
192
+
193
+ // Check name column (varchar, not null)
194
+ expect(nameCol).toBeDefined();
195
+ expect(nameCol!.type).toBe('string');
196
+ expect(nameCol!.rawType).toBe('varchar');
197
+ expect(nameCol!.nullable).toBe(false);
198
+
199
+ // Check email column (varchar, unique)
200
+ expect(emailCol).toBeDefined();
201
+ expect(emailCol!.type).toBe('string');
202
+ expect(emailCol!.nullable).toBe(false);
203
+
204
+ // Check is_active column (boolean with default)
205
+ expect(isActiveCol).toBeDefined();
206
+ expect(isActiveCol!.type).toBe('boolean');
207
+ expect(isActiveCol!.rawType).toBe('bool');
208
+ expect(isActiveCol!.nullable).toBe(false);
209
+
210
+ // Check metadata column (jsonb, nullable)
211
+ expect(metadataCol).toBeDefined();
212
+ expect(metadataCol!.type).toBe('json');
213
+ expect(metadataCol!.rawType).toBe('jsonb');
214
+ expect(metadataCol!.nullable).toBe(true);
215
+
216
+ // Check created_at column (timestamptz)
217
+ expect(createdAtCol).toBeDefined();
218
+ expect(createdAtCol!.type).toBe('datetime');
219
+ expect(createdAtCol!.rawType).toBe('timestamptz');
220
+ });
221
+
222
+ it('should detect primary key', async () => {
223
+ const tableInfo = await introspectTable(db, 'studio_introspect_users');
224
+
225
+ expect(tableInfo.primaryKey).toEqual(['id']);
226
+
227
+ const idColumn = tableInfo.columns.find((c) => c.name === 'id');
228
+ expect(idColumn!.isPrimaryKey).toBe(true);
229
+ });
230
+
231
+ it('should detect foreign keys', async () => {
232
+ const tableInfo = await introspectTable(db, 'studio_introspect_posts');
233
+
234
+ const userIdCol = tableInfo.columns.find((c) => c.name === 'user_id');
235
+ expect(userIdCol).toBeDefined();
236
+ expect(userIdCol!.isForeignKey).toBe(true);
237
+ expect(userIdCol!.foreignKeyTable).toBe('studio_introspect_users');
238
+ expect(userIdCol!.foreignKeyColumn).toBe('id');
239
+ });
240
+
241
+ it('should detect uuid primary key', async () => {
242
+ const tableInfo = await introspectTable(db, 'studio_introspect_posts');
243
+
244
+ expect(tableInfo.primaryKey).toEqual(['id']);
245
+
246
+ const idColumn = tableInfo.columns.find((c) => c.name === 'id');
247
+ expect(idColumn).toBeDefined();
248
+ expect(idColumn!.type).toBe('uuid');
249
+ expect(idColumn!.rawType).toBe('uuid');
250
+ expect(idColumn!.isPrimaryKey).toBe(true);
251
+ });
252
+
253
+ it('should map PostgreSQL types correctly', async () => {
254
+ const usersTable = await introspectTable(db, 'studio_introspect_users');
255
+ const postsTable = await introspectTable(db, 'studio_introspect_posts');
256
+
257
+ // Integer types -> number
258
+ const userIdCol = usersTable.columns.find((c) => c.name === 'id');
259
+ expect(userIdCol!.type).toBe('number');
260
+
261
+ // Boolean -> boolean
262
+ const isActiveCol = usersTable.columns.find(
263
+ (c) => c.name === 'is_active',
264
+ );
265
+ expect(isActiveCol!.type).toBe('boolean');
266
+
267
+ // JSONB -> json
268
+ const metadataCol = usersTable.columns.find((c) => c.name === 'metadata');
269
+ expect(metadataCol!.type).toBe('json');
270
+
271
+ // Timestamptz -> datetime
272
+ const createdAtCol = usersTable.columns.find(
273
+ (c) => c.name === 'created_at',
274
+ );
275
+ expect(createdAtCol!.type).toBe('datetime');
276
+
277
+ // UUID -> uuid
278
+ const postIdCol = postsTable.columns.find((c) => c.name === 'id');
279
+ expect(postIdCol!.type).toBe('uuid');
280
+
281
+ // Text -> string
282
+ const contentCol = postsTable.columns.find((c) => c.name === 'content');
283
+ expect(contentCol!.type).toBe('string');
284
+ expect(contentCol!.rawType).toBe('text');
285
+
286
+ // Timestamp (without tz) -> datetime
287
+ const publishedCol = postsTable.columns.find(
288
+ (c) => c.name === 'published_at',
289
+ );
290
+ expect(publishedCol!.type).toBe('datetime');
291
+ });
292
+
293
+ it('should detect nullable columns', async () => {
294
+ const tableInfo = await introspectTable(db, 'studio_introspect_posts');
295
+
296
+ const titleCol = tableInfo.columns.find((c) => c.name === 'title');
297
+ const contentCol = tableInfo.columns.find((c) => c.name === 'content');
298
+ const publishedAtCol = tableInfo.columns.find(
299
+ (c) => c.name === 'published_at',
300
+ );
301
+
302
+ expect(titleCol!.nullable).toBe(false);
303
+ expect(contentCol!.nullable).toBe(true);
304
+ expect(publishedAtCol!.nullable).toBe(true);
305
+ });
306
+
307
+ it('should provide estimated row count when data exists', async () => {
308
+ // Insert some data
309
+ const user = await db
310
+ .insertInto('studioIntrospectUsers')
311
+ .values({
312
+ name: 'Test User',
313
+ email: 'test@example.com',
314
+ })
315
+ .returningAll()
316
+ .executeTakeFirstOrThrow();
317
+
318
+ await db
319
+ .insertInto('studioIntrospectPosts')
320
+ .values([
321
+ { userId: user.id, title: 'Post 1' },
322
+ { userId: user.id, title: 'Post 2' },
323
+ { userId: user.id, title: 'Post 3' },
324
+ ])
325
+ .execute();
326
+
327
+ // ANALYZE to update statistics
328
+ await sql`ANALYZE studio_introspect_posts`.execute(db);
329
+
330
+ const tableInfo = await introspectTable(db, 'studio_introspect_posts');
331
+
332
+ // Row count estimate should exist (might not be exact)
333
+ expect(tableInfo.estimatedRowCount).toBeDefined();
334
+ expect(tableInfo.estimatedRowCount).toBeGreaterThan(0);
335
+ });
336
+
337
+ it('should return all columns in ordinal position order', async () => {
338
+ const tableInfo = await introspectTable(db, 'studio_introspect_users');
339
+
340
+ const columnNames = tableInfo.columns.map((c) => c.name);
341
+ expect(columnNames).toEqual([
342
+ 'id',
343
+ 'name',
344
+ 'email',
345
+ 'is_active',
346
+ 'metadata',
347
+ 'created_at',
348
+ 'updated_at',
349
+ ]);
350
+ });
351
+ });
352
+ });
@@ -0,0 +1,191 @@
1
+ import type { SelectQueryBuilder } from 'kysely';
2
+ import {
3
+ type ColumnInfo,
4
+ Direction,
5
+ type FilterCondition,
6
+ FilterOperator,
7
+ type SortConfig,
8
+ type TableInfo,
9
+ } from '../types';
10
+
11
+ /**
12
+ * Validates that a filter is applicable to the given column.
13
+ */
14
+ export function validateFilter(
15
+ filter: FilterCondition,
16
+ columnInfo: ColumnInfo,
17
+ ): { valid: boolean; error?: string } {
18
+ // Validate operator compatibility with column type
19
+ const typeCompatibility: Record<string, FilterOperator[]> = {
20
+ string: [
21
+ FilterOperator.Eq,
22
+ FilterOperator.Neq,
23
+ FilterOperator.Like,
24
+ FilterOperator.Ilike,
25
+ FilterOperator.In,
26
+ FilterOperator.Nin,
27
+ FilterOperator.IsNull,
28
+ FilterOperator.IsNotNull,
29
+ ],
30
+ number: [
31
+ FilterOperator.Eq,
32
+ FilterOperator.Neq,
33
+ FilterOperator.Gt,
34
+ FilterOperator.Gte,
35
+ FilterOperator.Lt,
36
+ FilterOperator.Lte,
37
+ FilterOperator.In,
38
+ FilterOperator.Nin,
39
+ FilterOperator.IsNull,
40
+ FilterOperator.IsNotNull,
41
+ ],
42
+ boolean: [
43
+ FilterOperator.Eq,
44
+ FilterOperator.Neq,
45
+ FilterOperator.IsNull,
46
+ FilterOperator.IsNotNull,
47
+ ],
48
+ date: [
49
+ FilterOperator.Eq,
50
+ FilterOperator.Neq,
51
+ FilterOperator.Gt,
52
+ FilterOperator.Gte,
53
+ FilterOperator.Lt,
54
+ FilterOperator.Lte,
55
+ FilterOperator.IsNull,
56
+ FilterOperator.IsNotNull,
57
+ ],
58
+ datetime: [
59
+ FilterOperator.Eq,
60
+ FilterOperator.Neq,
61
+ FilterOperator.Gt,
62
+ FilterOperator.Gte,
63
+ FilterOperator.Lt,
64
+ FilterOperator.Lte,
65
+ FilterOperator.IsNull,
66
+ FilterOperator.IsNotNull,
67
+ ],
68
+ uuid: [
69
+ FilterOperator.Eq,
70
+ FilterOperator.Neq,
71
+ FilterOperator.In,
72
+ FilterOperator.Nin,
73
+ FilterOperator.IsNull,
74
+ FilterOperator.IsNotNull,
75
+ ],
76
+ json: [FilterOperator.IsNull, FilterOperator.IsNotNull],
77
+ binary: [FilterOperator.IsNull, FilterOperator.IsNotNull],
78
+ unknown: [
79
+ FilterOperator.Eq,
80
+ FilterOperator.Neq,
81
+ FilterOperator.IsNull,
82
+ FilterOperator.IsNotNull,
83
+ ],
84
+ };
85
+
86
+ const allowedOps =
87
+ typeCompatibility[columnInfo.type] ?? typeCompatibility.unknown;
88
+
89
+ if (!allowedOps.includes(filter.operator)) {
90
+ return {
91
+ valid: false,
92
+ error: `Operator '${filter.operator}' not supported for column type '${columnInfo.type}'`,
93
+ };
94
+ }
95
+
96
+ return { valid: true };
97
+ }
98
+
99
+ /**
100
+ * Applies filters to a Kysely query builder.
101
+ */
102
+ export function applyFilters<DB, TB extends keyof DB, O>(
103
+ query: SelectQueryBuilder<DB, TB, O>,
104
+ filters: FilterCondition[],
105
+ tableInfo: TableInfo,
106
+ ): SelectQueryBuilder<DB, TB, O> {
107
+ let result = query;
108
+
109
+ for (const filter of filters) {
110
+ const column = tableInfo.columns.find((c) => c.name === filter.column);
111
+
112
+ if (!column) {
113
+ throw new Error(
114
+ `Column '${filter.column}' not found in table '${tableInfo.name}'`,
115
+ );
116
+ }
117
+
118
+ const validation = validateFilter(filter, column);
119
+ if (!validation.valid) {
120
+ throw new Error(validation.error);
121
+ }
122
+
123
+ result = applyFilterCondition(result, filter);
124
+ }
125
+
126
+ return result;
127
+ }
128
+
129
+ function applyFilterCondition<DB, TB extends keyof DB, O>(
130
+ query: SelectQueryBuilder<DB, TB, O>,
131
+ filter: FilterCondition,
132
+ ): SelectQueryBuilder<DB, TB, O> {
133
+ const { column, operator, value } = filter;
134
+
135
+ switch (operator) {
136
+ case FilterOperator.Eq:
137
+ return query.where(column as any, '=', value);
138
+ case FilterOperator.Neq:
139
+ return query.where(column as any, '!=', value);
140
+ case FilterOperator.Gt:
141
+ return query.where(column as any, '>', value);
142
+ case FilterOperator.Gte:
143
+ return query.where(column as any, '>=', value);
144
+ case FilterOperator.Lt:
145
+ return query.where(column as any, '<', value);
146
+ case FilterOperator.Lte:
147
+ return query.where(column as any, '<=', value);
148
+ case FilterOperator.Like:
149
+ return query.where(column as any, 'like', value);
150
+ case FilterOperator.Ilike:
151
+ return query.where(column as any, 'ilike', value);
152
+ case FilterOperator.In:
153
+ return query.where(column as any, 'in', value as any[]);
154
+ case FilterOperator.Nin:
155
+ return query.where(column as any, 'not in', value as any[]);
156
+ case FilterOperator.IsNull:
157
+ return query.where(column as any, 'is', null);
158
+ case FilterOperator.IsNotNull:
159
+ return query.where(column as any, 'is not', null);
160
+ default:
161
+ throw new Error(`Unknown filter operator: ${operator}`);
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Applies sorting to a Kysely query builder.
167
+ */
168
+ export function applySorting<DB, TB extends keyof DB, O>(
169
+ query: SelectQueryBuilder<DB, TB, O>,
170
+ sorts: SortConfig[],
171
+ tableInfo: TableInfo,
172
+ ): SelectQueryBuilder<DB, TB, O> {
173
+ let result = query;
174
+
175
+ for (const sort of sorts) {
176
+ const column = tableInfo.columns.find((c) => c.name === sort.column);
177
+
178
+ if (!column) {
179
+ throw new Error(
180
+ `Column '${sort.column}' not found in table '${tableInfo.name}'`,
181
+ );
182
+ }
183
+
184
+ result = result.orderBy(
185
+ sort.column as any,
186
+ sort.direction === Direction.Asc ? 'asc' : 'desc',
187
+ );
188
+ }
189
+
190
+ return result;
191
+ }
@@ -0,0 +1 @@
1
+ export { DataBrowser } from './DataBrowser';