@suiteportal/studio 0.1.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/index.cjs ADDED
@@ -0,0 +1,340 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ createApp: () => createApp,
34
+ startStudio: () => startStudio
35
+ });
36
+ module.exports = __toCommonJS(index_exports);
37
+ var import_node_server = require("@hono/node-server");
38
+ var import_node_path = require("path");
39
+ var import_node_url = require("url");
40
+ var import_node_fs = require("fs");
41
+ var import_node_child_process = require("child_process");
42
+ var import_client_runtime = require("@suiteportal/client-runtime");
43
+ var import_client_runtime2 = require("@suiteportal/client-runtime");
44
+
45
+ // src/server/app.ts
46
+ var import_hono4 = require("hono");
47
+ var import_serve_static = require("@hono/node-server/serve-static");
48
+ var import_cors = require("hono/cors");
49
+
50
+ // src/server/routes/schema.ts
51
+ var import_hono = require("hono");
52
+ function createSchemaRoutes(schema) {
53
+ const app = new import_hono.Hono();
54
+ app.get("/", (c) => {
55
+ const records = Object.entries(schema.records).map(([id, def]) => ({
56
+ id,
57
+ label: def.label,
58
+ isCustom: def.isCustom,
59
+ fieldCount: Object.keys(def.fields).length,
60
+ relationCount: Object.keys(def.relations).length,
61
+ sublistCount: Object.keys(def.sublists).length
62
+ }));
63
+ return c.json({
64
+ generatedAt: schema.generatedAt,
65
+ accountId: schema.accountId,
66
+ recordCount: records.length,
67
+ records
68
+ });
69
+ });
70
+ app.get("/:type", (c) => {
71
+ const type = c.req.param("type");
72
+ const record = schema.records[type];
73
+ if (!record) {
74
+ return c.json({ error: `Record type "${type}" not found` }, 404);
75
+ }
76
+ return c.json(record);
77
+ });
78
+ return app;
79
+ }
80
+
81
+ // src/server/routes/records.ts
82
+ var import_hono2 = require("hono");
83
+ function pickDefaultColumns(fields) {
84
+ const select = {};
85
+ if (fields["id"]) select["id"] = true;
86
+ for (const [id, f] of Object.entries(fields)) {
87
+ if (select[id]) continue;
88
+ if (id.startsWith("cust")) continue;
89
+ if (f.type === "unknown" || f.nativeType === "reference") continue;
90
+ if (f.queryable === false) continue;
91
+ select[id] = true;
92
+ if (Object.keys(select).length >= 12) break;
93
+ }
94
+ return select;
95
+ }
96
+ function createRecordRoutes(client, schema) {
97
+ const app = new import_hono2.Hono();
98
+ app.get("/:type", async (c) => {
99
+ const type = c.req.param("type");
100
+ const recordDef = schema.records[type];
101
+ if (!recordDef) {
102
+ return c.json({ error: `Record type "${type}" not found` }, 404);
103
+ }
104
+ const delegate = client.$delegate(type);
105
+ const whereParam = c.req.query("where");
106
+ const selectParam = c.req.query("select");
107
+ const orderByParam = c.req.query("orderBy");
108
+ const includeParam = c.req.query("include");
109
+ const take = c.req.query("take");
110
+ const skip = c.req.query("skip");
111
+ const args = {};
112
+ if (whereParam) {
113
+ try {
114
+ args["where"] = JSON.parse(whereParam);
115
+ } catch {
116
+ return c.json({ error: "Invalid where JSON" }, 400);
117
+ }
118
+ }
119
+ if (selectParam) {
120
+ try {
121
+ args["select"] = JSON.parse(selectParam);
122
+ } catch {
123
+ return c.json({ error: "Invalid select JSON" }, 400);
124
+ }
125
+ }
126
+ if (orderByParam) {
127
+ try {
128
+ args["orderBy"] = JSON.parse(orderByParam);
129
+ } catch {
130
+ return c.json({ error: "Invalid orderBy JSON" }, 400);
131
+ }
132
+ }
133
+ if (includeParam) {
134
+ try {
135
+ args["include"] = JSON.parse(includeParam);
136
+ } catch {
137
+ return c.json({ error: "Invalid include JSON" }, 400);
138
+ }
139
+ }
140
+ if (take) args["take"] = parseInt(take, 10);
141
+ if (skip) args["skip"] = parseInt(skip, 10);
142
+ if (!args["take"]) args["take"] = 50;
143
+ if (!args["select"]) {
144
+ args["select"] = pickDefaultColumns(recordDef.fields);
145
+ }
146
+ try {
147
+ const rows = await delegate.findMany(args);
148
+ return c.json({ data: rows, count: rows.length });
149
+ } catch (err) {
150
+ const status = err.status;
151
+ if (status === 400) {
152
+ try {
153
+ const fallbackArgs = { ...args, select: { id: true } };
154
+ const rows = await delegate.findMany(fallbackArgs);
155
+ return c.json({ data: rows, count: rows.length });
156
+ } catch {
157
+ throw err;
158
+ }
159
+ }
160
+ throw err;
161
+ }
162
+ });
163
+ app.get("/:type/count", async (c) => {
164
+ const type = c.req.param("type");
165
+ if (!schema.records[type]) {
166
+ return c.json({ error: `Record type "${type}" not found` }, 404);
167
+ }
168
+ const delegate = client.$delegate(type);
169
+ const whereParam = c.req.query("where");
170
+ const args = {};
171
+ if (whereParam) {
172
+ try {
173
+ args["where"] = JSON.parse(whereParam);
174
+ } catch {
175
+ return c.json({ error: "Invalid where JSON" }, 400);
176
+ }
177
+ }
178
+ const count = await delegate.count(args);
179
+ return c.json({ count });
180
+ });
181
+ app.post("/:type", async (c) => {
182
+ const type = c.req.param("type");
183
+ if (!schema.records[type]) {
184
+ return c.json({ error: `Record type "${type}" not found` }, 404);
185
+ }
186
+ const delegate = client.$delegate(type);
187
+ const body = await c.req.json();
188
+ const result = await delegate.create({ data: body });
189
+ return c.json(result, 201);
190
+ });
191
+ app.patch("/:type/:id", async (c) => {
192
+ const type = c.req.param("type");
193
+ const id = c.req.param("id");
194
+ if (!schema.records[type]) {
195
+ return c.json({ error: `Record type "${type}" not found` }, 404);
196
+ }
197
+ const delegate = client.$delegate(type);
198
+ const body = await c.req.json();
199
+ const result = await delegate.update({
200
+ where: { id: { equals: id } },
201
+ data: body
202
+ });
203
+ return c.json(result);
204
+ });
205
+ app.delete("/:type/:id", async (c) => {
206
+ const type = c.req.param("type");
207
+ const id = c.req.param("id");
208
+ if (!schema.records[type]) {
209
+ return c.json({ error: `Record type "${type}" not found` }, 404);
210
+ }
211
+ const delegate = client.$delegate(type);
212
+ await delegate.delete({ where: { id: { equals: id } } });
213
+ return c.json({ success: true });
214
+ });
215
+ return app;
216
+ }
217
+
218
+ // src/server/routes/query.ts
219
+ var import_hono3 = require("hono");
220
+ function createQueryRoutes(client) {
221
+ const app = new import_hono3.Hono();
222
+ app.post("/", async (c) => {
223
+ const body = await c.req.json();
224
+ if (!body.sql || typeof body.sql !== "string") {
225
+ return c.json({ error: 'Missing "sql" field in request body' }, 400);
226
+ }
227
+ const rows = await client.$queryRaw(body.sql);
228
+ return c.json({ data: rows, count: rows.length });
229
+ });
230
+ return app;
231
+ }
232
+
233
+ // src/server/middleware/error-handler.ts
234
+ var import_connector = require("@suiteportal/connector");
235
+ var onError = (err, c) => {
236
+ if (err instanceof import_connector.AuthError) {
237
+ return c.json({ error: "Authentication failed", message: err.message }, 401);
238
+ }
239
+ if (err instanceof import_connector.RateLimitError) {
240
+ return c.json({ error: "Rate limit exceeded", message: err.message }, 429);
241
+ }
242
+ if (err instanceof import_connector.TimeoutError) {
243
+ return c.json({ error: "Request timeout", message: err.message }, 504);
244
+ }
245
+ if (err instanceof import_connector.NetSuiteError) {
246
+ const status = err.status && err.status >= 400 && err.status < 600 ? err.status : 502;
247
+ const details = err.details;
248
+ const errorDetails = details?.["o:errorDetails"];
249
+ return c.json({
250
+ error: "NetSuite error",
251
+ message: err.message,
252
+ status,
253
+ details: errorDetails ?? void 0
254
+ }, status);
255
+ }
256
+ if (err instanceof Error) {
257
+ console.error(`[studio] ${err.message}`);
258
+ return c.json({ error: "Internal error", message: err.message }, 500);
259
+ }
260
+ return c.json({ error: "Unknown error" }, 500);
261
+ };
262
+
263
+ // src/server/app.ts
264
+ function createApp(options) {
265
+ const { client, schema, frontendDir } = options;
266
+ const app = new import_hono4.Hono();
267
+ app.onError(onError);
268
+ app.use("*", (0, import_cors.cors)());
269
+ app.route("/api/schema", createSchemaRoutes(schema));
270
+ app.route("/api/records", createRecordRoutes(client, schema));
271
+ app.route("/api/query", createQueryRoutes(client));
272
+ app.get("/api/health", (c) => c.json({ status: "ok" }));
273
+ if (frontendDir) {
274
+ app.use("/*", (0, import_serve_static.serveStatic)({ root: frontendDir }));
275
+ app.get("*", (0, import_serve_static.serveStatic)({ root: frontendDir, path: "/index.html" }));
276
+ }
277
+ return app;
278
+ }
279
+
280
+ // src/index.ts
281
+ var import_meta = {};
282
+ async function startStudio(config, options = {}) {
283
+ const port = options.port ?? 4480;
284
+ const shouldOpen = options.open ?? true;
285
+ const schemaPath = options.schemaPath ?? ".suiteportal/schema.json";
286
+ const schema = await (0, import_client_runtime2.loadSchema)(schemaPath);
287
+ const client = await (0, import_client_runtime.createClient)(config, { schemaPath });
288
+ const currentDir = typeof __dirname !== "undefined" ? __dirname : (0, import_node_path.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
289
+ const studioRoot = (0, import_node_path.resolve)(currentDir, "..");
290
+ const frontendDir = (0, import_node_path.resolve)(studioRoot, "dist-frontend");
291
+ if (!(0, import_node_fs.existsSync)(frontendDir)) {
292
+ const viteConfig = (0, import_node_path.resolve)(studioRoot, "vite.config.ts");
293
+ if ((0, import_node_fs.existsSync)(viteConfig)) {
294
+ console.log("\n Building Studio frontend...");
295
+ try {
296
+ (0, import_node_child_process.execSync)("npx vite build", { cwd: studioRoot, stdio: "pipe" });
297
+ console.log(" Frontend built successfully.\n");
298
+ } catch (e) {
299
+ console.log(" Could not build frontend \u2014 starting in API-only mode.\n");
300
+ }
301
+ }
302
+ }
303
+ const hasFrontend = (0, import_node_fs.existsSync)(frontendDir);
304
+ const app = createApp({
305
+ client,
306
+ schema,
307
+ frontendDir: hasFrontend ? frontendDir : void 0
308
+ });
309
+ const server = (0, import_node_server.serve)({
310
+ fetch: app.fetch,
311
+ port
312
+ });
313
+ const url = `http://localhost:${port}`;
314
+ console.log(`
315
+ SuitePortal Studio running at ${url}
316
+ `);
317
+ if (!hasFrontend) {
318
+ console.log(" (Frontend unavailable \u2014 API-only mode)\n");
319
+ }
320
+ if (shouldOpen && hasFrontend) {
321
+ try {
322
+ const openModule = await import("open");
323
+ await openModule.default(url);
324
+ } catch {
325
+ }
326
+ }
327
+ return {
328
+ port,
329
+ close: () => {
330
+ server.close();
331
+ client.$disconnect();
332
+ }
333
+ };
334
+ }
335
+ // Annotate the CommonJS export names for ESM import in node:
336
+ 0 && (module.exports = {
337
+ createApp,
338
+ startStudio
339
+ });
340
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/server/app.ts","../src/server/routes/schema.ts","../src/server/routes/records.ts","../src/server/routes/query.ts","../src/server/middleware/error-handler.ts"],"sourcesContent":["import { serve } from '@hono/node-server';\nimport { resolve, dirname } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { existsSync } from 'node:fs';\nimport { execSync } from 'node:child_process';\nimport type { NetSuiteConfig } from '@suiteportal/connector';\nimport { createClient } from '@suiteportal/client-runtime';\nimport { loadSchema } from '@suiteportal/client-runtime';\nimport { createApp } from './server/app.js';\n\nexport interface StudioOptions {\n /** Port to listen on. Default: 4480 */\n port?: number;\n /** Whether to open the browser. Default: true */\n open?: boolean;\n /** Path to schema.json. Default: '.suiteportal/schema.json' */\n schemaPath?: string;\n}\n\nexport interface StudioServer {\n port: number;\n close: () => void;\n}\n\n/**\n * Start SuitePortal Studio — a local web UI for browsing NetSuite data.\n */\nexport async function startStudio(\n config: NetSuiteConfig,\n options: StudioOptions = {},\n): Promise<StudioServer> {\n const port = options.port ?? 4480;\n const shouldOpen = options.open ?? true;\n const schemaPath = options.schemaPath ?? '.suiteportal/schema.json';\n\n // Load schema\n const schema = await loadSchema(schemaPath);\n\n // Create ORM client\n const client = await createClient(config, { schemaPath });\n\n // Resolve frontend dist directory (works in both ESM and CJS)\n const currentDir = typeof __dirname !== 'undefined'\n ? __dirname\n : dirname(fileURLToPath(import.meta.url));\n const studioRoot = resolve(currentDir, '..');\n const frontendDir = resolve(studioRoot, 'dist-frontend');\n\n // Auto-build frontend if not present\n if (!existsSync(frontendDir)) {\n const viteConfig = resolve(studioRoot, 'vite.config.ts');\n if (existsSync(viteConfig)) {\n console.log('\\n Building Studio frontend...');\n try {\n execSync('npx vite build', { cwd: studioRoot, stdio: 'pipe' });\n console.log(' Frontend built successfully.\\n');\n } catch (e) {\n console.log(' Could not build frontend — starting in API-only mode.\\n');\n }\n }\n }\n\n const hasFrontend = existsSync(frontendDir);\n\n // Create Hono app\n const app = createApp({\n client,\n schema,\n frontendDir: hasFrontend ? frontendDir : undefined,\n });\n\n // Start server\n const server = serve({\n fetch: app.fetch,\n port,\n });\n\n const url = `http://localhost:${port}`;\n console.log(`\\n SuitePortal Studio running at ${url}\\n`);\n\n if (!hasFrontend) {\n console.log(' (Frontend unavailable — API-only mode)\\n');\n }\n\n // Open browser\n if (shouldOpen && hasFrontend) {\n try {\n const openModule = await import('open');\n await openModule.default(url);\n } catch {\n // open is optional — silently ignore if unavailable\n }\n }\n\n return {\n port,\n close: () => { server.close(); client.$disconnect(); },\n };\n}\n\n// Re-export app creation for testing\nexport { createApp } from './server/app.js';\nexport type { CreateAppOptions } from './server/app.js';\n","import { Hono } from 'hono';\nimport { serveStatic } from '@hono/node-server/serve-static';\nimport { cors } from 'hono/cors';\nimport type { SuitePortalClient } from '@suiteportal/client-runtime';\nimport type { NormalizedSchema } from '@suiteportal/introspector';\nimport { createSchemaRoutes } from './routes/schema.js';\nimport { createRecordRoutes } from './routes/records.js';\nimport { createQueryRoutes } from './routes/query.js';\nimport { onError } from './middleware/error-handler.js';\n\nexport interface CreateAppOptions {\n client: SuitePortalClient;\n schema: NormalizedSchema;\n frontendDir?: string;\n}\n\nexport function createApp(options: CreateAppOptions): Hono {\n const { client, schema, frontendDir } = options;\n const app = new Hono();\n\n // Global error handler — prevents stack traces from leaking to console\n app.onError(onError);\n\n // Middleware\n app.use('*', cors());\n\n // API routes\n app.route('/api/schema', createSchemaRoutes(schema));\n app.route('/api/records', createRecordRoutes(client, schema));\n app.route('/api/query', createQueryRoutes(client));\n\n // Health check\n app.get('/api/health', (c) => c.json({ status: 'ok' }));\n\n // Serve static frontend\n if (frontendDir) {\n app.use('/*', serveStatic({ root: frontendDir }));\n // SPA fallback: serve index.html for all non-API routes\n app.get('*', serveStatic({ root: frontendDir, path: '/index.html' }));\n }\n\n return app;\n}\n","import { Hono } from 'hono';\nimport type { NormalizedSchema } from '@suiteportal/introspector';\n\nexport function createSchemaRoutes(schema: NormalizedSchema): Hono {\n const app = new Hono();\n\n // List all record types (summary)\n app.get('/', (c) => {\n const records = Object.entries(schema.records).map(([id, def]) => ({\n id,\n label: def.label,\n isCustom: def.isCustom,\n fieldCount: Object.keys(def.fields).length,\n relationCount: Object.keys(def.relations).length,\n sublistCount: Object.keys(def.sublists).length,\n }));\n\n return c.json({\n generatedAt: schema.generatedAt,\n accountId: schema.accountId,\n recordCount: records.length,\n records,\n });\n });\n\n // Get full record definition\n app.get('/:type', (c) => {\n const type = c.req.param('type');\n const record = schema.records[type];\n\n if (!record) {\n return c.json({ error: `Record type \"${type}\" not found` }, 404);\n }\n\n return c.json(record);\n });\n\n return app;\n}\n","import { Hono } from 'hono';\nimport type { SuitePortalClient } from '@suiteportal/client-runtime';\nimport type { NormalizedSchema, FieldDefinition } from '@suiteportal/introspector';\n\n/**\n * Pick safe default columns for a record type.\n * Uses the `queryable` flag from the schema (set during introspection).\n * Falls back to heuristics for schemas generated before queryability probing.\n */\nfunction pickDefaultColumns(fields: Record<string, FieldDefinition>): Record<string, boolean> {\n const select: Record<string, boolean> = {};\n\n // Always include id if present\n if (fields['id']) select['id'] = true;\n\n // Use queryable fields: non-custom, known type, not reference, not marked non-queryable\n for (const [id, f] of Object.entries(fields)) {\n if (select[id]) continue;\n if (id.startsWith('cust')) continue;\n if (f.type === 'unknown' || f.nativeType === 'reference') continue;\n if (f.queryable === false) continue;\n select[id] = true;\n if (Object.keys(select).length >= 12) break;\n }\n\n return select;\n}\n\nexport function createRecordRoutes(client: SuitePortalClient, schema: NormalizedSchema): Hono {\n const app = new Hono();\n\n // GET /api/records/:type — findMany\n app.get('/:type', async (c) => {\n const type = c.req.param('type');\n const recordDef = schema.records[type];\n\n if (!recordDef) {\n return c.json({ error: `Record type \"${type}\" not found` }, 404);\n }\n\n const delegate = client.$delegate(type);\n\n const whereParam = c.req.query('where');\n const selectParam = c.req.query('select');\n const orderByParam = c.req.query('orderBy');\n const includeParam = c.req.query('include');\n const take = c.req.query('take');\n const skip = c.req.query('skip');\n\n const args: Record<string, unknown> = {};\n\n if (whereParam) {\n try { args['where'] = JSON.parse(whereParam); } catch { return c.json({ error: 'Invalid where JSON' }, 400); }\n }\n if (selectParam) {\n try { args['select'] = JSON.parse(selectParam); } catch { return c.json({ error: 'Invalid select JSON' }, 400); }\n }\n if (orderByParam) {\n try { args['orderBy'] = JSON.parse(orderByParam); } catch { return c.json({ error: 'Invalid orderBy JSON' }, 400); }\n }\n if (includeParam) {\n try { args['include'] = JSON.parse(includeParam); } catch { return c.json({ error: 'Invalid include JSON' }, 400); }\n }\n if (take) args['take'] = parseInt(take, 10);\n if (skip) args['skip'] = parseInt(skip, 10);\n\n // Default take to 50\n if (!args['take']) args['take'] = 50;\n\n // If no explicit select, use safe default columns to avoid SuiteQL 400 errors\n if (!args['select']) {\n args['select'] = pickDefaultColumns(recordDef.fields);\n }\n\n // Try the query; if it fails with 400 (bad column), retry with minimal columns\n try {\n const rows = await delegate.findMany(args);\n return c.json({ data: rows, count: rows.length });\n } catch (err: unknown) {\n const status = (err as { status?: number }).status;\n if (status === 400) {\n // Retry with just id\n try {\n const fallbackArgs = { ...args, select: { id: true } };\n const rows = await delegate.findMany(fallbackArgs);\n return c.json({ data: rows, count: rows.length });\n } catch {\n throw err; // rethrow original error\n }\n }\n throw err;\n }\n });\n\n // GET /api/records/:type/count\n app.get('/:type/count', async (c) => {\n const type = c.req.param('type');\n\n if (!schema.records[type]) {\n return c.json({ error: `Record type \"${type}\" not found` }, 404);\n }\n\n const delegate = client.$delegate(type);\n\n const whereParam = c.req.query('where');\n const args: Record<string, unknown> = {};\n if (whereParam) {\n try { args['where'] = JSON.parse(whereParam); } catch { return c.json({ error: 'Invalid where JSON' }, 400); }\n }\n\n const count = await delegate.count(args);\n return c.json({ count });\n });\n\n // POST /api/records/:type — create\n app.post('/:type', async (c) => {\n const type = c.req.param('type');\n\n if (!schema.records[type]) {\n return c.json({ error: `Record type \"${type}\" not found` }, 404);\n }\n\n const delegate = client.$delegate(type);\n const body = await c.req.json<Record<string, unknown>>();\n const result = await delegate.create({ data: body });\n return c.json(result, 201);\n });\n\n // PATCH /api/records/:type/:id — update\n app.patch('/:type/:id', async (c) => {\n const type = c.req.param('type');\n const id = c.req.param('id');\n\n if (!schema.records[type]) {\n return c.json({ error: `Record type \"${type}\" not found` }, 404);\n }\n\n const delegate = client.$delegate(type);\n const body = await c.req.json<Record<string, unknown>>();\n const result = await delegate.update({\n where: { id: { equals: id } },\n data: body,\n });\n return c.json(result);\n });\n\n // DELETE /api/records/:type/:id\n app.delete('/:type/:id', async (c) => {\n const type = c.req.param('type');\n const id = c.req.param('id');\n\n if (!schema.records[type]) {\n return c.json({ error: `Record type \"${type}\" not found` }, 404);\n }\n\n const delegate = client.$delegate(type);\n await delegate.delete({ where: { id: { equals: id } } });\n return c.json({ success: true });\n });\n\n return app;\n}\n","import { Hono } from 'hono';\nimport type { SuitePortalClient } from '@suiteportal/client-runtime';\n\nexport function createQueryRoutes(client: SuitePortalClient): Hono {\n const app = new Hono();\n\n // POST /api/query — raw SuiteQL\n app.post('/', async (c) => {\n const body = await c.req.json<{ sql?: string }>();\n\n if (!body.sql || typeof body.sql !== 'string') {\n return c.json({ error: 'Missing \"sql\" field in request body' }, 400);\n }\n\n const rows = await client.$queryRaw(body.sql);\n return c.json({ data: rows, count: rows.length });\n });\n\n return app;\n}\n","import type { ErrorHandler } from 'hono';\nimport { NetSuiteError, AuthError, RateLimitError, TimeoutError } from '@suiteportal/connector';\n\n/**\n * Hono onError handler — maps NetSuiteError subclasses to HTTP responses.\n * This prevents unhandled errors from printing stack traces to the console.\n */\nexport const onError: ErrorHandler = (err, c) => {\n if (err instanceof AuthError) {\n return c.json({ error: 'Authentication failed', message: err.message }, 401);\n }\n if (err instanceof RateLimitError) {\n return c.json({ error: 'Rate limit exceeded', message: err.message }, 429);\n }\n if (err instanceof TimeoutError) {\n return c.json({ error: 'Request timeout', message: err.message }, 504);\n }\n if (err instanceof NetSuiteError) {\n const status = err.status && err.status >= 400 && err.status < 600 ? err.status : 502;\n const details = err.details as Record<string, unknown> | undefined;\n const errorDetails = details?.['o:errorDetails'];\n return c.json({\n error: 'NetSuite error',\n message: err.message,\n status,\n details: errorDetails ?? undefined,\n }, status as 400);\n }\n if (err instanceof Error) {\n console.error(`[studio] ${err.message}`);\n return c.json({ error: 'Internal error', message: err.message }, 500);\n }\n return c.json({ error: 'Unknown error' }, 500);\n};\n"],"mappings":"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","names":["import_client_runtime","import_hono","import_hono","import_hono"]}
@@ -0,0 +1,30 @@
1
+ import { NetSuiteConfig } from '@suiteportal/connector';
2
+ import { Hono } from 'hono';
3
+ import { SuitePortalClient } from '@suiteportal/client-runtime';
4
+ import { NormalizedSchema } from '@suiteportal/introspector';
5
+
6
+ interface CreateAppOptions {
7
+ client: SuitePortalClient;
8
+ schema: NormalizedSchema;
9
+ frontendDir?: string;
10
+ }
11
+ declare function createApp(options: CreateAppOptions): Hono;
12
+
13
+ interface StudioOptions {
14
+ /** Port to listen on. Default: 4480 */
15
+ port?: number;
16
+ /** Whether to open the browser. Default: true */
17
+ open?: boolean;
18
+ /** Path to schema.json. Default: '.suiteportal/schema.json' */
19
+ schemaPath?: string;
20
+ }
21
+ interface StudioServer {
22
+ port: number;
23
+ close: () => void;
24
+ }
25
+ /**
26
+ * Start SuitePortal Studio — a local web UI for browsing NetSuite data.
27
+ */
28
+ declare function startStudio(config: NetSuiteConfig, options?: StudioOptions): Promise<StudioServer>;
29
+
30
+ export { type CreateAppOptions, type StudioOptions, type StudioServer, createApp, startStudio };
@@ -0,0 +1,30 @@
1
+ import { NetSuiteConfig } from '@suiteportal/connector';
2
+ import { Hono } from 'hono';
3
+ import { SuitePortalClient } from '@suiteportal/client-runtime';
4
+ import { NormalizedSchema } from '@suiteportal/introspector';
5
+
6
+ interface CreateAppOptions {
7
+ client: SuitePortalClient;
8
+ schema: NormalizedSchema;
9
+ frontendDir?: string;
10
+ }
11
+ declare function createApp(options: CreateAppOptions): Hono;
12
+
13
+ interface StudioOptions {
14
+ /** Port to listen on. Default: 4480 */
15
+ port?: number;
16
+ /** Whether to open the browser. Default: true */
17
+ open?: boolean;
18
+ /** Path to schema.json. Default: '.suiteportal/schema.json' */
19
+ schemaPath?: string;
20
+ }
21
+ interface StudioServer {
22
+ port: number;
23
+ close: () => void;
24
+ }
25
+ /**
26
+ * Start SuitePortal Studio — a local web UI for browsing NetSuite data.
27
+ */
28
+ declare function startStudio(config: NetSuiteConfig, options?: StudioOptions): Promise<StudioServer>;
29
+
30
+ export { type CreateAppOptions, type StudioOptions, type StudioServer, createApp, startStudio };