@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,418 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CamelCasePlugin,
|
|
3
|
+
type Generated,
|
|
4
|
+
Kysely,
|
|
5
|
+
PostgresDialect,
|
|
6
|
+
sql,
|
|
7
|
+
} from 'kysely';
|
|
8
|
+
import pg from 'pg';
|
|
9
|
+
import {
|
|
10
|
+
afterAll,
|
|
11
|
+
afterEach,
|
|
12
|
+
beforeAll,
|
|
13
|
+
beforeEach,
|
|
14
|
+
describe,
|
|
15
|
+
expect,
|
|
16
|
+
it,
|
|
17
|
+
} from 'vitest';
|
|
18
|
+
import { TEST_DATABASE_CONFIG } from '../../../../testkit/test/globalSetup';
|
|
19
|
+
import { Direction, FilterOperator } from '../../types';
|
|
20
|
+
import { DataBrowser } from '../DataBrowser';
|
|
21
|
+
|
|
22
|
+
interface TestDatabase {
|
|
23
|
+
studioBrowserProducts: {
|
|
24
|
+
id: Generated<number>;
|
|
25
|
+
name: string;
|
|
26
|
+
price: number;
|
|
27
|
+
category: string;
|
|
28
|
+
inStock: boolean;
|
|
29
|
+
createdAt: Generated<Date>;
|
|
30
|
+
};
|
|
31
|
+
studioBrowserOrders: {
|
|
32
|
+
id: Generated<number>;
|
|
33
|
+
productId: number;
|
|
34
|
+
quantity: number;
|
|
35
|
+
total: number;
|
|
36
|
+
createdAt: Generated<Date>;
|
|
37
|
+
};
|
|
38
|
+
studioBrowserExcluded: {
|
|
39
|
+
id: Generated<number>;
|
|
40
|
+
value: string;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe('DataBrowser Integration Tests', () => {
|
|
45
|
+
let db: Kysely<TestDatabase>;
|
|
46
|
+
let browser: DataBrowser<TestDatabase>;
|
|
47
|
+
|
|
48
|
+
beforeAll(async () => {
|
|
49
|
+
db = new Kysely<TestDatabase>({
|
|
50
|
+
dialect: new PostgresDialect({
|
|
51
|
+
pool: new pg.Pool({
|
|
52
|
+
...TEST_DATABASE_CONFIG,
|
|
53
|
+
database: 'postgres',
|
|
54
|
+
}),
|
|
55
|
+
}),
|
|
56
|
+
plugins: [new CamelCasePlugin()],
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Create products table
|
|
60
|
+
await db.schema
|
|
61
|
+
.createTable('studio_browser_products')
|
|
62
|
+
.ifNotExists()
|
|
63
|
+
.addColumn('id', 'serial', (col) => col.primaryKey())
|
|
64
|
+
.addColumn('name', 'varchar(255)', (col) => col.notNull())
|
|
65
|
+
.addColumn('price', 'numeric(10, 2)', (col) => col.notNull())
|
|
66
|
+
.addColumn('category', 'varchar(100)', (col) => col.notNull())
|
|
67
|
+
.addColumn('in_stock', 'boolean', (col) => col.notNull().defaultTo(true))
|
|
68
|
+
.addColumn('created_at', 'timestamptz', (col) =>
|
|
69
|
+
col.defaultTo(sql`now()`).notNull(),
|
|
70
|
+
)
|
|
71
|
+
.execute();
|
|
72
|
+
|
|
73
|
+
// Create orders table with foreign key
|
|
74
|
+
await db.schema
|
|
75
|
+
.createTable('studio_browser_orders')
|
|
76
|
+
.ifNotExists()
|
|
77
|
+
.addColumn('id', 'serial', (col) => col.primaryKey())
|
|
78
|
+
.addColumn('product_id', 'integer', (col) =>
|
|
79
|
+
col
|
|
80
|
+
.notNull()
|
|
81
|
+
.references('studio_browser_products.id')
|
|
82
|
+
.onDelete('cascade'),
|
|
83
|
+
)
|
|
84
|
+
.addColumn('quantity', 'integer', (col) => col.notNull())
|
|
85
|
+
.addColumn('total', 'numeric(10, 2)', (col) => col.notNull())
|
|
86
|
+
.addColumn('created_at', 'timestamptz', (col) =>
|
|
87
|
+
col.defaultTo(sql`now()`).notNull(),
|
|
88
|
+
)
|
|
89
|
+
.execute();
|
|
90
|
+
|
|
91
|
+
// Create excluded table
|
|
92
|
+
await db.schema
|
|
93
|
+
.createTable('studio_browser_excluded')
|
|
94
|
+
.ifNotExists()
|
|
95
|
+
.addColumn('id', 'serial', (col) => col.primaryKey())
|
|
96
|
+
.addColumn('value', 'varchar(100)', (col) => col.notNull())
|
|
97
|
+
.execute();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
beforeEach(async () => {
|
|
101
|
+
// Create fresh browser instance for each test
|
|
102
|
+
browser = new DataBrowser({
|
|
103
|
+
db,
|
|
104
|
+
cursor: { field: 'id', direction: Direction.Asc },
|
|
105
|
+
tableCursors: {},
|
|
106
|
+
excludeTables: ['studio_browser_excluded'],
|
|
107
|
+
defaultPageSize: 10,
|
|
108
|
+
showBinaryColumns: false,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Insert test products
|
|
112
|
+
await db
|
|
113
|
+
.insertInto('studioBrowserProducts')
|
|
114
|
+
.values([
|
|
115
|
+
{
|
|
116
|
+
name: 'Product A',
|
|
117
|
+
price: 100,
|
|
118
|
+
category: 'electronics',
|
|
119
|
+
inStock: true,
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'Product B',
|
|
123
|
+
price: 200,
|
|
124
|
+
category: 'electronics',
|
|
125
|
+
inStock: true,
|
|
126
|
+
},
|
|
127
|
+
{ name: 'Product C', price: 50, category: 'clothing', inStock: false },
|
|
128
|
+
{ name: 'Product D', price: 150, category: 'clothing', inStock: true },
|
|
129
|
+
{
|
|
130
|
+
name: 'Product E',
|
|
131
|
+
price: 300,
|
|
132
|
+
category: 'electronics',
|
|
133
|
+
inStock: true,
|
|
134
|
+
},
|
|
135
|
+
])
|
|
136
|
+
.execute();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
afterEach(async () => {
|
|
140
|
+
await db.deleteFrom('studioBrowserOrders').execute();
|
|
141
|
+
await db.deleteFrom('studioBrowserProducts').execute();
|
|
142
|
+
await db.deleteFrom('studioBrowserExcluded').execute();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
afterAll(async () => {
|
|
146
|
+
await db.schema.dropTable('studio_browser_orders').ifExists().execute();
|
|
147
|
+
await db.schema.dropTable('studio_browser_products').ifExists().execute();
|
|
148
|
+
await db.schema.dropTable('studio_browser_excluded').ifExists().execute();
|
|
149
|
+
await db.destroy();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe('getSchema', () => {
|
|
153
|
+
it('should return schema with tables', async () => {
|
|
154
|
+
const schema = await browser.getSchema();
|
|
155
|
+
|
|
156
|
+
const tableNames = schema.tables.map((t) => t.name);
|
|
157
|
+
expect(tableNames).toContain('studio_browser_products');
|
|
158
|
+
expect(tableNames).toContain('studio_browser_orders');
|
|
159
|
+
expect(tableNames).not.toContain('studio_browser_excluded');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should cache schema results', async () => {
|
|
163
|
+
const schema1 = await browser.getSchema();
|
|
164
|
+
const schema2 = await browser.getSchema();
|
|
165
|
+
|
|
166
|
+
// Same reference means cached
|
|
167
|
+
expect(schema1).toBe(schema2);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should refresh cache when forceRefresh is true', async () => {
|
|
171
|
+
const schema1 = await browser.getSchema();
|
|
172
|
+
const schema2 = await browser.getSchema(true);
|
|
173
|
+
|
|
174
|
+
// Different reference means new fetch
|
|
175
|
+
expect(schema1).not.toBe(schema2);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('getTableInfo', () => {
|
|
180
|
+
it('should return table info for existing table', async () => {
|
|
181
|
+
const tableInfo = await browser.getTableInfo('studio_browser_products');
|
|
182
|
+
|
|
183
|
+
expect(tableInfo).not.toBeNull();
|
|
184
|
+
expect(tableInfo!.name).toBe('studio_browser_products');
|
|
185
|
+
expect(tableInfo!.columns.length).toBeGreaterThan(0);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should return null for non-existent table', async () => {
|
|
189
|
+
const tableInfo = await browser.getTableInfo('non_existent_table');
|
|
190
|
+
|
|
191
|
+
expect(tableInfo).toBeNull();
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe('query', () => {
|
|
196
|
+
it('should return paginated results', async () => {
|
|
197
|
+
const result = await browser.query({
|
|
198
|
+
table: 'studio_browser_products',
|
|
199
|
+
pageSize: 3,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
expect(result.rows).toHaveLength(3);
|
|
203
|
+
expect(result.hasMore).toBe(true);
|
|
204
|
+
expect(result.nextCursor).toBeDefined();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should throw error for non-existent table', async () => {
|
|
208
|
+
await expect(
|
|
209
|
+
browser.query({ table: 'non_existent_table' }),
|
|
210
|
+
).rejects.toThrow("Table 'non_existent_table' not found");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should respect pageSize limit', async () => {
|
|
214
|
+
const result = await browser.query({
|
|
215
|
+
table: 'studio_browser_products',
|
|
216
|
+
pageSize: 2,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
expect(result.rows).toHaveLength(2);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should cap pageSize at 100', async () => {
|
|
223
|
+
// Insert more products
|
|
224
|
+
const products = [];
|
|
225
|
+
for (let i = 0; i < 110; i++) {
|
|
226
|
+
products.push({
|
|
227
|
+
name: `Product ${i}`,
|
|
228
|
+
price: i * 10,
|
|
229
|
+
category: 'test',
|
|
230
|
+
inStock: true,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
await db.insertInto('studioBrowserProducts').values(products).execute();
|
|
234
|
+
|
|
235
|
+
const result = await browser.query({
|
|
236
|
+
table: 'studio_browser_products',
|
|
237
|
+
pageSize: 150,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
expect(result.rows.length).toBeLessThanOrEqual(100);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should paginate using cursor', async () => {
|
|
244
|
+
const firstPage = await browser.query({
|
|
245
|
+
table: 'studio_browser_products',
|
|
246
|
+
pageSize: 2,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
expect(firstPage.rows).toHaveLength(2);
|
|
250
|
+
expect(firstPage.nextCursor).toBeDefined();
|
|
251
|
+
|
|
252
|
+
const secondPage = await browser.query({
|
|
253
|
+
table: 'studio_browser_products',
|
|
254
|
+
pageSize: 2,
|
|
255
|
+
cursor: firstPage.nextCursor,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
expect(secondPage.rows).toHaveLength(2);
|
|
259
|
+
|
|
260
|
+
// Items should be different
|
|
261
|
+
const firstIds = firstPage.rows.map((r: any) => r.id);
|
|
262
|
+
const secondIds = secondPage.rows.map((r: any) => r.id);
|
|
263
|
+
expect(firstIds.every((id: number) => !secondIds.includes(id))).toBe(
|
|
264
|
+
true,
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('should apply filters', async () => {
|
|
269
|
+
const result = await browser.query({
|
|
270
|
+
table: 'studio_browser_products',
|
|
271
|
+
filters: [
|
|
272
|
+
{
|
|
273
|
+
column: 'category',
|
|
274
|
+
operator: FilterOperator.Eq,
|
|
275
|
+
value: 'electronics',
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
expect(result.rows).toHaveLength(3);
|
|
281
|
+
result.rows.forEach((row: any) => {
|
|
282
|
+
expect(row.category).toBe('electronics');
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('should apply multiple filters', async () => {
|
|
287
|
+
const result = await browser.query({
|
|
288
|
+
table: 'studio_browser_products',
|
|
289
|
+
filters: [
|
|
290
|
+
{
|
|
291
|
+
column: 'category',
|
|
292
|
+
operator: FilterOperator.Eq,
|
|
293
|
+
value: 'electronics',
|
|
294
|
+
},
|
|
295
|
+
{ column: 'price', operator: FilterOperator.Gt, value: 150 },
|
|
296
|
+
],
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
expect(result.rows).toHaveLength(2);
|
|
300
|
+
result.rows.forEach((row: any) => {
|
|
301
|
+
expect(row.category).toBe('electronics');
|
|
302
|
+
expect(Number(row.price)).toBeGreaterThan(150);
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('should apply sorting', async () => {
|
|
307
|
+
const result = await browser.query({
|
|
308
|
+
table: 'studio_browser_products',
|
|
309
|
+
sort: [{ column: 'price', direction: Direction.Desc }],
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
const prices = result.rows.map((r: any) => Number(r.price));
|
|
313
|
+
for (let i = 1; i < prices.length; i++) {
|
|
314
|
+
expect(prices[i]).toBeLessThanOrEqual(prices[i - 1]);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('should handle empty results', async () => {
|
|
319
|
+
const result = await browser.query({
|
|
320
|
+
table: 'studio_browser_products',
|
|
321
|
+
filters: [
|
|
322
|
+
{
|
|
323
|
+
column: 'category',
|
|
324
|
+
operator: FilterOperator.Eq,
|
|
325
|
+
value: 'nonexistent',
|
|
326
|
+
},
|
|
327
|
+
],
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
expect(result.rows).toHaveLength(0);
|
|
331
|
+
expect(result.hasMore).toBe(false);
|
|
332
|
+
expect(result.nextCursor).toBeNull();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('should set prevCursor when cursor is provided', async () => {
|
|
336
|
+
const firstPage = await browser.query({
|
|
337
|
+
table: 'studio_browser_products',
|
|
338
|
+
pageSize: 2,
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const secondPage = await browser.query({
|
|
342
|
+
table: 'studio_browser_products',
|
|
343
|
+
pageSize: 2,
|
|
344
|
+
cursor: firstPage.nextCursor,
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
expect(secondPage.prevCursor).toBeDefined();
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('should not set prevCursor on first page', async () => {
|
|
351
|
+
const result = await browser.query({
|
|
352
|
+
table: 'studio_browser_products',
|
|
353
|
+
pageSize: 2,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
expect(result.prevCursor).toBeNull();
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
describe('getCursorConfig', () => {
|
|
361
|
+
it('should return default cursor config', () => {
|
|
362
|
+
const config = browser.getCursorConfig('studio_browser_products');
|
|
363
|
+
|
|
364
|
+
expect(config.field).toBe('id');
|
|
365
|
+
expect(config.direction).toBe(Direction.Asc);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('should return table-specific cursor config', () => {
|
|
369
|
+
const customBrowser = new DataBrowser({
|
|
370
|
+
db,
|
|
371
|
+
cursor: { field: 'id', direction: Direction.Asc },
|
|
372
|
+
tableCursors: {
|
|
373
|
+
studio_browser_products: {
|
|
374
|
+
field: 'created_at',
|
|
375
|
+
direction: Direction.Desc,
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
excludeTables: [],
|
|
379
|
+
defaultPageSize: 10,
|
|
380
|
+
showBinaryColumns: false,
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
const config = customBrowser.getCursorConfig('studio_browser_products');
|
|
384
|
+
|
|
385
|
+
expect(config.field).toBe('created_at');
|
|
386
|
+
expect(config.direction).toBe(Direction.Desc);
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
describe('database getter', () => {
|
|
391
|
+
it('should return the underlying database instance', () => {
|
|
392
|
+
expect(browser.database).toBe(db);
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
describe('cursor direction in query', () => {
|
|
397
|
+
it('should use descending cursor direction correctly', async () => {
|
|
398
|
+
const descBrowser = new DataBrowser({
|
|
399
|
+
db,
|
|
400
|
+
cursor: { field: 'id', direction: Direction.Desc },
|
|
401
|
+
tableCursors: {},
|
|
402
|
+
excludeTables: [],
|
|
403
|
+
defaultPageSize: 10,
|
|
404
|
+
showBinaryColumns: false,
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
const result = await descBrowser.query({
|
|
408
|
+
table: 'studio_browser_products',
|
|
409
|
+
pageSize: 3,
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
const ids = result.rows.map((r: any) => r.id);
|
|
413
|
+
for (let i = 1; i < ids.length; i++) {
|
|
414
|
+
expect(ids[i]).toBeLessThan(ids[i - 1]);
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
});
|