@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,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
+ });