@geekmidas/studio 0.2.0 → 1.0.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/CHANGELOG.md +13 -0
- package/dist/{DataBrowser-hGwiTffZ.d.cts → DataBrowser-B-jz8KBR.d.mts} +5 -2
- package/dist/DataBrowser-B-jz8KBR.d.mts.map +1 -0
- package/dist/{DataBrowser-SOcqmZb2.d.mts → DataBrowser-BTe9HWJy.d.cts} +5 -2
- package/dist/DataBrowser-BTe9HWJy.d.cts.map +1 -0
- package/dist/{DataBrowser-c-Gs6PZB.cjs → DataBrowser-D8c_pBf4.cjs} +4 -4
- package/dist/DataBrowser-D8c_pBf4.cjs.map +1 -0
- package/dist/{DataBrowser-DQ3-ZxdV.mjs → DataBrowser-kgcI9ApJ.mjs} +4 -4
- package/dist/DataBrowser-kgcI9ApJ.mjs.map +1 -0
- package/dist/Studio-CYzz3wD2.d.cts +152 -0
- package/dist/Studio-CYzz3wD2.d.cts.map +1 -0
- package/dist/Studio-D5yGscb8.d.mts +152 -0
- package/dist/Studio-D5yGscb8.d.mts.map +1 -0
- package/dist/data/index.cjs +1 -1
- package/dist/data/index.d.cts +1 -1
- package/dist/data/index.d.mts +1 -1
- package/dist/data/index.mjs +1 -1
- package/dist/index.cjs +33 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -131
- package/dist/index.d.mts +4 -131
- package/dist/index.mjs +33 -3
- package/dist/index.mjs.map +1 -1
- package/dist/server/hono.cjs +168 -21
- package/dist/server/hono.cjs.map +1 -1
- package/dist/server/hono.d.cts +13 -2
- package/dist/server/hono.d.cts.map +1 -0
- package/dist/server/hono.d.mts +13 -2
- package/dist/server/hono.d.mts.map +1 -0
- package/dist/server/hono.mjs +168 -21
- package/dist/server/hono.mjs.map +1 -1
- package/dist/types-BZv87Ikv.mjs.map +1 -1
- package/dist/types-CMttUZYk.cjs.map +1 -1
- package/package.json +5 -5
- package/src/Studio.ts +341 -292
- package/src/__tests__/Studio.spec.ts +447 -0
- package/src/data/DataBrowser.ts +147 -143
- package/src/data/__tests__/DataBrowser.integration.spec.ts +404 -404
- package/src/data/__tests__/filtering.integration.spec.ts +726 -726
- package/src/data/__tests__/introspection.integration.spec.ts +340 -340
- package/src/data/__tests__/pagination.spec.ts +123 -0
- package/src/data/filtering.ts +154 -154
- package/src/data/introspection.ts +141 -141
- package/src/data/pagination.ts +15 -15
- package/src/index.ts +22 -24
- package/src/server/__tests__/hono.integration.spec.ts +605 -347
- package/src/server/hono.ts +392 -190
- package/src/types.ts +138 -138
- package/src/ui-assets.ts +10 -13
- package/tsconfig.json +9 -0
- package/tsdown.config.ts +9 -9
- package/ui/CHANGELOG.md +12 -0
- package/ui/package.json +28 -22
- package/ui/src/App.tsx +95 -235
- package/ui/src/api.ts +184 -42
- package/ui/src/components/FilterPanel.tsx +198 -198
- package/ui/src/components/NavRail.tsx +183 -0
- package/ui/src/components/RowDetail.tsx +106 -106
- package/ui/src/components/StudioHeader.tsx +109 -0
- package/ui/src/components/TableList.tsx +49 -49
- package/ui/src/components/TableView.tsx +530 -485
- package/ui/src/main.tsx +3 -3
- package/ui/src/pages/DashboardPage.tsx +500 -0
- package/ui/src/pages/DatabasePage.tsx +226 -0
- package/ui/src/pages/EndpointDetailsPage.tsx +288 -0
- package/ui/src/pages/ExceptionsPage.tsx +268 -0
- package/ui/src/pages/LogsPage.tsx +228 -0
- package/ui/src/pages/MonitoringPage.tsx +46 -0
- package/ui/src/pages/PerformancePage.tsx +307 -0
- package/ui/src/pages/RequestsPage.tsx +379 -0
- package/ui/src/providers/StudioProvider.tsx +194 -0
- package/ui/src/styles.css +53 -142
- package/ui/src/types.ts +154 -30
- package/ui/tsconfig.tsbuildinfo +1 -1
- package/ui/vite.config.ts +6 -6
- package/dist/DataBrowser-DQ3-ZxdV.mjs.map +0 -1
- package/dist/DataBrowser-c-Gs6PZB.cjs.map +0 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
CamelCasePlugin,
|
|
3
|
+
type Generated,
|
|
4
|
+
Kysely,
|
|
5
|
+
PostgresDialect,
|
|
6
|
+
sql,
|
|
7
7
|
} from 'kysely';
|
|
8
8
|
import pg from 'pg';
|
|
9
9
|
import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest';
|
|
@@ -11,342 +11,342 @@ import { TEST_DATABASE_CONFIG } from '../../../../testkit/test/globalSetup';
|
|
|
11
11
|
import { introspectSchema, introspectTable } from '../introspection';
|
|
12
12
|
|
|
13
13
|
interface TestDatabase {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
40
|
}
|
|
41
41
|
|
|
42
42
|
describe('Schema Introspection Integration Tests', () => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
352
|
});
|