@suiteportal/client-runtime 0.1.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/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # @suiteportal/client-runtime
2
+
3
+ Prisma-like query runtime for NetSuite. Provides `findMany`, `findFirst`, `count`, and raw SuiteQL execution with a fluent, type-safe API.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @suiteportal/client-runtime
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import { createClient } from './.suiteportal/client';
15
+
16
+ const ns = await createClient(config);
17
+
18
+ const customers = await ns.customer.findMany({
19
+ where: { balance: { gt: 1000 }, isInactive: { equals: false } },
20
+ select: { companyName: true, email: true, balance: true },
21
+ orderBy: { balance: 'desc' },
22
+ take: 50,
23
+ });
24
+
25
+ const order = await ns.salesorder.findFirst({
26
+ where: { entity: { equals: 42 } },
27
+ });
28
+
29
+ const total = await ns.customer.count({
30
+ where: { isInactive: { equals: false } },
31
+ });
32
+ ```
33
+
34
+ ## Features
35
+
36
+ - Prisma-like `findMany`, `findFirst`, `count` API
37
+ - `where` filters: `equals`, `not`, `gt`, `gte`, `lt`, `lte`, `in`, `contains`, `startsWith`
38
+ - `select` field projection
39
+ - `orderBy` sorting with `asc`/`desc`
40
+ - `take`/`skip` pagination
41
+ - `$queryRaw` escape hatch for raw SuiteQL
42
+ - ES Proxy-based dynamic record access
43
+
44
+ ## Documentation
45
+
46
+ Full docs at [suiteportal.dev](https://suiteportal.dev)
47
+
48
+ ## License
49
+
50
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,377 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ ModelDelegate: () => ModelDelegate,
24
+ SuitePortalClient: () => SuitePortalClient,
25
+ buildCountQuery: () => buildCountQuery,
26
+ buildOrderBy: () => buildOrderBy,
27
+ buildSelect: () => buildSelect,
28
+ buildSuiteQL: () => buildSuiteQL,
29
+ buildWhere: () => buildWhere,
30
+ clearSchemaCache: () => clearSchemaCache,
31
+ createClient: () => createClient,
32
+ escapeIdentifier: () => escapeIdentifier,
33
+ escapeList: () => escapeList,
34
+ escapeValue: () => escapeValue,
35
+ loadSchema: () => loadSchema
36
+ });
37
+ module.exports = __toCommonJS(index_exports);
38
+
39
+ // src/client.ts
40
+ var import_connector2 = require("@suiteportal/connector");
41
+ var import_connector3 = require("@suiteportal/connector");
42
+
43
+ // src/schema-loader.ts
44
+ var import_promises = require("fs/promises");
45
+ var cachedSchema = null;
46
+ var cachedPath = null;
47
+ async function loadSchema(schemaPath) {
48
+ if (cachedSchema && cachedPath === schemaPath) {
49
+ return cachedSchema;
50
+ }
51
+ const raw = await (0, import_promises.readFile)(schemaPath, "utf-8");
52
+ const schema = JSON.parse(raw);
53
+ if (!schema.records || typeof schema.records !== "object") {
54
+ throw new Error(`Invalid schema: missing "records" in ${schemaPath}`);
55
+ }
56
+ cachedSchema = schema;
57
+ cachedPath = schemaPath;
58
+ return schema;
59
+ }
60
+ function clearSchemaCache() {
61
+ cachedSchema = null;
62
+ cachedPath = null;
63
+ }
64
+
65
+ // src/model-delegate.ts
66
+ var import_connector = require("@suiteportal/connector");
67
+
68
+ // src/query/sql-escape.ts
69
+ function escapeValue(value) {
70
+ if (value === null || value === void 0) {
71
+ return "NULL";
72
+ }
73
+ if (typeof value === "boolean") {
74
+ return value ? "'T'" : "'F'";
75
+ }
76
+ if (typeof value === "number") {
77
+ if (!Number.isFinite(value)) {
78
+ throw new Error(`Cannot escape non-finite number: ${value}`);
79
+ }
80
+ return String(value);
81
+ }
82
+ if (value instanceof Date) {
83
+ return `'${value.toISOString().slice(0, 10)}'`;
84
+ }
85
+ const str = String(value);
86
+ return `'${str.replace(/'/g, "''")}'`;
87
+ }
88
+ function escapeIdentifier(name) {
89
+ if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
90
+ return name;
91
+ }
92
+ return `"${name.replace(/"/g, '""')}"`;
93
+ }
94
+ function escapeList(values) {
95
+ if (values.length === 0) {
96
+ throw new Error("Cannot create IN clause with empty array");
97
+ }
98
+ return `(${values.map(escapeValue).join(", ")})`;
99
+ }
100
+
101
+ // src/query/select-builder.ts
102
+ function buildSelect(fields, select) {
103
+ if (select) {
104
+ const columns2 = Object.entries(select).filter(([, included]) => included).map(([field]) => escapeIdentifier(field));
105
+ if (!columns2.includes("id")) {
106
+ columns2.unshift("id");
107
+ }
108
+ return columns2.join(", ");
109
+ }
110
+ const columns = Object.keys(fields).map(escapeIdentifier);
111
+ const idIndex = columns.indexOf("id");
112
+ if (idIndex > 0) {
113
+ columns.splice(idIndex, 1);
114
+ columns.unshift("id");
115
+ }
116
+ return columns.length > 0 ? columns.join(", ") : "*";
117
+ }
118
+
119
+ // src/query/where-builder.ts
120
+ var FILTER_KEYS = /* @__PURE__ */ new Set([
121
+ "equals",
122
+ "not",
123
+ "in",
124
+ "notIn",
125
+ "gt",
126
+ "gte",
127
+ "lt",
128
+ "lte",
129
+ "contains",
130
+ "startsWith",
131
+ "endsWith",
132
+ "isNull"
133
+ ]);
134
+ function isFilterOperator(value) {
135
+ if (value === null || value === void 0 || typeof value !== "object" || Array.isArray(value)) {
136
+ return false;
137
+ }
138
+ return Object.keys(value).some((k) => FILTER_KEYS.has(k));
139
+ }
140
+ function buildFieldCondition(field, filter) {
141
+ const col = escapeIdentifier(field);
142
+ const conditions = [];
143
+ if (filter.equals !== void 0) {
144
+ if (filter.equals === null) {
145
+ conditions.push(`${col} IS NULL`);
146
+ } else {
147
+ conditions.push(`${col} = ${escapeValue(filter.equals)}`);
148
+ }
149
+ }
150
+ if (filter.not !== void 0) {
151
+ if (filter.not === null) {
152
+ conditions.push(`${col} IS NOT NULL`);
153
+ } else {
154
+ conditions.push(`${col} != ${escapeValue(filter.not)}`);
155
+ }
156
+ }
157
+ if (filter.in !== void 0) {
158
+ conditions.push(`${col} IN ${escapeList(filter.in)}`);
159
+ }
160
+ if (filter.notIn !== void 0) {
161
+ conditions.push(`${col} NOT IN ${escapeList(filter.notIn)}`);
162
+ }
163
+ if (filter.gt !== void 0) {
164
+ conditions.push(`${col} > ${escapeValue(filter.gt)}`);
165
+ }
166
+ if (filter.gte !== void 0) {
167
+ conditions.push(`${col} >= ${escapeValue(filter.gte)}`);
168
+ }
169
+ if (filter.lt !== void 0) {
170
+ conditions.push(`${col} < ${escapeValue(filter.lt)}`);
171
+ }
172
+ if (filter.lte !== void 0) {
173
+ conditions.push(`${col} <= ${escapeValue(filter.lte)}`);
174
+ }
175
+ if (filter.contains !== void 0) {
176
+ conditions.push(`${col} LIKE ${escapeValue(`%${filter.contains}%`)}`);
177
+ }
178
+ if (filter.startsWith !== void 0) {
179
+ conditions.push(`${col} LIKE ${escapeValue(`${filter.startsWith}%`)}`);
180
+ }
181
+ if (filter.endsWith !== void 0) {
182
+ conditions.push(`${col} LIKE ${escapeValue(`%${filter.endsWith}`)}`);
183
+ }
184
+ if (filter.isNull !== void 0) {
185
+ conditions.push(filter.isNull ? `${col} IS NULL` : `${col} IS NOT NULL`);
186
+ }
187
+ return conditions;
188
+ }
189
+ function buildWhere(where) {
190
+ if (!where) return "";
191
+ const parts = [];
192
+ for (const [key, value] of Object.entries(where)) {
193
+ if (key === "AND" && Array.isArray(value)) {
194
+ const subClauses = value.map((sub) => buildWhere(sub)).filter(Boolean);
195
+ if (subClauses.length > 0) {
196
+ parts.push(`(${subClauses.join(" AND ")})`);
197
+ }
198
+ continue;
199
+ }
200
+ if (key === "OR" && Array.isArray(value)) {
201
+ const subClauses = value.map((sub) => buildWhere(sub)).filter(Boolean);
202
+ if (subClauses.length > 0) {
203
+ parts.push(`(${subClauses.join(" OR ")})`);
204
+ }
205
+ continue;
206
+ }
207
+ if (key === "NOT" && value && typeof value === "object" && !Array.isArray(value)) {
208
+ const sub = buildWhere(value);
209
+ if (sub) {
210
+ parts.push(`NOT (${sub})`);
211
+ }
212
+ continue;
213
+ }
214
+ if (isFilterOperator(value)) {
215
+ parts.push(...buildFieldCondition(key, value));
216
+ } else {
217
+ const col = escapeIdentifier(key);
218
+ if (value === null || value === void 0) {
219
+ parts.push(`${col} IS NULL`);
220
+ } else {
221
+ parts.push(`${col} = ${escapeValue(value)}`);
222
+ }
223
+ }
224
+ }
225
+ return parts.join(" AND ");
226
+ }
227
+
228
+ // src/query/orderby-builder.ts
229
+ function buildOrderBy(orderBy) {
230
+ if (!orderBy) return "";
231
+ const parts = Object.entries(orderBy).map(
232
+ ([field, direction]) => `${escapeIdentifier(field)} ${direction.toUpperCase()}`
233
+ );
234
+ return parts.join(", ");
235
+ }
236
+
237
+ // src/query/query-builder.ts
238
+ function buildSuiteQL(recordId, fields, args) {
239
+ const selectClause = buildSelect(fields, args?.select);
240
+ const whereClause = buildWhere(args?.where);
241
+ const orderByClause = buildOrderBy(args?.orderBy);
242
+ let sql = `SELECT ${selectClause} FROM ${recordId}`;
243
+ if (whereClause) {
244
+ sql += ` WHERE ${whereClause}`;
245
+ }
246
+ if (orderByClause) {
247
+ sql += ` ORDER BY ${orderByClause}`;
248
+ }
249
+ return {
250
+ sql,
251
+ limit: args?.take ?? 1e3,
252
+ offset: args?.skip ?? 0
253
+ };
254
+ }
255
+ function buildCountQuery(recordId, args) {
256
+ const whereClause = buildWhere(args?.where);
257
+ let sql = `SELECT COUNT(*) AS count FROM ${recordId}`;
258
+ if (whereClause) {
259
+ sql += ` WHERE ${whereClause}`;
260
+ }
261
+ return sql;
262
+ }
263
+
264
+ // src/model-delegate.ts
265
+ var ModelDelegate = class {
266
+ constructor(recordId, recordDef, httpClient) {
267
+ this.recordId = recordId;
268
+ this.recordDef = recordDef;
269
+ this.httpClient = httpClient;
270
+ }
271
+ /** Find multiple records matching the given criteria. */
272
+ async findMany(args) {
273
+ const { sql, limit, offset } = buildSuiteQL(
274
+ this.recordId,
275
+ this.recordDef.fields,
276
+ args
277
+ );
278
+ const result = await (0, import_connector.executeSuiteQL)(this.httpClient, sql, { limit, offset });
279
+ return result.items;
280
+ }
281
+ /** Find the first record matching the given criteria, or null. */
282
+ async findFirst(args) {
283
+ const rows = await this.findMany({
284
+ ...args,
285
+ take: 1
286
+ });
287
+ return rows[0] ?? null;
288
+ }
289
+ /** Count records matching the given criteria. */
290
+ async count(args) {
291
+ const sql = buildCountQuery(this.recordId, args);
292
+ const result = await (0, import_connector.executeSuiteQL)(this.httpClient, sql, {
293
+ limit: 1
294
+ });
295
+ const row = result.items[0];
296
+ return row ? Number(row.count) : 0;
297
+ }
298
+ };
299
+
300
+ // src/client.ts
301
+ var SuitePortalClient = class {
302
+ httpClient;
303
+ schemaPath;
304
+ schema = null;
305
+ delegates = /* @__PURE__ */ new Map();
306
+ constructor(config, options) {
307
+ this.httpClient = new import_connector2.NetSuiteClient(config);
308
+ this.schemaPath = options?.schemaPath ?? ".suiteportal/schema.json";
309
+ return new Proxy(this, {
310
+ get(target, prop, receiver) {
311
+ if (typeof prop === "string" && prop.startsWith("$")) {
312
+ return Reflect.get(target, prop, receiver);
313
+ }
314
+ if (prop in target || typeof prop === "symbol" || prop === "then") {
315
+ return Reflect.get(target, prop, receiver);
316
+ }
317
+ return target.$delegate(prop);
318
+ }
319
+ });
320
+ }
321
+ /** Execute a raw SuiteQL query. */
322
+ async $queryRaw(sql) {
323
+ const result = await (0, import_connector3.executeSuiteQL)(this.httpClient, sql);
324
+ return result.items;
325
+ }
326
+ /** Clear the schema cache and delegates. */
327
+ $disconnect() {
328
+ this.delegates.clear();
329
+ this.schema = null;
330
+ clearSchemaCache();
331
+ }
332
+ /** Get or create a ModelDelegate for the given record name. */
333
+ $delegate(recordName) {
334
+ const existing = this.delegates.get(recordName);
335
+ if (existing) return existing;
336
+ if (!this.schema) {
337
+ throw new Error(
338
+ "Schema not loaded. Call await client.$loadSchema() before querying, or use createClient() which loads automatically."
339
+ );
340
+ }
341
+ const recordDef = this.schema.records[recordName];
342
+ if (!recordDef) {
343
+ throw new Error(
344
+ `Unknown record type "${recordName}". Available: ${Object.keys(this.schema.records).slice(0, 10).join(", ")}...`
345
+ );
346
+ }
347
+ const delegate = new ModelDelegate(recordName, recordDef, this.httpClient);
348
+ this.delegates.set(recordName, delegate);
349
+ return delegate;
350
+ }
351
+ /** Load the schema from disk. Must be called before accessing record delegates. */
352
+ async $loadSchema() {
353
+ this.schema = await loadSchema(this.schemaPath);
354
+ }
355
+ };
356
+ async function createClient(config, options) {
357
+ const client = new SuitePortalClient(config, options);
358
+ await client.$loadSchema();
359
+ return client;
360
+ }
361
+ // Annotate the CommonJS export names for ESM import in node:
362
+ 0 && (module.exports = {
363
+ ModelDelegate,
364
+ SuitePortalClient,
365
+ buildCountQuery,
366
+ buildOrderBy,
367
+ buildSelect,
368
+ buildSuiteQL,
369
+ buildWhere,
370
+ clearSchemaCache,
371
+ createClient,
372
+ escapeIdentifier,
373
+ escapeList,
374
+ escapeValue,
375
+ loadSchema
376
+ });
377
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/client.ts","../src/schema-loader.ts","../src/model-delegate.ts","../src/query/sql-escape.ts","../src/query/select-builder.ts","../src/query/where-builder.ts","../src/query/orderby-builder.ts","../src/query/query-builder.ts"],"sourcesContent":["// Client\nexport { SuitePortalClient, createClient } from './client.js';\nexport type { ClientOptions } from './client.js';\n\n// Model delegate\nexport { ModelDelegate } from './model-delegate.js';\n\n// Query builders (exposed for advanced use / testing)\nexport { buildSuiteQL, buildCountQuery } from './query/query-builder.js';\nexport { buildWhere } from './query/where-builder.js';\nexport { buildSelect } from './query/select-builder.js';\nexport { buildOrderBy } from './query/orderby-builder.js';\nexport { escapeValue, escapeIdentifier, escapeList } from './query/sql-escape.js';\n\n// Schema loader\nexport { loadSchema, clearSchemaCache } from './schema-loader.js';\n\n// Types\nexport type {\n FilterOperator,\n WhereInput,\n SelectInput,\n OrderByInput,\n FindManyArgs,\n FindFirstArgs,\n CountArgs,\n} from './types.js';\n","import { NetSuiteClient as HttpClient } from '@suiteportal/connector';\nimport { executeSuiteQL } from '@suiteportal/connector';\nimport type { NetSuiteConfig, SuiteQLRow } from '@suiteportal/connector';\nimport type { NormalizedSchema } from '@suiteportal/introspector';\nimport { loadSchema, clearSchemaCache } from './schema-loader.js';\nimport { ModelDelegate } from './model-delegate.js';\n\nexport interface ClientOptions {\n /** Path to schema.json. Default: '.suiteportal/schema.json' */\n schemaPath?: string;\n}\n\n/**\n * The SuitePortal query client. Provides Prisma-like access to NetSuite records\n * via dynamically created ModelDelegate instances.\n *\n * Access records as properties: `client.customer.findMany(...)`.\n * Uses a Proxy to lazily create delegates on first access.\n */\nexport class SuitePortalClient {\n private readonly httpClient: HttpClient;\n private readonly schemaPath: string;\n private schema: NormalizedSchema | null = null;\n private readonly delegates = new Map<string, ModelDelegate>();\n\n constructor(config: NetSuiteConfig, options?: ClientOptions) {\n this.httpClient = new HttpClient(config);\n this.schemaPath = options?.schemaPath ?? '.suiteportal/schema.json';\n\n // Return a Proxy so record names can be accessed as properties\n return new Proxy(this, {\n get(target, prop, receiver) {\n // Known own methods/properties\n if (typeof prop === 'string' && prop.startsWith('$')) {\n return Reflect.get(target, prop, receiver);\n }\n\n // Internal properties and methods\n if (\n prop in target ||\n typeof prop === 'symbol' ||\n prop === 'then' // Prevent Promise-like detection\n ) {\n return Reflect.get(target, prop, receiver);\n }\n\n // Treat as a record name → return a delegate\n return target.$delegate(prop as string);\n },\n });\n }\n\n /** Execute a raw SuiteQL query. */\n async $queryRaw<T = SuiteQLRow>(sql: string): Promise<T[]> {\n const result = await executeSuiteQL<T>(this.httpClient, sql);\n return result.items;\n }\n\n /** Clear the schema cache and delegates. */\n $disconnect(): void {\n this.delegates.clear();\n this.schema = null;\n clearSchemaCache();\n }\n\n /** Get or create a ModelDelegate for the given record name. */\n $delegate(recordName: string): ModelDelegate {\n const existing = this.delegates.get(recordName);\n if (existing) return existing;\n\n if (!this.schema) {\n throw new Error(\n 'Schema not loaded. Call await client.$loadSchema() before querying, ' +\n 'or use createClient() which loads automatically.',\n );\n }\n\n const recordDef = this.schema.records[recordName];\n if (!recordDef) {\n throw new Error(\n `Unknown record type \"${recordName}\". ` +\n `Available: ${Object.keys(this.schema.records).slice(0, 10).join(', ')}...`,\n );\n }\n\n const delegate = new ModelDelegate(recordName, recordDef, this.httpClient);\n this.delegates.set(recordName, delegate);\n return delegate;\n }\n\n /** Load the schema from disk. Must be called before accessing record delegates. */\n async $loadSchema(): Promise<void> {\n this.schema = await loadSchema(this.schemaPath);\n }\n}\n\n/**\n * Create a SuitePortalClient and load the schema.\n * This is the recommended way to create a client.\n */\nexport async function createClient(\n config: NetSuiteConfig,\n options?: ClientOptions,\n): Promise<SuitePortalClient> {\n const client = new SuitePortalClient(config, options);\n await client.$loadSchema();\n return client;\n}\n","import { readFile } from 'node:fs/promises';\nimport type { NormalizedSchema } from '@suiteportal/introspector';\n\nlet cachedSchema: NormalizedSchema | null = null;\nlet cachedPath: string | null = null;\n\n/**\n * Load and parse schema.json from disk. Caches the result so\n * subsequent calls with the same path return immediately.\n */\nexport async function loadSchema(schemaPath: string): Promise<NormalizedSchema> {\n if (cachedSchema && cachedPath === schemaPath) {\n return cachedSchema;\n }\n\n const raw = await readFile(schemaPath, 'utf-8');\n const schema = JSON.parse(raw) as NormalizedSchema;\n\n if (!schema.records || typeof schema.records !== 'object') {\n throw new Error(`Invalid schema: missing \"records\" in ${schemaPath}`);\n }\n\n cachedSchema = schema;\n cachedPath = schemaPath;\n return schema;\n}\n\n/** Clear the cached schema (useful for testing). */\nexport function clearSchemaCache(): void {\n cachedSchema = null;\n cachedPath = null;\n}\n","import type { NetSuiteClient } from '@suiteportal/connector';\nimport { executeSuiteQL } from '@suiteportal/connector';\nimport type { RecordDefinition } from '@suiteportal/introspector';\nimport type { FindManyArgs, FindFirstArgs, CountArgs } from './types.js';\nimport { buildSuiteQL, buildCountQuery } from './query/query-builder.js';\n\n/**\n * A delegate for a single record type. Provides findMany, findFirst, and count.\n */\nexport class ModelDelegate {\n constructor(\n private readonly recordId: string,\n private readonly recordDef: RecordDefinition,\n private readonly httpClient: NetSuiteClient,\n ) {}\n\n /** Find multiple records matching the given criteria. */\n async findMany(args?: FindManyArgs): Promise<Record<string, unknown>[]> {\n const { sql, limit, offset } = buildSuiteQL(\n this.recordId,\n this.recordDef.fields,\n args,\n );\n\n const result = await executeSuiteQL(this.httpClient, sql, { limit, offset });\n return result.items;\n }\n\n /** Find the first record matching the given criteria, or null. */\n async findFirst(args?: FindFirstArgs): Promise<Record<string, unknown> | null> {\n const rows = await this.findMany({\n ...args,\n take: 1,\n });\n return rows[0] ?? null;\n }\n\n /** Count records matching the given criteria. */\n async count(args?: CountArgs): Promise<number> {\n const sql = buildCountQuery(this.recordId, args);\n const result = await executeSuiteQL<{ count: string }>(this.httpClient, sql, {\n limit: 1,\n });\n\n const row = result.items[0];\n return row ? Number(row.count) : 0;\n }\n}\n","/**\n * Escape a value for safe inclusion in a SuiteQL query string.\n * Returns the SQL literal representation (with quotes for strings).\n */\nexport function escapeValue(value: unknown): string {\n if (value === null || value === undefined) {\n return 'NULL';\n }\n\n if (typeof value === 'boolean') {\n return value ? \"'T'\" : \"'F'\";\n }\n\n if (typeof value === 'number') {\n if (!Number.isFinite(value)) {\n throw new Error(`Cannot escape non-finite number: ${value}`);\n }\n return String(value);\n }\n\n if (value instanceof Date) {\n return `'${value.toISOString().slice(0, 10)}'`;\n }\n\n // String — escape single quotes by doubling them\n const str = String(value);\n return `'${str.replace(/'/g, \"''\")}'`;\n}\n\n/**\n * Escape a SQL identifier (column/table name).\n * Wraps in double quotes if it contains special characters or is a reserved word.\n */\nexport function escapeIdentifier(name: string): string {\n // SuiteQL identifiers are case-insensitive and generally safe as-is,\n // but we wrap in double quotes if the name contains anything unusual\n if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {\n return name;\n }\n return `\"${name.replace(/\"/g, '\"\"')}\"`;\n}\n\n/**\n * Format an array of values for an IN clause: (val1, val2, val3)\n */\nexport function escapeList(values: unknown[]): string {\n if (values.length === 0) {\n throw new Error('Cannot create IN clause with empty array');\n }\n return `(${values.map(escapeValue).join(', ')})`;\n}\n","import type { SelectInput } from '../types.js';\nimport type { FieldDefinition } from '@suiteportal/introspector';\nimport { escapeIdentifier } from './sql-escape.js';\n\n/**\n * Build a SQL SELECT column list.\n *\n * - If `select` is provided, emit only the selected fields (plus `id` always).\n * - If `select` is omitted, emit all scalar fields from the record definition.\n */\nexport function buildSelect(\n fields: Record<string, FieldDefinition>,\n select?: SelectInput,\n): string {\n if (select) {\n const columns = Object.entries(select)\n .filter(([, included]) => included)\n .map(([field]) => escapeIdentifier(field));\n\n // Always include id\n if (!columns.includes('id')) {\n columns.unshift('id');\n }\n\n return columns.join(', ');\n }\n\n // Default: all fields\n const columns = Object.keys(fields).map(escapeIdentifier);\n\n // Ensure id is first if it exists\n const idIndex = columns.indexOf('id');\n if (idIndex > 0) {\n columns.splice(idIndex, 1);\n columns.unshift('id');\n }\n\n return columns.length > 0 ? columns.join(', ') : '*';\n}\n","import type { WhereInput, FilterOperator } from '../types.js';\nimport { escapeValue, escapeIdentifier, escapeList } from './sql-escape.js';\n\nconst FILTER_KEYS = new Set([\n 'equals', 'not', 'in', 'notIn', 'gt', 'gte', 'lt', 'lte',\n 'contains', 'startsWith', 'endsWith', 'isNull',\n]);\n\n/** Check if a value looks like a FilterOperator object. */\nfunction isFilterOperator(value: unknown): value is FilterOperator {\n if (value === null || value === undefined || typeof value !== 'object' || Array.isArray(value)) {\n return false;\n }\n return Object.keys(value as object).some((k) => FILTER_KEYS.has(k));\n}\n\n/** Build a single field condition from a FilterOperator. */\nfunction buildFieldCondition(field: string, filter: FilterOperator): string[] {\n const col = escapeIdentifier(field);\n const conditions: string[] = [];\n\n if (filter.equals !== undefined) {\n if (filter.equals === null) {\n conditions.push(`${col} IS NULL`);\n } else {\n conditions.push(`${col} = ${escapeValue(filter.equals)}`);\n }\n }\n\n if (filter.not !== undefined) {\n if (filter.not === null) {\n conditions.push(`${col} IS NOT NULL`);\n } else {\n conditions.push(`${col} != ${escapeValue(filter.not)}`);\n }\n }\n\n if (filter.in !== undefined) {\n conditions.push(`${col} IN ${escapeList(filter.in)}`);\n }\n\n if (filter.notIn !== undefined) {\n conditions.push(`${col} NOT IN ${escapeList(filter.notIn)}`);\n }\n\n if (filter.gt !== undefined) {\n conditions.push(`${col} > ${escapeValue(filter.gt)}`);\n }\n\n if (filter.gte !== undefined) {\n conditions.push(`${col} >= ${escapeValue(filter.gte)}`);\n }\n\n if (filter.lt !== undefined) {\n conditions.push(`${col} < ${escapeValue(filter.lt)}`);\n }\n\n if (filter.lte !== undefined) {\n conditions.push(`${col} <= ${escapeValue(filter.lte)}`);\n }\n\n if (filter.contains !== undefined) {\n conditions.push(`${col} LIKE ${escapeValue(`%${filter.contains}%`)}`);\n }\n\n if (filter.startsWith !== undefined) {\n conditions.push(`${col} LIKE ${escapeValue(`${filter.startsWith}%`)}`);\n }\n\n if (filter.endsWith !== undefined) {\n conditions.push(`${col} LIKE ${escapeValue(`%${filter.endsWith}`)}`);\n }\n\n if (filter.isNull !== undefined) {\n conditions.push(filter.isNull ? `${col} IS NULL` : `${col} IS NOT NULL`);\n }\n\n return conditions;\n}\n\n/**\n * Build a SQL WHERE clause from a WhereInput.\n * Returns the clause without the \"WHERE\" keyword, or empty string if no conditions.\n */\nexport function buildWhere(where?: WhereInput): string {\n if (!where) return '';\n\n const parts: string[] = [];\n\n for (const [key, value] of Object.entries(where)) {\n if (key === 'AND' && Array.isArray(value)) {\n const subClauses = (value as WhereInput[])\n .map((sub) => buildWhere(sub))\n .filter(Boolean);\n if (subClauses.length > 0) {\n parts.push(`(${subClauses.join(' AND ')})`);\n }\n continue;\n }\n\n if (key === 'OR' && Array.isArray(value)) {\n const subClauses = (value as WhereInput[])\n .map((sub) => buildWhere(sub))\n .filter(Boolean);\n if (subClauses.length > 0) {\n parts.push(`(${subClauses.join(' OR ')})`);\n }\n continue;\n }\n\n if (key === 'NOT' && value && typeof value === 'object' && !Array.isArray(value)) {\n const sub = buildWhere(value as WhereInput);\n if (sub) {\n parts.push(`NOT (${sub})`);\n }\n continue;\n }\n\n // Field-level condition\n if (isFilterOperator(value)) {\n parts.push(...buildFieldCondition(key, value));\n } else {\n // Shorthand: { field: value } is equals\n const col = escapeIdentifier(key);\n if (value === null || value === undefined) {\n parts.push(`${col} IS NULL`);\n } else {\n parts.push(`${col} = ${escapeValue(value)}`);\n }\n }\n }\n\n return parts.join(' AND ');\n}\n","import type { OrderByInput } from '../types.js';\nimport { escapeIdentifier } from './sql-escape.js';\n\n/**\n * Build a SQL ORDER BY clause from an OrderByInput.\n * Returns the clause without the \"ORDER BY\" keyword, or empty string if none.\n */\nexport function buildOrderBy(orderBy?: OrderByInput): string {\n if (!orderBy) return '';\n\n const parts = Object.entries(orderBy).map(\n ([field, direction]) => `${escapeIdentifier(field)} ${direction.toUpperCase()}`,\n );\n\n return parts.join(', ');\n}\n","import type { FieldDefinition } from '@suiteportal/introspector';\nimport type { FindManyArgs } from '../types.js';\nimport { buildSelect } from './select-builder.js';\nimport { buildWhere } from './where-builder.js';\nimport { buildOrderBy } from './orderby-builder.js';\n\nexport interface BuiltQuery {\n sql: string;\n limit: number;\n offset: number;\n}\n\n/**\n * Build a complete SuiteQL query from FindManyArgs.\n *\n * The `take` and `skip` values are NOT added to the SQL —\n * they're passed as REST query params (`limit`, `offset`) to the SuiteQL endpoint.\n */\nexport function buildSuiteQL(\n recordId: string,\n fields: Record<string, FieldDefinition>,\n args?: FindManyArgs,\n): BuiltQuery {\n const selectClause = buildSelect(fields, args?.select);\n const whereClause = buildWhere(args?.where);\n const orderByClause = buildOrderBy(args?.orderBy);\n\n let sql = `SELECT ${selectClause} FROM ${recordId}`;\n\n if (whereClause) {\n sql += ` WHERE ${whereClause}`;\n }\n\n if (orderByClause) {\n sql += ` ORDER BY ${orderByClause}`;\n }\n\n return {\n sql,\n limit: args?.take ?? 1000,\n offset: args?.skip ?? 0,\n };\n}\n\n/**\n * Build a SuiteQL COUNT query.\n */\nexport function buildCountQuery(\n recordId: string,\n args?: { where?: FindManyArgs['where'] },\n): string {\n const whereClause = buildWhere(args?.where);\n let sql = `SELECT COUNT(*) AS count FROM ${recordId}`;\n\n if (whereClause) {\n sql += ` WHERE ${whereClause}`;\n }\n\n return sql;\n}\n"],"mappings":"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","names":["import_connector","columns","HttpClient"]}
@@ -0,0 +1,161 @@
1
+ import { NetSuiteClient, NetSuiteConfig, SuiteQLRow } from '@suiteportal/connector';
2
+ import { RecordDefinition, FieldDefinition, NormalizedSchema } from '@suiteportal/introspector';
3
+
4
+ /** Filter operators for where clauses. */
5
+ interface FilterOperator {
6
+ equals?: unknown;
7
+ not?: unknown;
8
+ in?: unknown[];
9
+ notIn?: unknown[];
10
+ gt?: unknown;
11
+ gte?: unknown;
12
+ lt?: unknown;
13
+ lte?: unknown;
14
+ contains?: string;
15
+ startsWith?: string;
16
+ endsWith?: string;
17
+ isNull?: boolean;
18
+ }
19
+ /** Where input — field names map to a value (shorthand for equals) or FilterOperator. */
20
+ type WhereInput = {
21
+ [field: string]: unknown | FilterOperator;
22
+ } & {
23
+ AND?: WhereInput[];
24
+ OR?: WhereInput[];
25
+ NOT?: WhereInput;
26
+ };
27
+ /** Select input — field names map to true to include. */
28
+ type SelectInput = Record<string, boolean>;
29
+ /** OrderBy input — field name → 'asc' | 'desc'. */
30
+ type OrderByInput = Record<string, 'asc' | 'desc'>;
31
+ /** Arguments for findMany queries. */
32
+ interface FindManyArgs {
33
+ where?: WhereInput;
34
+ select?: SelectInput;
35
+ orderBy?: OrderByInput;
36
+ take?: number;
37
+ skip?: number;
38
+ }
39
+ /** Arguments for findFirst queries. */
40
+ interface FindFirstArgs {
41
+ where?: WhereInput;
42
+ select?: SelectInput;
43
+ orderBy?: OrderByInput;
44
+ skip?: number;
45
+ }
46
+ /** Arguments for count queries. */
47
+ interface CountArgs {
48
+ where?: WhereInput;
49
+ }
50
+
51
+ /**
52
+ * A delegate for a single record type. Provides findMany, findFirst, and count.
53
+ */
54
+ declare class ModelDelegate {
55
+ private readonly recordId;
56
+ private readonly recordDef;
57
+ private readonly httpClient;
58
+ constructor(recordId: string, recordDef: RecordDefinition, httpClient: NetSuiteClient);
59
+ /** Find multiple records matching the given criteria. */
60
+ findMany(args?: FindManyArgs): Promise<Record<string, unknown>[]>;
61
+ /** Find the first record matching the given criteria, or null. */
62
+ findFirst(args?: FindFirstArgs): Promise<Record<string, unknown> | null>;
63
+ /** Count records matching the given criteria. */
64
+ count(args?: CountArgs): Promise<number>;
65
+ }
66
+
67
+ interface ClientOptions {
68
+ /** Path to schema.json. Default: '.suiteportal/schema.json' */
69
+ schemaPath?: string;
70
+ }
71
+ /**
72
+ * The SuitePortal query client. Provides Prisma-like access to NetSuite records
73
+ * via dynamically created ModelDelegate instances.
74
+ *
75
+ * Access records as properties: `client.customer.findMany(...)`.
76
+ * Uses a Proxy to lazily create delegates on first access.
77
+ */
78
+ declare class SuitePortalClient {
79
+ private readonly httpClient;
80
+ private readonly schemaPath;
81
+ private schema;
82
+ private readonly delegates;
83
+ constructor(config: NetSuiteConfig, options?: ClientOptions);
84
+ /** Execute a raw SuiteQL query. */
85
+ $queryRaw<T = SuiteQLRow>(sql: string): Promise<T[]>;
86
+ /** Clear the schema cache and delegates. */
87
+ $disconnect(): void;
88
+ /** Get or create a ModelDelegate for the given record name. */
89
+ $delegate(recordName: string): ModelDelegate;
90
+ /** Load the schema from disk. Must be called before accessing record delegates. */
91
+ $loadSchema(): Promise<void>;
92
+ }
93
+ /**
94
+ * Create a SuitePortalClient and load the schema.
95
+ * This is the recommended way to create a client.
96
+ */
97
+ declare function createClient(config: NetSuiteConfig, options?: ClientOptions): Promise<SuitePortalClient>;
98
+
99
+ interface BuiltQuery {
100
+ sql: string;
101
+ limit: number;
102
+ offset: number;
103
+ }
104
+ /**
105
+ * Build a complete SuiteQL query from FindManyArgs.
106
+ *
107
+ * The `take` and `skip` values are NOT added to the SQL —
108
+ * they're passed as REST query params (`limit`, `offset`) to the SuiteQL endpoint.
109
+ */
110
+ declare function buildSuiteQL(recordId: string, fields: Record<string, FieldDefinition>, args?: FindManyArgs): BuiltQuery;
111
+ /**
112
+ * Build a SuiteQL COUNT query.
113
+ */
114
+ declare function buildCountQuery(recordId: string, args?: {
115
+ where?: FindManyArgs['where'];
116
+ }): string;
117
+
118
+ /**
119
+ * Build a SQL WHERE clause from a WhereInput.
120
+ * Returns the clause without the "WHERE" keyword, or empty string if no conditions.
121
+ */
122
+ declare function buildWhere(where?: WhereInput): string;
123
+
124
+ /**
125
+ * Build a SQL SELECT column list.
126
+ *
127
+ * - If `select` is provided, emit only the selected fields (plus `id` always).
128
+ * - If `select` is omitted, emit all scalar fields from the record definition.
129
+ */
130
+ declare function buildSelect(fields: Record<string, FieldDefinition>, select?: SelectInput): string;
131
+
132
+ /**
133
+ * Build a SQL ORDER BY clause from an OrderByInput.
134
+ * Returns the clause without the "ORDER BY" keyword, or empty string if none.
135
+ */
136
+ declare function buildOrderBy(orderBy?: OrderByInput): string;
137
+
138
+ /**
139
+ * Escape a value for safe inclusion in a SuiteQL query string.
140
+ * Returns the SQL literal representation (with quotes for strings).
141
+ */
142
+ declare function escapeValue(value: unknown): string;
143
+ /**
144
+ * Escape a SQL identifier (column/table name).
145
+ * Wraps in double quotes if it contains special characters or is a reserved word.
146
+ */
147
+ declare function escapeIdentifier(name: string): string;
148
+ /**
149
+ * Format an array of values for an IN clause: (val1, val2, val3)
150
+ */
151
+ declare function escapeList(values: unknown[]): string;
152
+
153
+ /**
154
+ * Load and parse schema.json from disk. Caches the result so
155
+ * subsequent calls with the same path return immediately.
156
+ */
157
+ declare function loadSchema(schemaPath: string): Promise<NormalizedSchema>;
158
+ /** Clear the cached schema (useful for testing). */
159
+ declare function clearSchemaCache(): void;
160
+
161
+ export { type ClientOptions, type CountArgs, type FilterOperator, type FindFirstArgs, type FindManyArgs, ModelDelegate, type OrderByInput, type SelectInput, SuitePortalClient, type WhereInput, buildCountQuery, buildOrderBy, buildSelect, buildSuiteQL, buildWhere, clearSchemaCache, createClient, escapeIdentifier, escapeList, escapeValue, loadSchema };
@@ -0,0 +1,161 @@
1
+ import { NetSuiteClient, NetSuiteConfig, SuiteQLRow } from '@suiteportal/connector';
2
+ import { RecordDefinition, FieldDefinition, NormalizedSchema } from '@suiteportal/introspector';
3
+
4
+ /** Filter operators for where clauses. */
5
+ interface FilterOperator {
6
+ equals?: unknown;
7
+ not?: unknown;
8
+ in?: unknown[];
9
+ notIn?: unknown[];
10
+ gt?: unknown;
11
+ gte?: unknown;
12
+ lt?: unknown;
13
+ lte?: unknown;
14
+ contains?: string;
15
+ startsWith?: string;
16
+ endsWith?: string;
17
+ isNull?: boolean;
18
+ }
19
+ /** Where input — field names map to a value (shorthand for equals) or FilterOperator. */
20
+ type WhereInput = {
21
+ [field: string]: unknown | FilterOperator;
22
+ } & {
23
+ AND?: WhereInput[];
24
+ OR?: WhereInput[];
25
+ NOT?: WhereInput;
26
+ };
27
+ /** Select input — field names map to true to include. */
28
+ type SelectInput = Record<string, boolean>;
29
+ /** OrderBy input — field name → 'asc' | 'desc'. */
30
+ type OrderByInput = Record<string, 'asc' | 'desc'>;
31
+ /** Arguments for findMany queries. */
32
+ interface FindManyArgs {
33
+ where?: WhereInput;
34
+ select?: SelectInput;
35
+ orderBy?: OrderByInput;
36
+ take?: number;
37
+ skip?: number;
38
+ }
39
+ /** Arguments for findFirst queries. */
40
+ interface FindFirstArgs {
41
+ where?: WhereInput;
42
+ select?: SelectInput;
43
+ orderBy?: OrderByInput;
44
+ skip?: number;
45
+ }
46
+ /** Arguments for count queries. */
47
+ interface CountArgs {
48
+ where?: WhereInput;
49
+ }
50
+
51
+ /**
52
+ * A delegate for a single record type. Provides findMany, findFirst, and count.
53
+ */
54
+ declare class ModelDelegate {
55
+ private readonly recordId;
56
+ private readonly recordDef;
57
+ private readonly httpClient;
58
+ constructor(recordId: string, recordDef: RecordDefinition, httpClient: NetSuiteClient);
59
+ /** Find multiple records matching the given criteria. */
60
+ findMany(args?: FindManyArgs): Promise<Record<string, unknown>[]>;
61
+ /** Find the first record matching the given criteria, or null. */
62
+ findFirst(args?: FindFirstArgs): Promise<Record<string, unknown> | null>;
63
+ /** Count records matching the given criteria. */
64
+ count(args?: CountArgs): Promise<number>;
65
+ }
66
+
67
+ interface ClientOptions {
68
+ /** Path to schema.json. Default: '.suiteportal/schema.json' */
69
+ schemaPath?: string;
70
+ }
71
+ /**
72
+ * The SuitePortal query client. Provides Prisma-like access to NetSuite records
73
+ * via dynamically created ModelDelegate instances.
74
+ *
75
+ * Access records as properties: `client.customer.findMany(...)`.
76
+ * Uses a Proxy to lazily create delegates on first access.
77
+ */
78
+ declare class SuitePortalClient {
79
+ private readonly httpClient;
80
+ private readonly schemaPath;
81
+ private schema;
82
+ private readonly delegates;
83
+ constructor(config: NetSuiteConfig, options?: ClientOptions);
84
+ /** Execute a raw SuiteQL query. */
85
+ $queryRaw<T = SuiteQLRow>(sql: string): Promise<T[]>;
86
+ /** Clear the schema cache and delegates. */
87
+ $disconnect(): void;
88
+ /** Get or create a ModelDelegate for the given record name. */
89
+ $delegate(recordName: string): ModelDelegate;
90
+ /** Load the schema from disk. Must be called before accessing record delegates. */
91
+ $loadSchema(): Promise<void>;
92
+ }
93
+ /**
94
+ * Create a SuitePortalClient and load the schema.
95
+ * This is the recommended way to create a client.
96
+ */
97
+ declare function createClient(config: NetSuiteConfig, options?: ClientOptions): Promise<SuitePortalClient>;
98
+
99
+ interface BuiltQuery {
100
+ sql: string;
101
+ limit: number;
102
+ offset: number;
103
+ }
104
+ /**
105
+ * Build a complete SuiteQL query from FindManyArgs.
106
+ *
107
+ * The `take` and `skip` values are NOT added to the SQL —
108
+ * they're passed as REST query params (`limit`, `offset`) to the SuiteQL endpoint.
109
+ */
110
+ declare function buildSuiteQL(recordId: string, fields: Record<string, FieldDefinition>, args?: FindManyArgs): BuiltQuery;
111
+ /**
112
+ * Build a SuiteQL COUNT query.
113
+ */
114
+ declare function buildCountQuery(recordId: string, args?: {
115
+ where?: FindManyArgs['where'];
116
+ }): string;
117
+
118
+ /**
119
+ * Build a SQL WHERE clause from a WhereInput.
120
+ * Returns the clause without the "WHERE" keyword, or empty string if no conditions.
121
+ */
122
+ declare function buildWhere(where?: WhereInput): string;
123
+
124
+ /**
125
+ * Build a SQL SELECT column list.
126
+ *
127
+ * - If `select` is provided, emit only the selected fields (plus `id` always).
128
+ * - If `select` is omitted, emit all scalar fields from the record definition.
129
+ */
130
+ declare function buildSelect(fields: Record<string, FieldDefinition>, select?: SelectInput): string;
131
+
132
+ /**
133
+ * Build a SQL ORDER BY clause from an OrderByInput.
134
+ * Returns the clause without the "ORDER BY" keyword, or empty string if none.
135
+ */
136
+ declare function buildOrderBy(orderBy?: OrderByInput): string;
137
+
138
+ /**
139
+ * Escape a value for safe inclusion in a SuiteQL query string.
140
+ * Returns the SQL literal representation (with quotes for strings).
141
+ */
142
+ declare function escapeValue(value: unknown): string;
143
+ /**
144
+ * Escape a SQL identifier (column/table name).
145
+ * Wraps in double quotes if it contains special characters or is a reserved word.
146
+ */
147
+ declare function escapeIdentifier(name: string): string;
148
+ /**
149
+ * Format an array of values for an IN clause: (val1, val2, val3)
150
+ */
151
+ declare function escapeList(values: unknown[]): string;
152
+
153
+ /**
154
+ * Load and parse schema.json from disk. Caches the result so
155
+ * subsequent calls with the same path return immediately.
156
+ */
157
+ declare function loadSchema(schemaPath: string): Promise<NormalizedSchema>;
158
+ /** Clear the cached schema (useful for testing). */
159
+ declare function clearSchemaCache(): void;
160
+
161
+ export { type ClientOptions, type CountArgs, type FilterOperator, type FindFirstArgs, type FindManyArgs, ModelDelegate, type OrderByInput, type SelectInput, SuitePortalClient, type WhereInput, buildCountQuery, buildOrderBy, buildSelect, buildSuiteQL, buildWhere, clearSchemaCache, createClient, escapeIdentifier, escapeList, escapeValue, loadSchema };
package/dist/index.js ADDED
@@ -0,0 +1,338 @@
1
+ // src/client.ts
2
+ import { NetSuiteClient as HttpClient } from "@suiteportal/connector";
3
+ import { executeSuiteQL as executeSuiteQL2 } from "@suiteportal/connector";
4
+
5
+ // src/schema-loader.ts
6
+ import { readFile } from "fs/promises";
7
+ var cachedSchema = null;
8
+ var cachedPath = null;
9
+ async function loadSchema(schemaPath) {
10
+ if (cachedSchema && cachedPath === schemaPath) {
11
+ return cachedSchema;
12
+ }
13
+ const raw = await readFile(schemaPath, "utf-8");
14
+ const schema = JSON.parse(raw);
15
+ if (!schema.records || typeof schema.records !== "object") {
16
+ throw new Error(`Invalid schema: missing "records" in ${schemaPath}`);
17
+ }
18
+ cachedSchema = schema;
19
+ cachedPath = schemaPath;
20
+ return schema;
21
+ }
22
+ function clearSchemaCache() {
23
+ cachedSchema = null;
24
+ cachedPath = null;
25
+ }
26
+
27
+ // src/model-delegate.ts
28
+ import { executeSuiteQL } from "@suiteportal/connector";
29
+
30
+ // src/query/sql-escape.ts
31
+ function escapeValue(value) {
32
+ if (value === null || value === void 0) {
33
+ return "NULL";
34
+ }
35
+ if (typeof value === "boolean") {
36
+ return value ? "'T'" : "'F'";
37
+ }
38
+ if (typeof value === "number") {
39
+ if (!Number.isFinite(value)) {
40
+ throw new Error(`Cannot escape non-finite number: ${value}`);
41
+ }
42
+ return String(value);
43
+ }
44
+ if (value instanceof Date) {
45
+ return `'${value.toISOString().slice(0, 10)}'`;
46
+ }
47
+ const str = String(value);
48
+ return `'${str.replace(/'/g, "''")}'`;
49
+ }
50
+ function escapeIdentifier(name) {
51
+ if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
52
+ return name;
53
+ }
54
+ return `"${name.replace(/"/g, '""')}"`;
55
+ }
56
+ function escapeList(values) {
57
+ if (values.length === 0) {
58
+ throw new Error("Cannot create IN clause with empty array");
59
+ }
60
+ return `(${values.map(escapeValue).join(", ")})`;
61
+ }
62
+
63
+ // src/query/select-builder.ts
64
+ function buildSelect(fields, select) {
65
+ if (select) {
66
+ const columns2 = Object.entries(select).filter(([, included]) => included).map(([field]) => escapeIdentifier(field));
67
+ if (!columns2.includes("id")) {
68
+ columns2.unshift("id");
69
+ }
70
+ return columns2.join(", ");
71
+ }
72
+ const columns = Object.keys(fields).map(escapeIdentifier);
73
+ const idIndex = columns.indexOf("id");
74
+ if (idIndex > 0) {
75
+ columns.splice(idIndex, 1);
76
+ columns.unshift("id");
77
+ }
78
+ return columns.length > 0 ? columns.join(", ") : "*";
79
+ }
80
+
81
+ // src/query/where-builder.ts
82
+ var FILTER_KEYS = /* @__PURE__ */ new Set([
83
+ "equals",
84
+ "not",
85
+ "in",
86
+ "notIn",
87
+ "gt",
88
+ "gte",
89
+ "lt",
90
+ "lte",
91
+ "contains",
92
+ "startsWith",
93
+ "endsWith",
94
+ "isNull"
95
+ ]);
96
+ function isFilterOperator(value) {
97
+ if (value === null || value === void 0 || typeof value !== "object" || Array.isArray(value)) {
98
+ return false;
99
+ }
100
+ return Object.keys(value).some((k) => FILTER_KEYS.has(k));
101
+ }
102
+ function buildFieldCondition(field, filter) {
103
+ const col = escapeIdentifier(field);
104
+ const conditions = [];
105
+ if (filter.equals !== void 0) {
106
+ if (filter.equals === null) {
107
+ conditions.push(`${col} IS NULL`);
108
+ } else {
109
+ conditions.push(`${col} = ${escapeValue(filter.equals)}`);
110
+ }
111
+ }
112
+ if (filter.not !== void 0) {
113
+ if (filter.not === null) {
114
+ conditions.push(`${col} IS NOT NULL`);
115
+ } else {
116
+ conditions.push(`${col} != ${escapeValue(filter.not)}`);
117
+ }
118
+ }
119
+ if (filter.in !== void 0) {
120
+ conditions.push(`${col} IN ${escapeList(filter.in)}`);
121
+ }
122
+ if (filter.notIn !== void 0) {
123
+ conditions.push(`${col} NOT IN ${escapeList(filter.notIn)}`);
124
+ }
125
+ if (filter.gt !== void 0) {
126
+ conditions.push(`${col} > ${escapeValue(filter.gt)}`);
127
+ }
128
+ if (filter.gte !== void 0) {
129
+ conditions.push(`${col} >= ${escapeValue(filter.gte)}`);
130
+ }
131
+ if (filter.lt !== void 0) {
132
+ conditions.push(`${col} < ${escapeValue(filter.lt)}`);
133
+ }
134
+ if (filter.lte !== void 0) {
135
+ conditions.push(`${col} <= ${escapeValue(filter.lte)}`);
136
+ }
137
+ if (filter.contains !== void 0) {
138
+ conditions.push(`${col} LIKE ${escapeValue(`%${filter.contains}%`)}`);
139
+ }
140
+ if (filter.startsWith !== void 0) {
141
+ conditions.push(`${col} LIKE ${escapeValue(`${filter.startsWith}%`)}`);
142
+ }
143
+ if (filter.endsWith !== void 0) {
144
+ conditions.push(`${col} LIKE ${escapeValue(`%${filter.endsWith}`)}`);
145
+ }
146
+ if (filter.isNull !== void 0) {
147
+ conditions.push(filter.isNull ? `${col} IS NULL` : `${col} IS NOT NULL`);
148
+ }
149
+ return conditions;
150
+ }
151
+ function buildWhere(where) {
152
+ if (!where) return "";
153
+ const parts = [];
154
+ for (const [key, value] of Object.entries(where)) {
155
+ if (key === "AND" && Array.isArray(value)) {
156
+ const subClauses = value.map((sub) => buildWhere(sub)).filter(Boolean);
157
+ if (subClauses.length > 0) {
158
+ parts.push(`(${subClauses.join(" AND ")})`);
159
+ }
160
+ continue;
161
+ }
162
+ if (key === "OR" && Array.isArray(value)) {
163
+ const subClauses = value.map((sub) => buildWhere(sub)).filter(Boolean);
164
+ if (subClauses.length > 0) {
165
+ parts.push(`(${subClauses.join(" OR ")})`);
166
+ }
167
+ continue;
168
+ }
169
+ if (key === "NOT" && value && typeof value === "object" && !Array.isArray(value)) {
170
+ const sub = buildWhere(value);
171
+ if (sub) {
172
+ parts.push(`NOT (${sub})`);
173
+ }
174
+ continue;
175
+ }
176
+ if (isFilterOperator(value)) {
177
+ parts.push(...buildFieldCondition(key, value));
178
+ } else {
179
+ const col = escapeIdentifier(key);
180
+ if (value === null || value === void 0) {
181
+ parts.push(`${col} IS NULL`);
182
+ } else {
183
+ parts.push(`${col} = ${escapeValue(value)}`);
184
+ }
185
+ }
186
+ }
187
+ return parts.join(" AND ");
188
+ }
189
+
190
+ // src/query/orderby-builder.ts
191
+ function buildOrderBy(orderBy) {
192
+ if (!orderBy) return "";
193
+ const parts = Object.entries(orderBy).map(
194
+ ([field, direction]) => `${escapeIdentifier(field)} ${direction.toUpperCase()}`
195
+ );
196
+ return parts.join(", ");
197
+ }
198
+
199
+ // src/query/query-builder.ts
200
+ function buildSuiteQL(recordId, fields, args) {
201
+ const selectClause = buildSelect(fields, args?.select);
202
+ const whereClause = buildWhere(args?.where);
203
+ const orderByClause = buildOrderBy(args?.orderBy);
204
+ let sql = `SELECT ${selectClause} FROM ${recordId}`;
205
+ if (whereClause) {
206
+ sql += ` WHERE ${whereClause}`;
207
+ }
208
+ if (orderByClause) {
209
+ sql += ` ORDER BY ${orderByClause}`;
210
+ }
211
+ return {
212
+ sql,
213
+ limit: args?.take ?? 1e3,
214
+ offset: args?.skip ?? 0
215
+ };
216
+ }
217
+ function buildCountQuery(recordId, args) {
218
+ const whereClause = buildWhere(args?.where);
219
+ let sql = `SELECT COUNT(*) AS count FROM ${recordId}`;
220
+ if (whereClause) {
221
+ sql += ` WHERE ${whereClause}`;
222
+ }
223
+ return sql;
224
+ }
225
+
226
+ // src/model-delegate.ts
227
+ var ModelDelegate = class {
228
+ constructor(recordId, recordDef, httpClient) {
229
+ this.recordId = recordId;
230
+ this.recordDef = recordDef;
231
+ this.httpClient = httpClient;
232
+ }
233
+ /** Find multiple records matching the given criteria. */
234
+ async findMany(args) {
235
+ const { sql, limit, offset } = buildSuiteQL(
236
+ this.recordId,
237
+ this.recordDef.fields,
238
+ args
239
+ );
240
+ const result = await executeSuiteQL(this.httpClient, sql, { limit, offset });
241
+ return result.items;
242
+ }
243
+ /** Find the first record matching the given criteria, or null. */
244
+ async findFirst(args) {
245
+ const rows = await this.findMany({
246
+ ...args,
247
+ take: 1
248
+ });
249
+ return rows[0] ?? null;
250
+ }
251
+ /** Count records matching the given criteria. */
252
+ async count(args) {
253
+ const sql = buildCountQuery(this.recordId, args);
254
+ const result = await executeSuiteQL(this.httpClient, sql, {
255
+ limit: 1
256
+ });
257
+ const row = result.items[0];
258
+ return row ? Number(row.count) : 0;
259
+ }
260
+ };
261
+
262
+ // src/client.ts
263
+ var SuitePortalClient = class {
264
+ httpClient;
265
+ schemaPath;
266
+ schema = null;
267
+ delegates = /* @__PURE__ */ new Map();
268
+ constructor(config, options) {
269
+ this.httpClient = new HttpClient(config);
270
+ this.schemaPath = options?.schemaPath ?? ".suiteportal/schema.json";
271
+ return new Proxy(this, {
272
+ get(target, prop, receiver) {
273
+ if (typeof prop === "string" && prop.startsWith("$")) {
274
+ return Reflect.get(target, prop, receiver);
275
+ }
276
+ if (prop in target || typeof prop === "symbol" || prop === "then") {
277
+ return Reflect.get(target, prop, receiver);
278
+ }
279
+ return target.$delegate(prop);
280
+ }
281
+ });
282
+ }
283
+ /** Execute a raw SuiteQL query. */
284
+ async $queryRaw(sql) {
285
+ const result = await executeSuiteQL2(this.httpClient, sql);
286
+ return result.items;
287
+ }
288
+ /** Clear the schema cache and delegates. */
289
+ $disconnect() {
290
+ this.delegates.clear();
291
+ this.schema = null;
292
+ clearSchemaCache();
293
+ }
294
+ /** Get or create a ModelDelegate for the given record name. */
295
+ $delegate(recordName) {
296
+ const existing = this.delegates.get(recordName);
297
+ if (existing) return existing;
298
+ if (!this.schema) {
299
+ throw new Error(
300
+ "Schema not loaded. Call await client.$loadSchema() before querying, or use createClient() which loads automatically."
301
+ );
302
+ }
303
+ const recordDef = this.schema.records[recordName];
304
+ if (!recordDef) {
305
+ throw new Error(
306
+ `Unknown record type "${recordName}". Available: ${Object.keys(this.schema.records).slice(0, 10).join(", ")}...`
307
+ );
308
+ }
309
+ const delegate = new ModelDelegate(recordName, recordDef, this.httpClient);
310
+ this.delegates.set(recordName, delegate);
311
+ return delegate;
312
+ }
313
+ /** Load the schema from disk. Must be called before accessing record delegates. */
314
+ async $loadSchema() {
315
+ this.schema = await loadSchema(this.schemaPath);
316
+ }
317
+ };
318
+ async function createClient(config, options) {
319
+ const client = new SuitePortalClient(config, options);
320
+ await client.$loadSchema();
321
+ return client;
322
+ }
323
+ export {
324
+ ModelDelegate,
325
+ SuitePortalClient,
326
+ buildCountQuery,
327
+ buildOrderBy,
328
+ buildSelect,
329
+ buildSuiteQL,
330
+ buildWhere,
331
+ clearSchemaCache,
332
+ createClient,
333
+ escapeIdentifier,
334
+ escapeList,
335
+ escapeValue,
336
+ loadSchema
337
+ };
338
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client.ts","../src/schema-loader.ts","../src/model-delegate.ts","../src/query/sql-escape.ts","../src/query/select-builder.ts","../src/query/where-builder.ts","../src/query/orderby-builder.ts","../src/query/query-builder.ts"],"sourcesContent":["import { NetSuiteClient as HttpClient } from '@suiteportal/connector';\nimport { executeSuiteQL } from '@suiteportal/connector';\nimport type { NetSuiteConfig, SuiteQLRow } from '@suiteportal/connector';\nimport type { NormalizedSchema } from '@suiteportal/introspector';\nimport { loadSchema, clearSchemaCache } from './schema-loader.js';\nimport { ModelDelegate } from './model-delegate.js';\n\nexport interface ClientOptions {\n /** Path to schema.json. Default: '.suiteportal/schema.json' */\n schemaPath?: string;\n}\n\n/**\n * The SuitePortal query client. Provides Prisma-like access to NetSuite records\n * via dynamically created ModelDelegate instances.\n *\n * Access records as properties: `client.customer.findMany(...)`.\n * Uses a Proxy to lazily create delegates on first access.\n */\nexport class SuitePortalClient {\n private readonly httpClient: HttpClient;\n private readonly schemaPath: string;\n private schema: NormalizedSchema | null = null;\n private readonly delegates = new Map<string, ModelDelegate>();\n\n constructor(config: NetSuiteConfig, options?: ClientOptions) {\n this.httpClient = new HttpClient(config);\n this.schemaPath = options?.schemaPath ?? '.suiteportal/schema.json';\n\n // Return a Proxy so record names can be accessed as properties\n return new Proxy(this, {\n get(target, prop, receiver) {\n // Known own methods/properties\n if (typeof prop === 'string' && prop.startsWith('$')) {\n return Reflect.get(target, prop, receiver);\n }\n\n // Internal properties and methods\n if (\n prop in target ||\n typeof prop === 'symbol' ||\n prop === 'then' // Prevent Promise-like detection\n ) {\n return Reflect.get(target, prop, receiver);\n }\n\n // Treat as a record name → return a delegate\n return target.$delegate(prop as string);\n },\n });\n }\n\n /** Execute a raw SuiteQL query. */\n async $queryRaw<T = SuiteQLRow>(sql: string): Promise<T[]> {\n const result = await executeSuiteQL<T>(this.httpClient, sql);\n return result.items;\n }\n\n /** Clear the schema cache and delegates. */\n $disconnect(): void {\n this.delegates.clear();\n this.schema = null;\n clearSchemaCache();\n }\n\n /** Get or create a ModelDelegate for the given record name. */\n $delegate(recordName: string): ModelDelegate {\n const existing = this.delegates.get(recordName);\n if (existing) return existing;\n\n if (!this.schema) {\n throw new Error(\n 'Schema not loaded. Call await client.$loadSchema() before querying, ' +\n 'or use createClient() which loads automatically.',\n );\n }\n\n const recordDef = this.schema.records[recordName];\n if (!recordDef) {\n throw new Error(\n `Unknown record type \"${recordName}\". ` +\n `Available: ${Object.keys(this.schema.records).slice(0, 10).join(', ')}...`,\n );\n }\n\n const delegate = new ModelDelegate(recordName, recordDef, this.httpClient);\n this.delegates.set(recordName, delegate);\n return delegate;\n }\n\n /** Load the schema from disk. Must be called before accessing record delegates. */\n async $loadSchema(): Promise<void> {\n this.schema = await loadSchema(this.schemaPath);\n }\n}\n\n/**\n * Create a SuitePortalClient and load the schema.\n * This is the recommended way to create a client.\n */\nexport async function createClient(\n config: NetSuiteConfig,\n options?: ClientOptions,\n): Promise<SuitePortalClient> {\n const client = new SuitePortalClient(config, options);\n await client.$loadSchema();\n return client;\n}\n","import { readFile } from 'node:fs/promises';\nimport type { NormalizedSchema } from '@suiteportal/introspector';\n\nlet cachedSchema: NormalizedSchema | null = null;\nlet cachedPath: string | null = null;\n\n/**\n * Load and parse schema.json from disk. Caches the result so\n * subsequent calls with the same path return immediately.\n */\nexport async function loadSchema(schemaPath: string): Promise<NormalizedSchema> {\n if (cachedSchema && cachedPath === schemaPath) {\n return cachedSchema;\n }\n\n const raw = await readFile(schemaPath, 'utf-8');\n const schema = JSON.parse(raw) as NormalizedSchema;\n\n if (!schema.records || typeof schema.records !== 'object') {\n throw new Error(`Invalid schema: missing \"records\" in ${schemaPath}`);\n }\n\n cachedSchema = schema;\n cachedPath = schemaPath;\n return schema;\n}\n\n/** Clear the cached schema (useful for testing). */\nexport function clearSchemaCache(): void {\n cachedSchema = null;\n cachedPath = null;\n}\n","import type { NetSuiteClient } from '@suiteportal/connector';\nimport { executeSuiteQL } from '@suiteportal/connector';\nimport type { RecordDefinition } from '@suiteportal/introspector';\nimport type { FindManyArgs, FindFirstArgs, CountArgs } from './types.js';\nimport { buildSuiteQL, buildCountQuery } from './query/query-builder.js';\n\n/**\n * A delegate for a single record type. Provides findMany, findFirst, and count.\n */\nexport class ModelDelegate {\n constructor(\n private readonly recordId: string,\n private readonly recordDef: RecordDefinition,\n private readonly httpClient: NetSuiteClient,\n ) {}\n\n /** Find multiple records matching the given criteria. */\n async findMany(args?: FindManyArgs): Promise<Record<string, unknown>[]> {\n const { sql, limit, offset } = buildSuiteQL(\n this.recordId,\n this.recordDef.fields,\n args,\n );\n\n const result = await executeSuiteQL(this.httpClient, sql, { limit, offset });\n return result.items;\n }\n\n /** Find the first record matching the given criteria, or null. */\n async findFirst(args?: FindFirstArgs): Promise<Record<string, unknown> | null> {\n const rows = await this.findMany({\n ...args,\n take: 1,\n });\n return rows[0] ?? null;\n }\n\n /** Count records matching the given criteria. */\n async count(args?: CountArgs): Promise<number> {\n const sql = buildCountQuery(this.recordId, args);\n const result = await executeSuiteQL<{ count: string }>(this.httpClient, sql, {\n limit: 1,\n });\n\n const row = result.items[0];\n return row ? Number(row.count) : 0;\n }\n}\n","/**\n * Escape a value for safe inclusion in a SuiteQL query string.\n * Returns the SQL literal representation (with quotes for strings).\n */\nexport function escapeValue(value: unknown): string {\n if (value === null || value === undefined) {\n return 'NULL';\n }\n\n if (typeof value === 'boolean') {\n return value ? \"'T'\" : \"'F'\";\n }\n\n if (typeof value === 'number') {\n if (!Number.isFinite(value)) {\n throw new Error(`Cannot escape non-finite number: ${value}`);\n }\n return String(value);\n }\n\n if (value instanceof Date) {\n return `'${value.toISOString().slice(0, 10)}'`;\n }\n\n // String — escape single quotes by doubling them\n const str = String(value);\n return `'${str.replace(/'/g, \"''\")}'`;\n}\n\n/**\n * Escape a SQL identifier (column/table name).\n * Wraps in double quotes if it contains special characters or is a reserved word.\n */\nexport function escapeIdentifier(name: string): string {\n // SuiteQL identifiers are case-insensitive and generally safe as-is,\n // but we wrap in double quotes if the name contains anything unusual\n if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {\n return name;\n }\n return `\"${name.replace(/\"/g, '\"\"')}\"`;\n}\n\n/**\n * Format an array of values for an IN clause: (val1, val2, val3)\n */\nexport function escapeList(values: unknown[]): string {\n if (values.length === 0) {\n throw new Error('Cannot create IN clause with empty array');\n }\n return `(${values.map(escapeValue).join(', ')})`;\n}\n","import type { SelectInput } from '../types.js';\nimport type { FieldDefinition } from '@suiteportal/introspector';\nimport { escapeIdentifier } from './sql-escape.js';\n\n/**\n * Build a SQL SELECT column list.\n *\n * - If `select` is provided, emit only the selected fields (plus `id` always).\n * - If `select` is omitted, emit all scalar fields from the record definition.\n */\nexport function buildSelect(\n fields: Record<string, FieldDefinition>,\n select?: SelectInput,\n): string {\n if (select) {\n const columns = Object.entries(select)\n .filter(([, included]) => included)\n .map(([field]) => escapeIdentifier(field));\n\n // Always include id\n if (!columns.includes('id')) {\n columns.unshift('id');\n }\n\n return columns.join(', ');\n }\n\n // Default: all fields\n const columns = Object.keys(fields).map(escapeIdentifier);\n\n // Ensure id is first if it exists\n const idIndex = columns.indexOf('id');\n if (idIndex > 0) {\n columns.splice(idIndex, 1);\n columns.unshift('id');\n }\n\n return columns.length > 0 ? columns.join(', ') : '*';\n}\n","import type { WhereInput, FilterOperator } from '../types.js';\nimport { escapeValue, escapeIdentifier, escapeList } from './sql-escape.js';\n\nconst FILTER_KEYS = new Set([\n 'equals', 'not', 'in', 'notIn', 'gt', 'gte', 'lt', 'lte',\n 'contains', 'startsWith', 'endsWith', 'isNull',\n]);\n\n/** Check if a value looks like a FilterOperator object. */\nfunction isFilterOperator(value: unknown): value is FilterOperator {\n if (value === null || value === undefined || typeof value !== 'object' || Array.isArray(value)) {\n return false;\n }\n return Object.keys(value as object).some((k) => FILTER_KEYS.has(k));\n}\n\n/** Build a single field condition from a FilterOperator. */\nfunction buildFieldCondition(field: string, filter: FilterOperator): string[] {\n const col = escapeIdentifier(field);\n const conditions: string[] = [];\n\n if (filter.equals !== undefined) {\n if (filter.equals === null) {\n conditions.push(`${col} IS NULL`);\n } else {\n conditions.push(`${col} = ${escapeValue(filter.equals)}`);\n }\n }\n\n if (filter.not !== undefined) {\n if (filter.not === null) {\n conditions.push(`${col} IS NOT NULL`);\n } else {\n conditions.push(`${col} != ${escapeValue(filter.not)}`);\n }\n }\n\n if (filter.in !== undefined) {\n conditions.push(`${col} IN ${escapeList(filter.in)}`);\n }\n\n if (filter.notIn !== undefined) {\n conditions.push(`${col} NOT IN ${escapeList(filter.notIn)}`);\n }\n\n if (filter.gt !== undefined) {\n conditions.push(`${col} > ${escapeValue(filter.gt)}`);\n }\n\n if (filter.gte !== undefined) {\n conditions.push(`${col} >= ${escapeValue(filter.gte)}`);\n }\n\n if (filter.lt !== undefined) {\n conditions.push(`${col} < ${escapeValue(filter.lt)}`);\n }\n\n if (filter.lte !== undefined) {\n conditions.push(`${col} <= ${escapeValue(filter.lte)}`);\n }\n\n if (filter.contains !== undefined) {\n conditions.push(`${col} LIKE ${escapeValue(`%${filter.contains}%`)}`);\n }\n\n if (filter.startsWith !== undefined) {\n conditions.push(`${col} LIKE ${escapeValue(`${filter.startsWith}%`)}`);\n }\n\n if (filter.endsWith !== undefined) {\n conditions.push(`${col} LIKE ${escapeValue(`%${filter.endsWith}`)}`);\n }\n\n if (filter.isNull !== undefined) {\n conditions.push(filter.isNull ? `${col} IS NULL` : `${col} IS NOT NULL`);\n }\n\n return conditions;\n}\n\n/**\n * Build a SQL WHERE clause from a WhereInput.\n * Returns the clause without the \"WHERE\" keyword, or empty string if no conditions.\n */\nexport function buildWhere(where?: WhereInput): string {\n if (!where) return '';\n\n const parts: string[] = [];\n\n for (const [key, value] of Object.entries(where)) {\n if (key === 'AND' && Array.isArray(value)) {\n const subClauses = (value as WhereInput[])\n .map((sub) => buildWhere(sub))\n .filter(Boolean);\n if (subClauses.length > 0) {\n parts.push(`(${subClauses.join(' AND ')})`);\n }\n continue;\n }\n\n if (key === 'OR' && Array.isArray(value)) {\n const subClauses = (value as WhereInput[])\n .map((sub) => buildWhere(sub))\n .filter(Boolean);\n if (subClauses.length > 0) {\n parts.push(`(${subClauses.join(' OR ')})`);\n }\n continue;\n }\n\n if (key === 'NOT' && value && typeof value === 'object' && !Array.isArray(value)) {\n const sub = buildWhere(value as WhereInput);\n if (sub) {\n parts.push(`NOT (${sub})`);\n }\n continue;\n }\n\n // Field-level condition\n if (isFilterOperator(value)) {\n parts.push(...buildFieldCondition(key, value));\n } else {\n // Shorthand: { field: value } is equals\n const col = escapeIdentifier(key);\n if (value === null || value === undefined) {\n parts.push(`${col} IS NULL`);\n } else {\n parts.push(`${col} = ${escapeValue(value)}`);\n }\n }\n }\n\n return parts.join(' AND ');\n}\n","import type { OrderByInput } from '../types.js';\nimport { escapeIdentifier } from './sql-escape.js';\n\n/**\n * Build a SQL ORDER BY clause from an OrderByInput.\n * Returns the clause without the \"ORDER BY\" keyword, or empty string if none.\n */\nexport function buildOrderBy(orderBy?: OrderByInput): string {\n if (!orderBy) return '';\n\n const parts = Object.entries(orderBy).map(\n ([field, direction]) => `${escapeIdentifier(field)} ${direction.toUpperCase()}`,\n );\n\n return parts.join(', ');\n}\n","import type { FieldDefinition } from '@suiteportal/introspector';\nimport type { FindManyArgs } from '../types.js';\nimport { buildSelect } from './select-builder.js';\nimport { buildWhere } from './where-builder.js';\nimport { buildOrderBy } from './orderby-builder.js';\n\nexport interface BuiltQuery {\n sql: string;\n limit: number;\n offset: number;\n}\n\n/**\n * Build a complete SuiteQL query from FindManyArgs.\n *\n * The `take` and `skip` values are NOT added to the SQL —\n * they're passed as REST query params (`limit`, `offset`) to the SuiteQL endpoint.\n */\nexport function buildSuiteQL(\n recordId: string,\n fields: Record<string, FieldDefinition>,\n args?: FindManyArgs,\n): BuiltQuery {\n const selectClause = buildSelect(fields, args?.select);\n const whereClause = buildWhere(args?.where);\n const orderByClause = buildOrderBy(args?.orderBy);\n\n let sql = `SELECT ${selectClause} FROM ${recordId}`;\n\n if (whereClause) {\n sql += ` WHERE ${whereClause}`;\n }\n\n if (orderByClause) {\n sql += ` ORDER BY ${orderByClause}`;\n }\n\n return {\n sql,\n limit: args?.take ?? 1000,\n offset: args?.skip ?? 0,\n };\n}\n\n/**\n * Build a SuiteQL COUNT query.\n */\nexport function buildCountQuery(\n recordId: string,\n args?: { where?: FindManyArgs['where'] },\n): string {\n const whereClause = buildWhere(args?.where);\n let sql = `SELECT COUNT(*) AS count FROM ${recordId}`;\n\n if (whereClause) {\n sql += ` WHERE ${whereClause}`;\n }\n\n return sql;\n}\n"],"mappings":"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","names":["executeSuiteQL","columns","executeSuiteQL"]}
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@suiteportal/client-runtime",
3
+ "version": "0.1.0",
4
+ "description": "Prisma-like query runtime for SuitePortal NetSuite ORM",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": ["dist"],
17
+ "scripts": {
18
+ "build": "tsup",
19
+ "clean": "rm -rf dist"
20
+ },
21
+ "dependencies": {
22
+ "@suiteportal/connector": "^0.1.0",
23
+ "@suiteportal/introspector": "^0.1.0"
24
+ },
25
+ "license": "MIT",
26
+ "author": "Trey Hulse",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/treyhulse/netsuite-orm",
30
+ "directory": "packages/client-runtime"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "engines": {
36
+ "node": ">=20.0.0"
37
+ },
38
+ "keywords": ["netsuite", "orm", "prisma", "query-builder", "suiteql"]
39
+ }