@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,225 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import type { Context } from 'hono';
|
|
3
|
+
import type { DataBrowser } from '../data/DataBrowser';
|
|
4
|
+
import {
|
|
5
|
+
Direction,
|
|
6
|
+
type FilterCondition,
|
|
7
|
+
FilterOperator,
|
|
8
|
+
type SortConfig,
|
|
9
|
+
} from '../types';
|
|
10
|
+
import { getAsset, getIndexHtml } from '../ui-assets';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Interface for the Studio instance used by the Hono adapter.
|
|
14
|
+
* Only requires the data browser - monitoring routes are separate.
|
|
15
|
+
*/
|
|
16
|
+
export interface StudioLike {
|
|
17
|
+
data: DataBrowser<unknown>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Parse filter conditions from query parameters.
|
|
22
|
+
* Format: filter[column][operator]=value
|
|
23
|
+
* Example: filter[name][eq]=John&filter[age][gt]=18
|
|
24
|
+
*/
|
|
25
|
+
function parseFilters(c: Context): FilterCondition[] {
|
|
26
|
+
const filters: FilterCondition[] = [];
|
|
27
|
+
const url = new URL(c.req.url);
|
|
28
|
+
|
|
29
|
+
url.searchParams.forEach((value, key) => {
|
|
30
|
+
const match = key.match(/^filter\[(\w+)\]\[(\w+)\]$/);
|
|
31
|
+
if (match) {
|
|
32
|
+
const [, column, operator] = match;
|
|
33
|
+
const op = operator as FilterOperator;
|
|
34
|
+
|
|
35
|
+
// Validate operator
|
|
36
|
+
if (!Object.values(FilterOperator).includes(op)) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Handle special cases
|
|
41
|
+
if (op === FilterOperator.In || op === FilterOperator.Nin) {
|
|
42
|
+
filters.push({ column, operator: op, value: value.split(',') });
|
|
43
|
+
} else if (
|
|
44
|
+
op === FilterOperator.IsNull ||
|
|
45
|
+
op === FilterOperator.IsNotNull
|
|
46
|
+
) {
|
|
47
|
+
filters.push({ column, operator: op });
|
|
48
|
+
} else {
|
|
49
|
+
// Try to parse as number or boolean
|
|
50
|
+
let parsedValue: unknown = value;
|
|
51
|
+
if (value === 'true') parsedValue = true;
|
|
52
|
+
else if (value === 'false') parsedValue = false;
|
|
53
|
+
else if (!isNaN(Number(value)) && value !== '')
|
|
54
|
+
parsedValue = Number(value);
|
|
55
|
+
|
|
56
|
+
filters.push({ column, operator: op, value: parsedValue });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return filters;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Parse sort configuration from query parameters.
|
|
66
|
+
* Format: sort=column:direction,column:direction
|
|
67
|
+
* Example: sort=name:asc,created_at:desc
|
|
68
|
+
*/
|
|
69
|
+
function parseSort(c: Context): SortConfig[] {
|
|
70
|
+
const sortParam = c.req.query('sort');
|
|
71
|
+
if (!sortParam) return [];
|
|
72
|
+
|
|
73
|
+
return sortParam.split(',').map((part) => {
|
|
74
|
+
const [column, dir] = part.split(':');
|
|
75
|
+
return {
|
|
76
|
+
column,
|
|
77
|
+
direction: dir === 'desc' ? Direction.Desc : Direction.Asc,
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Create Hono app with Studio API routes and dashboard UI.
|
|
84
|
+
*/
|
|
85
|
+
export function createStudioApp(studio: StudioLike): Hono {
|
|
86
|
+
const app = new Hono();
|
|
87
|
+
|
|
88
|
+
// ============================================
|
|
89
|
+
// Schema API
|
|
90
|
+
// ============================================
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* GET /api/schema
|
|
94
|
+
* Get the complete database schema
|
|
95
|
+
*/
|
|
96
|
+
app.get('/api/schema', async (c) => {
|
|
97
|
+
const forceRefresh = c.req.query('refresh') === 'true';
|
|
98
|
+
const schema = await studio.data.getSchema(forceRefresh);
|
|
99
|
+
return c.json(schema);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* GET /api/tables
|
|
104
|
+
* List all tables with basic info
|
|
105
|
+
*/
|
|
106
|
+
app.get('/api/tables', async (c) => {
|
|
107
|
+
const schema = await studio.data.getSchema();
|
|
108
|
+
const tables = schema.tables.map((t) => ({
|
|
109
|
+
name: t.name,
|
|
110
|
+
schema: t.schema,
|
|
111
|
+
columnCount: t.columns.length,
|
|
112
|
+
primaryKey: t.primaryKey,
|
|
113
|
+
estimatedRowCount: t.estimatedRowCount,
|
|
114
|
+
}));
|
|
115
|
+
return c.json({ tables });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* GET /api/tables/:name
|
|
120
|
+
* Get detailed information about a specific table
|
|
121
|
+
*/
|
|
122
|
+
app.get('/api/tables/:name', async (c) => {
|
|
123
|
+
const tableName = c.req.param('name');
|
|
124
|
+
const tableInfo = await studio.data.getTableInfo(tableName);
|
|
125
|
+
|
|
126
|
+
if (!tableInfo) {
|
|
127
|
+
return c.json({ error: `Table '${tableName}' not found` }, 404);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return c.json(tableInfo);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* GET /api/tables/:name/rows
|
|
135
|
+
* Query table data with pagination, filtering, and sorting
|
|
136
|
+
*
|
|
137
|
+
* Query parameters:
|
|
138
|
+
* - pageSize: number (default: 50, max: 100)
|
|
139
|
+
* - cursor: string (pagination cursor)
|
|
140
|
+
* - filter[column][operator]=value (e.g., filter[status][eq]=active)
|
|
141
|
+
* - sort=column:direction (e.g., sort=created_at:desc)
|
|
142
|
+
*/
|
|
143
|
+
app.get('/api/tables/:name/rows', async (c) => {
|
|
144
|
+
const tableName = c.req.param('name');
|
|
145
|
+
const pageSize = Math.min(
|
|
146
|
+
parseInt(c.req.query('pageSize') || '50', 10),
|
|
147
|
+
100,
|
|
148
|
+
);
|
|
149
|
+
const cursor = c.req.query('cursor') || undefined;
|
|
150
|
+
const filters = parseFilters(c);
|
|
151
|
+
const sort = parseSort(c);
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const result = await studio.data.query({
|
|
155
|
+
table: tableName,
|
|
156
|
+
pageSize,
|
|
157
|
+
cursor,
|
|
158
|
+
filters: filters.length > 0 ? filters : undefined,
|
|
159
|
+
sort: sort.length > 0 ? sort : undefined,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
return c.json(result);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
if (error instanceof Error && error.message.includes('not found')) {
|
|
165
|
+
return c.json({ error: error.message }, 404);
|
|
166
|
+
}
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// ============================================
|
|
172
|
+
// Static Assets & Dashboard UI
|
|
173
|
+
// ============================================
|
|
174
|
+
|
|
175
|
+
// Static assets
|
|
176
|
+
app.get('/assets/:filename', (c) => {
|
|
177
|
+
const filename = c.req.param('filename');
|
|
178
|
+
const assetPath = `assets/${filename}`;
|
|
179
|
+
const asset = getAsset(assetPath);
|
|
180
|
+
if (asset) {
|
|
181
|
+
return c.body(asset.content, 200, {
|
|
182
|
+
'Content-Type': asset.contentType,
|
|
183
|
+
'Cache-Control': 'public, max-age=31536000, immutable',
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return c.notFound();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Dashboard UI - serve React app
|
|
190
|
+
app.get('/', (c) => {
|
|
191
|
+
const html = getIndexHtml();
|
|
192
|
+
if (!html) {
|
|
193
|
+
return c.json({
|
|
194
|
+
message: 'Studio API is running',
|
|
195
|
+
note: 'UI not available. Run "pnpm build:ui" first.',
|
|
196
|
+
endpoints: {
|
|
197
|
+
schema: '/api/schema',
|
|
198
|
+
tables: '/api/tables',
|
|
199
|
+
tableInfo: '/api/tables/:name',
|
|
200
|
+
tableRows: '/api/tables/:name/rows',
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
return c.html(html);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// SPA fallback - serve index.html for client-side routing
|
|
208
|
+
app.get('/*', (c) => {
|
|
209
|
+
// Skip API routes
|
|
210
|
+
if (c.req.path.startsWith('/api/')) {
|
|
211
|
+
return c.notFound();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const html = getIndexHtml();
|
|
215
|
+
if (!html) {
|
|
216
|
+
return c.notFound();
|
|
217
|
+
}
|
|
218
|
+
return c.html(html);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
return app;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Re-export types
|
|
225
|
+
export type { StudioLike, DataBrowser };
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import type { TelescopeStorage } from '@geekmidas/telescope';
|
|
2
|
+
import type { Kysely } from 'kysely';
|
|
3
|
+
|
|
4
|
+
// ============================================
|
|
5
|
+
// Enums
|
|
6
|
+
// ============================================
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Sort direction for cursor-based pagination and sorting.
|
|
10
|
+
*/
|
|
11
|
+
export enum Direction {
|
|
12
|
+
Asc = 'asc',
|
|
13
|
+
Desc = 'desc',
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Filter operators for querying data.
|
|
18
|
+
*/
|
|
19
|
+
export enum FilterOperator {
|
|
20
|
+
Eq = 'eq',
|
|
21
|
+
Neq = 'neq',
|
|
22
|
+
Gt = 'gt',
|
|
23
|
+
Gte = 'gte',
|
|
24
|
+
Lt = 'lt',
|
|
25
|
+
Lte = 'lte',
|
|
26
|
+
Like = 'like',
|
|
27
|
+
Ilike = 'ilike',
|
|
28
|
+
In = 'in',
|
|
29
|
+
Nin = 'nin',
|
|
30
|
+
IsNull = 'is_null',
|
|
31
|
+
IsNotNull = 'is_not_null',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ============================================
|
|
35
|
+
// Cursor Configuration
|
|
36
|
+
// ============================================
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Configuration for cursor-based pagination.
|
|
40
|
+
*/
|
|
41
|
+
export interface CursorConfig {
|
|
42
|
+
/** The field to use for cursor-based pagination (e.g., 'id', 'created_at') */
|
|
43
|
+
field: string;
|
|
44
|
+
/** Sort direction for the cursor field */
|
|
45
|
+
direction: Direction;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Per-table cursor configuration overrides.
|
|
50
|
+
*/
|
|
51
|
+
export interface TableCursorConfig {
|
|
52
|
+
[tableName: string]: CursorConfig;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ============================================
|
|
56
|
+
// Monitoring Configuration
|
|
57
|
+
// ============================================
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Configuration for the monitoring feature (Telescope).
|
|
61
|
+
*/
|
|
62
|
+
export interface MonitoringOptions {
|
|
63
|
+
/** Storage backend for monitoring data */
|
|
64
|
+
storage: TelescopeStorage;
|
|
65
|
+
/** Patterns to ignore when recording requests (supports wildcards) */
|
|
66
|
+
ignorePatterns?: string[];
|
|
67
|
+
/** Whether to record request/response bodies (default: true) */
|
|
68
|
+
recordBody?: boolean;
|
|
69
|
+
/** Maximum body size to record in bytes (default: 64KB) */
|
|
70
|
+
maxBodySize?: number;
|
|
71
|
+
/** Hours after which to prune old entries */
|
|
72
|
+
pruneAfterHours?: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ============================================
|
|
76
|
+
// Data Browser Configuration
|
|
77
|
+
// ============================================
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Configuration for the data browser feature.
|
|
81
|
+
*/
|
|
82
|
+
export interface DataBrowserOptions<DB = unknown> {
|
|
83
|
+
/** Kysely database instance */
|
|
84
|
+
db: Kysely<DB>;
|
|
85
|
+
/** Default cursor configuration for all tables */
|
|
86
|
+
cursor: CursorConfig;
|
|
87
|
+
/** Per-table cursor overrides */
|
|
88
|
+
tableCursors?: TableCursorConfig;
|
|
89
|
+
/** Tables to exclude from browsing */
|
|
90
|
+
excludeTables?: string[];
|
|
91
|
+
/** Maximum rows per page (default: 50, max: 100) */
|
|
92
|
+
defaultPageSize?: number;
|
|
93
|
+
/** Whether to allow viewing of binary/blob columns (default: false) */
|
|
94
|
+
showBinaryColumns?: boolean;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ============================================
|
|
98
|
+
// Studio Configuration
|
|
99
|
+
// ============================================
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Configuration for the Studio dashboard.
|
|
103
|
+
*/
|
|
104
|
+
export interface StudioOptions<DB = unknown> {
|
|
105
|
+
/** Monitoring configuration */
|
|
106
|
+
monitoring: MonitoringOptions;
|
|
107
|
+
/** Data browser configuration */
|
|
108
|
+
data: DataBrowserOptions<DB>;
|
|
109
|
+
/** Dashboard path (default: '/__studio') */
|
|
110
|
+
path?: string;
|
|
111
|
+
/** Whether Studio is enabled (default: true) */
|
|
112
|
+
enabled?: boolean;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Normalized Studio options with all defaults applied.
|
|
117
|
+
*/
|
|
118
|
+
export interface NormalizedStudioOptions<DB = unknown> {
|
|
119
|
+
monitoring: Required<MonitoringOptions>;
|
|
120
|
+
data: Required<DataBrowserOptions<DB>>;
|
|
121
|
+
path: string;
|
|
122
|
+
enabled: boolean;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ============================================
|
|
126
|
+
// Table Introspection Types
|
|
127
|
+
// ============================================
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Generic column type classification.
|
|
131
|
+
*/
|
|
132
|
+
export type ColumnType =
|
|
133
|
+
| 'string'
|
|
134
|
+
| 'number'
|
|
135
|
+
| 'boolean'
|
|
136
|
+
| 'date'
|
|
137
|
+
| 'datetime'
|
|
138
|
+
| 'json'
|
|
139
|
+
| 'binary'
|
|
140
|
+
| 'uuid'
|
|
141
|
+
| 'unknown';
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Information about a database column.
|
|
145
|
+
*/
|
|
146
|
+
export interface ColumnInfo {
|
|
147
|
+
/** Column name */
|
|
148
|
+
name: string;
|
|
149
|
+
/** Generic column type */
|
|
150
|
+
type: ColumnType;
|
|
151
|
+
/** Raw database type (e.g., 'varchar', 'int4') */
|
|
152
|
+
rawType: string;
|
|
153
|
+
/** Whether the column allows NULL values */
|
|
154
|
+
nullable: boolean;
|
|
155
|
+
/** Whether this column is part of the primary key */
|
|
156
|
+
isPrimaryKey: boolean;
|
|
157
|
+
/** Whether this column is a foreign key */
|
|
158
|
+
isForeignKey: boolean;
|
|
159
|
+
/** Referenced table if foreign key */
|
|
160
|
+
foreignKeyTable?: string;
|
|
161
|
+
/** Referenced column if foreign key */
|
|
162
|
+
foreignKeyColumn?: string;
|
|
163
|
+
/** Default value expression */
|
|
164
|
+
defaultValue?: string;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Information about a database table.
|
|
169
|
+
*/
|
|
170
|
+
export interface TableInfo {
|
|
171
|
+
/** Table name */
|
|
172
|
+
name: string;
|
|
173
|
+
/** Schema name (e.g., 'public') */
|
|
174
|
+
schema: string;
|
|
175
|
+
/** List of columns */
|
|
176
|
+
columns: ColumnInfo[];
|
|
177
|
+
/** Primary key column names */
|
|
178
|
+
primaryKey: string[];
|
|
179
|
+
/** Estimated row count (if available) */
|
|
180
|
+
estimatedRowCount?: number;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Complete schema information.
|
|
185
|
+
*/
|
|
186
|
+
export interface SchemaInfo {
|
|
187
|
+
/** List of tables */
|
|
188
|
+
tables: TableInfo[];
|
|
189
|
+
/** When the schema was last introspected */
|
|
190
|
+
updatedAt: Date;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ============================================
|
|
194
|
+
// Query Types
|
|
195
|
+
// ============================================
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* A single filter condition.
|
|
199
|
+
*/
|
|
200
|
+
export interface FilterCondition {
|
|
201
|
+
/** Column to filter on */
|
|
202
|
+
column: string;
|
|
203
|
+
/** Filter operator */
|
|
204
|
+
operator: FilterOperator;
|
|
205
|
+
/** Value to compare against (optional for IsNull/IsNotNull operators) */
|
|
206
|
+
value?: unknown;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Sort configuration for a column.
|
|
211
|
+
*/
|
|
212
|
+
export interface SortConfig {
|
|
213
|
+
/** Column to sort by */
|
|
214
|
+
column: string;
|
|
215
|
+
/** Sort direction */
|
|
216
|
+
direction: Direction;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Options for querying table data.
|
|
221
|
+
*/
|
|
222
|
+
export interface QueryOptions {
|
|
223
|
+
/** Table to query */
|
|
224
|
+
table: string;
|
|
225
|
+
/** Filter conditions */
|
|
226
|
+
filters?: FilterCondition[];
|
|
227
|
+
/** Sort configuration */
|
|
228
|
+
sort?: SortConfig[];
|
|
229
|
+
/** Cursor for pagination */
|
|
230
|
+
cursor?: string | null;
|
|
231
|
+
/** Number of rows per page */
|
|
232
|
+
pageSize?: number;
|
|
233
|
+
/** Pagination direction */
|
|
234
|
+
direction?: 'next' | 'prev';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Result of a paginated query.
|
|
239
|
+
*/
|
|
240
|
+
export interface QueryResult<T = Record<string, unknown>> {
|
|
241
|
+
/** Retrieved rows */
|
|
242
|
+
rows: T[];
|
|
243
|
+
/** Whether there are more rows */
|
|
244
|
+
hasMore: boolean;
|
|
245
|
+
/** Cursor for next page */
|
|
246
|
+
nextCursor: string | null;
|
|
247
|
+
/** Cursor for previous page */
|
|
248
|
+
prevCursor: string | null;
|
|
249
|
+
/** Estimated total row count */
|
|
250
|
+
totalEstimate?: number;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ============================================
|
|
254
|
+
// WebSocket Events
|
|
255
|
+
// ============================================
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Types of events broadcast via WebSocket.
|
|
259
|
+
*/
|
|
260
|
+
export type StudioEventType =
|
|
261
|
+
| 'request'
|
|
262
|
+
| 'exception'
|
|
263
|
+
| 'log'
|
|
264
|
+
| 'stats'
|
|
265
|
+
| 'connected'
|
|
266
|
+
| 'schema_updated';
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* A WebSocket event payload.
|
|
270
|
+
*/
|
|
271
|
+
export interface StudioEvent<T = unknown> {
|
|
272
|
+
/** Event type */
|
|
273
|
+
type: StudioEventType;
|
|
274
|
+
/** Event payload */
|
|
275
|
+
payload: T;
|
|
276
|
+
/** Event timestamp */
|
|
277
|
+
timestamp: number;
|
|
278
|
+
}
|