@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.
- package/dist/DataBrowser-DQ3-ZxdV.mjs +427 -0
- package/dist/DataBrowser-DQ3-ZxdV.mjs.map +1 -0
- package/dist/DataBrowser-SOcqmZb2.d.mts +267 -0
- package/dist/DataBrowser-c-Gs6PZB.cjs +432 -0
- package/dist/DataBrowser-c-Gs6PZB.cjs.map +1 -0
- package/dist/DataBrowser-hGwiTffZ.d.cts +267 -0
- package/dist/chunk-CUT6urMc.cjs +30 -0
- package/dist/data/index.cjs +4 -0
- package/dist/data/index.d.cts +2 -0
- package/dist/data/index.d.mts +2 -0
- package/dist/data/index.mjs +4 -0
- package/dist/index.cjs +239 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +132 -0
- package/dist/index.d.mts +132 -0
- package/dist/index.mjs +230 -0
- package/dist/index.mjs.map +1 -0
- package/dist/server/hono.cjs +192 -0
- package/dist/server/hono.cjs.map +1 -0
- package/dist/server/hono.d.cts +19 -0
- package/dist/server/hono.d.mts +19 -0
- package/dist/server/hono.mjs +191 -0
- package/dist/server/hono.mjs.map +1 -0
- package/dist/types-BZv87Ikv.mjs +31 -0
- package/dist/types-BZv87Ikv.mjs.map +1 -0
- package/dist/types-CMttUZYk.cjs +43 -0
- package/dist/types-CMttUZYk.cjs.map +1 -0
- package/package.json +54 -0
- package/src/Studio.ts +318 -0
- package/src/data/DataBrowser.ts +166 -0
- package/src/data/__tests__/DataBrowser.integration.spec.ts +418 -0
- package/src/data/__tests__/filtering.integration.spec.ts +741 -0
- package/src/data/__tests__/introspection.integration.spec.ts +352 -0
- package/src/data/filtering.ts +191 -0
- package/src/data/index.ts +1 -0
- package/src/data/introspection.ts +220 -0
- package/src/data/pagination.ts +33 -0
- package/src/index.ts +31 -0
- package/src/server/__tests__/hono.integration.spec.ts +361 -0
- package/src/server/hono.ts +225 -0
- package/src/types.ts +278 -0
- package/src/ui-assets.ts +40 -0
- package/tsdown.config.ts +13 -0
- package/ui/index.html +12 -0
- package/ui/node_modules/.bin/browserslist +21 -0
- package/ui/node_modules/.bin/jiti +21 -0
- package/ui/node_modules/.bin/terser +21 -0
- package/ui/node_modules/.bin/tsc +21 -0
- package/ui/node_modules/.bin/tsserver +21 -0
- package/ui/node_modules/.bin/tsx +21 -0
- package/ui/node_modules/.bin/vite +21 -0
- package/ui/package.json +24 -0
- package/ui/src/App.tsx +141 -0
- package/ui/src/api.ts +71 -0
- package/ui/src/components/RowDetail.tsx +113 -0
- package/ui/src/components/TableList.tsx +51 -0
- package/ui/src/components/TableView.tsx +219 -0
- package/ui/src/main.tsx +10 -0
- package/ui/src/styles.css +36 -0
- package/ui/src/types.ts +50 -0
- package/ui/src/vite-env.d.ts +1 -0
- package/ui/tsconfig.json +21 -0
- package/ui/tsconfig.tsbuildinfo +1 -0
- 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';
|