@suiteportal/introspector 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 +36 -0
- package/dist/index.cjs +547 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +236 -0
- package/dist/index.d.ts +236 -0
- package/dist/index.js +506 -0
- package/dist/index.js.map +1 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# @suiteportal/introspector
|
|
2
|
+
|
|
3
|
+
NetSuite schema discovery and normalization. Fetches REST metadata catalog and SuiteQL custom records/fields, then produces a unified normalized schema.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @suiteportal/introspector
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { introspect } from '@suiteportal/introspector';
|
|
15
|
+
import { createConnector } from '@suiteportal/connector';
|
|
16
|
+
|
|
17
|
+
const connector = createConnector({ /* credentials */ });
|
|
18
|
+
const schema = await introspect(connector);
|
|
19
|
+
// schema contains all record types, fields, and inferred relations
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Features
|
|
23
|
+
|
|
24
|
+
- 7-step introspection pipeline
|
|
25
|
+
- REST metadata catalog discovery
|
|
26
|
+
- SuiteQL custom record/field enumeration
|
|
27
|
+
- Relation inference between record types
|
|
28
|
+
- Outputs normalized schema to `.suiteportal/schema.json`
|
|
29
|
+
|
|
30
|
+
## Documentation
|
|
31
|
+
|
|
32
|
+
Full docs at [suiteportal.dev](https://suiteportal.dev)
|
|
33
|
+
|
|
34
|
+
## License
|
|
35
|
+
|
|
36
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,547 @@
|
|
|
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
|
+
fetchCatalogIndex: () => fetchCatalogIndex,
|
|
24
|
+
fetchCustomFields: () => fetchCustomFields,
|
|
25
|
+
fetchCustomRecordTypes: () => fetchCustomRecordTypes,
|
|
26
|
+
fetchRecordMetadata: () => fetchRecordMetadata,
|
|
27
|
+
fetchRecordMetadataBatch: () => fetchRecordMetadataBatch,
|
|
28
|
+
getCanonicalHref: () => getCanonicalHref,
|
|
29
|
+
inferRelationsFromCatalog: () => inferRelationsFromCatalog,
|
|
30
|
+
inferRelationsFromConventions: () => inferRelationsFromConventions,
|
|
31
|
+
introspect: () => introspect,
|
|
32
|
+
mapCustomFieldType: () => mapCustomFieldType,
|
|
33
|
+
mapFieldType: () => mapFieldType,
|
|
34
|
+
normalizeSchema: () => normalizeSchema,
|
|
35
|
+
parseFieldSchema: () => parseFieldSchema,
|
|
36
|
+
parseRecordMetadata: () => parseRecordMetadata,
|
|
37
|
+
writeSchemaFiles: () => writeSchemaFiles
|
|
38
|
+
});
|
|
39
|
+
module.exports = __toCommonJS(index_exports);
|
|
40
|
+
|
|
41
|
+
// src/catalog/record-catalog.ts
|
|
42
|
+
var CATALOG_PATH = "/services/rest/record/v1/metadata-catalog";
|
|
43
|
+
async function fetchCatalogIndex(client) {
|
|
44
|
+
const response = await client.request({
|
|
45
|
+
method: "GET",
|
|
46
|
+
path: CATALOG_PATH
|
|
47
|
+
});
|
|
48
|
+
return response.data;
|
|
49
|
+
}
|
|
50
|
+
function getCanonicalHref(links) {
|
|
51
|
+
const canonical = links.find(
|
|
52
|
+
(l) => l.rel === "canonical" && l.mediaType === "application/schema+json"
|
|
53
|
+
);
|
|
54
|
+
if (canonical) return canonical.href;
|
|
55
|
+
const anyCanonical = links.find((l) => l.rel === "canonical");
|
|
56
|
+
if (anyCanonical) return anyCanonical.href;
|
|
57
|
+
return links[0]?.href;
|
|
58
|
+
}
|
|
59
|
+
async function fetchRecordMetadata(client, recordHref) {
|
|
60
|
+
let path;
|
|
61
|
+
if (recordHref.includes("://")) {
|
|
62
|
+
const url = new URL(recordHref);
|
|
63
|
+
path = url.pathname + url.search;
|
|
64
|
+
} else {
|
|
65
|
+
path = recordHref;
|
|
66
|
+
}
|
|
67
|
+
const response = await client.request({
|
|
68
|
+
method: "GET",
|
|
69
|
+
path,
|
|
70
|
+
headers: {
|
|
71
|
+
Accept: "application/schema+json"
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
return response.data;
|
|
75
|
+
}
|
|
76
|
+
async function fetchRecordMetadataBatch(client, catalog, recordTypes) {
|
|
77
|
+
let entries = catalog.items;
|
|
78
|
+
if (recordTypes?.length) {
|
|
79
|
+
const filter = new Set(recordTypes.map((r) => r.toLowerCase()));
|
|
80
|
+
entries = entries.filter((e) => filter.has(e.name.toLowerCase()));
|
|
81
|
+
}
|
|
82
|
+
const results = /* @__PURE__ */ new Map();
|
|
83
|
+
const promises = entries.map(async (entry) => {
|
|
84
|
+
const href = getCanonicalHref(entry.links);
|
|
85
|
+
if (!href) return;
|
|
86
|
+
try {
|
|
87
|
+
const metadata = await fetchRecordMetadata(client, href);
|
|
88
|
+
results.set(entry.name.toLowerCase(), metadata);
|
|
89
|
+
} catch {
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
await Promise.all(promises);
|
|
93
|
+
return results;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/suiteql/custom-records.ts
|
|
97
|
+
var import_connector = require("@suiteportal/connector");
|
|
98
|
+
var QUERY = `
|
|
99
|
+
SELECT
|
|
100
|
+
internalid,
|
|
101
|
+
scriptid,
|
|
102
|
+
name
|
|
103
|
+
FROM customrecordtype
|
|
104
|
+
ORDER BY scriptid
|
|
105
|
+
`;
|
|
106
|
+
async function fetchCustomRecordTypes(client) {
|
|
107
|
+
return (0, import_connector.executeSuiteQLPaginated)(client, QUERY);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/suiteql/custom-fields.ts
|
|
111
|
+
var import_connector2 = require("@suiteportal/connector");
|
|
112
|
+
function buildQuery() {
|
|
113
|
+
return `
|
|
114
|
+
SELECT
|
|
115
|
+
internalid,
|
|
116
|
+
scriptid,
|
|
117
|
+
fieldtype,
|
|
118
|
+
name,
|
|
119
|
+
ismandatory,
|
|
120
|
+
recordtype
|
|
121
|
+
FROM customfield
|
|
122
|
+
ORDER BY scriptid
|
|
123
|
+
`;
|
|
124
|
+
}
|
|
125
|
+
async function fetchCustomFields(client) {
|
|
126
|
+
return (0, import_connector2.executeSuiteQLPaginated)(client, buildQuery());
|
|
127
|
+
}
|
|
128
|
+
function mapCustomFieldType(fieldType) {
|
|
129
|
+
const map = {
|
|
130
|
+
CHECKBOX: "boolean",
|
|
131
|
+
CLOBTEXT: "text",
|
|
132
|
+
CURRENCY: "currency",
|
|
133
|
+
DATE: "date",
|
|
134
|
+
DATETIMETZ: "datetime",
|
|
135
|
+
DECIMALNUM: "float",
|
|
136
|
+
EMAIL: "email",
|
|
137
|
+
FLOAT: "float",
|
|
138
|
+
HELP: "richtext",
|
|
139
|
+
IMAGE: "url",
|
|
140
|
+
INLINEHTML: "richtext",
|
|
141
|
+
INTEGER: "integer",
|
|
142
|
+
LIST: "select",
|
|
143
|
+
MULTISELECT: "multiselect",
|
|
144
|
+
PASSWORD: "string",
|
|
145
|
+
PERCENT: "percent",
|
|
146
|
+
PHONE: "phone",
|
|
147
|
+
RECORD: "select",
|
|
148
|
+
RICHTEXT: "richtext",
|
|
149
|
+
SCRIPT: "string",
|
|
150
|
+
SELECT: "select",
|
|
151
|
+
TEXT: "string",
|
|
152
|
+
TEXTAREA: "text",
|
|
153
|
+
TIMEOFDAY: "string",
|
|
154
|
+
URL: "url"
|
|
155
|
+
};
|
|
156
|
+
return map[fieldType.toUpperCase()] ?? "unknown";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/catalog/catalog-parser.ts
|
|
160
|
+
var REFERENCE_FIELD_KEYS = /* @__PURE__ */ new Set(["id", "refName", "externalId", "links"]);
|
|
161
|
+
function isReferenceField(schema) {
|
|
162
|
+
if (schema.type !== "object" || !schema.properties) return false;
|
|
163
|
+
const keys = Object.keys(schema.properties);
|
|
164
|
+
return keys.length <= 4 && keys.every((k) => REFERENCE_FIELD_KEYS.has(k));
|
|
165
|
+
}
|
|
166
|
+
function isSublist(schema) {
|
|
167
|
+
if (schema.type !== "object" || !schema.properties) return false;
|
|
168
|
+
return "totalResults" in schema.properties || "count" in schema.properties || "hasMore" in schema.properties;
|
|
169
|
+
}
|
|
170
|
+
function parseRecordMetadata(name, metadata) {
|
|
171
|
+
const fields = {};
|
|
172
|
+
const relations = {};
|
|
173
|
+
const sublists = {};
|
|
174
|
+
if (metadata.properties) {
|
|
175
|
+
for (const [fieldId, schema] of Object.entries(metadata.properties)) {
|
|
176
|
+
if (fieldId === "links" || fieldId === "_links") continue;
|
|
177
|
+
if (isReferenceField(schema)) {
|
|
178
|
+
fields[fieldId] = {
|
|
179
|
+
id: fieldId,
|
|
180
|
+
label: schema.title ?? fieldId,
|
|
181
|
+
type: "select",
|
|
182
|
+
required: schema.nullable === false,
|
|
183
|
+
readOnly: schema.readOnly === true,
|
|
184
|
+
nativeType: "reference"
|
|
185
|
+
};
|
|
186
|
+
} else if (isSublist(schema)) {
|
|
187
|
+
sublists[fieldId] = parseSublistSchema(fieldId, schema);
|
|
188
|
+
} else {
|
|
189
|
+
fields[fieldId] = parseFieldSchema(fieldId, schema);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
id: name.toLowerCase(),
|
|
195
|
+
label: metadata.label ?? metadata.name ?? name,
|
|
196
|
+
isCustom: name.startsWith("customrecord"),
|
|
197
|
+
fields,
|
|
198
|
+
relations,
|
|
199
|
+
sublists
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function parseFieldSchema(fieldId, schema) {
|
|
203
|
+
const type = mapFieldType(schema);
|
|
204
|
+
const def = {
|
|
205
|
+
id: fieldId,
|
|
206
|
+
label: schema.title ?? fieldId,
|
|
207
|
+
type,
|
|
208
|
+
required: schema.nullable === false,
|
|
209
|
+
readOnly: schema.readOnly === true,
|
|
210
|
+
nativeType: schema.type ?? schema.format ?? "unknown"
|
|
211
|
+
};
|
|
212
|
+
if (schema.enum && schema.enum.length > 0) {
|
|
213
|
+
def.enumValues = schema.enum.map((v) => ({ value: v, label: v }));
|
|
214
|
+
}
|
|
215
|
+
return def;
|
|
216
|
+
}
|
|
217
|
+
function mapFieldType(schema) {
|
|
218
|
+
if (schema.type === "object" && schema.properties) {
|
|
219
|
+
if (isReferenceField(schema)) return "select";
|
|
220
|
+
return "unknown";
|
|
221
|
+
}
|
|
222
|
+
if (schema.enum) return "select";
|
|
223
|
+
const type = schema.type?.toLowerCase();
|
|
224
|
+
const format = schema.format?.toLowerCase();
|
|
225
|
+
if (format === "date") return "date";
|
|
226
|
+
if (format === "date-time") return "datetime";
|
|
227
|
+
if (format === "email") return "email";
|
|
228
|
+
if (format === "uri" || format === "url") return "url";
|
|
229
|
+
if (format === "phone") return "phone";
|
|
230
|
+
if (format === "currency") return "currency";
|
|
231
|
+
if (format === "percent") return "percent";
|
|
232
|
+
if (format === "rich-text" || format === "richtext") return "richtext";
|
|
233
|
+
if (format === "float" || format === "double") return "float";
|
|
234
|
+
if (format === "int64" || format === "int32") return "integer";
|
|
235
|
+
if (type === "string") return "string";
|
|
236
|
+
if (type === "integer") return "integer";
|
|
237
|
+
if (type === "number") return "float";
|
|
238
|
+
if (type === "boolean") return "boolean";
|
|
239
|
+
if (type === "array") return "multiselect";
|
|
240
|
+
return "unknown";
|
|
241
|
+
}
|
|
242
|
+
function parseSublistSchema(sublistId, schema) {
|
|
243
|
+
const fields = {};
|
|
244
|
+
const itemsSchema = schema.properties?.["items"];
|
|
245
|
+
const propsSource = itemsSchema?.properties ?? schema.properties;
|
|
246
|
+
if (propsSource) {
|
|
247
|
+
for (const [fieldId, fieldSchema] of Object.entries(propsSource)) {
|
|
248
|
+
if (["links", "_links", "totalResults", "count", "hasMore", "offset", "items"].includes(fieldId)) continue;
|
|
249
|
+
if (isReferenceField(fieldSchema)) {
|
|
250
|
+
fields[fieldId] = {
|
|
251
|
+
id: fieldId,
|
|
252
|
+
label: fieldSchema.title ?? fieldId,
|
|
253
|
+
type: "select",
|
|
254
|
+
required: fieldSchema.nullable === false,
|
|
255
|
+
readOnly: fieldSchema.readOnly === true,
|
|
256
|
+
nativeType: "reference"
|
|
257
|
+
};
|
|
258
|
+
} else if (!isSublist(fieldSchema)) {
|
|
259
|
+
fields[fieldId] = parseFieldSchema(fieldId, fieldSchema);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
id: sublistId,
|
|
265
|
+
label: schema.title ?? sublistId,
|
|
266
|
+
fields
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// src/schema/normalizer.ts
|
|
271
|
+
function normalizeSchema(accountId, catalogRecords, customRecordTypes, customFields) {
|
|
272
|
+
const records = {};
|
|
273
|
+
for (const [name, metadata] of catalogRecords) {
|
|
274
|
+
records[name] = parseRecordMetadata(name, metadata);
|
|
275
|
+
}
|
|
276
|
+
for (const crt of customRecordTypes) {
|
|
277
|
+
const key = crt.scriptid.toLowerCase();
|
|
278
|
+
if (!records[key]) {
|
|
279
|
+
records[key] = {
|
|
280
|
+
id: key,
|
|
281
|
+
label: crt.name,
|
|
282
|
+
isCustom: true,
|
|
283
|
+
fields: {},
|
|
284
|
+
relations: {},
|
|
285
|
+
sublists: {}
|
|
286
|
+
};
|
|
287
|
+
} else {
|
|
288
|
+
if (!records[key].label || records[key].label === key) {
|
|
289
|
+
records[key].label = crt.name;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
const fieldsByPrefix = groupCustomFieldsByPrefix(customFields);
|
|
294
|
+
for (const [prefix, fields] of fieldsByPrefix) {
|
|
295
|
+
mergeCustomFields(records, prefix, fields);
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
299
|
+
accountId,
|
|
300
|
+
records
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function groupCustomFieldsByPrefix(fields) {
|
|
304
|
+
const groups = /* @__PURE__ */ new Map();
|
|
305
|
+
for (const field of fields) {
|
|
306
|
+
const scriptId = field.scriptid.toLowerCase();
|
|
307
|
+
let prefix;
|
|
308
|
+
if (scriptId.startsWith("custbody")) prefix = "custbody";
|
|
309
|
+
else if (scriptId.startsWith("custcol")) prefix = "custcol";
|
|
310
|
+
else if (scriptId.startsWith("custrecord")) prefix = "custrecord";
|
|
311
|
+
else if (scriptId.startsWith("custentity")) prefix = "custentity";
|
|
312
|
+
else if (scriptId.startsWith("custitem")) prefix = "custitem";
|
|
313
|
+
else if (scriptId.startsWith("custevent")) prefix = "custevent";
|
|
314
|
+
else prefix = "other";
|
|
315
|
+
const existing = groups.get(prefix);
|
|
316
|
+
if (existing) {
|
|
317
|
+
existing.push(field);
|
|
318
|
+
} else {
|
|
319
|
+
groups.set(prefix, [field]);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return groups;
|
|
323
|
+
}
|
|
324
|
+
function mergeCustomFields(records, prefix, fields) {
|
|
325
|
+
const targetRecords = getTargetRecordsForPrefix(prefix, records);
|
|
326
|
+
for (const field of fields) {
|
|
327
|
+
const fieldDef = customFieldToFieldDefinition(field);
|
|
328
|
+
for (const record of targetRecords) {
|
|
329
|
+
if (!record.fields[fieldDef.id]) {
|
|
330
|
+
record.fields[fieldDef.id] = fieldDef;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
function getTargetRecordsForPrefix(prefix, records) {
|
|
336
|
+
const targets = [];
|
|
337
|
+
const entityTypes = ["customer", "vendor", "employee", "partner", "contact", "lead", "prospect"];
|
|
338
|
+
const transactionTypes = ["salesorder", "purchaseorder", "invoice", "vendorbill", "estimate", "opportunity"];
|
|
339
|
+
const itemTypes = ["inventoryitem", "noninventoryitem", "serviceitem", "kititem", "assemblyitem"];
|
|
340
|
+
let typeList;
|
|
341
|
+
switch (prefix) {
|
|
342
|
+
case "custentity":
|
|
343
|
+
typeList = entityTypes;
|
|
344
|
+
break;
|
|
345
|
+
case "custbody":
|
|
346
|
+
case "custcol":
|
|
347
|
+
typeList = transactionTypes;
|
|
348
|
+
break;
|
|
349
|
+
case "custitem":
|
|
350
|
+
typeList = itemTypes;
|
|
351
|
+
break;
|
|
352
|
+
case "custrecord":
|
|
353
|
+
return [];
|
|
354
|
+
default:
|
|
355
|
+
return [];
|
|
356
|
+
}
|
|
357
|
+
for (const typeName of typeList) {
|
|
358
|
+
if (records[typeName]) {
|
|
359
|
+
targets.push(records[typeName]);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return targets;
|
|
363
|
+
}
|
|
364
|
+
function customFieldToFieldDefinition(field) {
|
|
365
|
+
return {
|
|
366
|
+
id: field.scriptid.toLowerCase(),
|
|
367
|
+
label: field.name,
|
|
368
|
+
type: mapCustomFieldType(field.fieldtype),
|
|
369
|
+
required: field.ismandatory === "T",
|
|
370
|
+
readOnly: false,
|
|
371
|
+
nativeType: field.fieldtype
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// src/schema/relations.ts
|
|
376
|
+
var KNOWN_FIELD_MAPPINGS = {
|
|
377
|
+
entity: "customer",
|
|
378
|
+
customer: "customer",
|
|
379
|
+
vendor: "vendor",
|
|
380
|
+
employee: "employee",
|
|
381
|
+
partner: "partner",
|
|
382
|
+
contact: "contact",
|
|
383
|
+
subsidiary: "subsidiary",
|
|
384
|
+
department: "department",
|
|
385
|
+
class: "classification",
|
|
386
|
+
location: "location",
|
|
387
|
+
currency: "currency",
|
|
388
|
+
terms: "term",
|
|
389
|
+
salesrep: "employee",
|
|
390
|
+
nexus: "nexus",
|
|
391
|
+
account: "account",
|
|
392
|
+
item: "item",
|
|
393
|
+
units: "unitstype",
|
|
394
|
+
parent: "",
|
|
395
|
+
// parent references same record type
|
|
396
|
+
createdby: "employee",
|
|
397
|
+
lastmodifiedby: "employee",
|
|
398
|
+
territory: "territory",
|
|
399
|
+
leadsource: "leadsource",
|
|
400
|
+
pricelevel: "pricelevel",
|
|
401
|
+
entitystatus: "entitystatus",
|
|
402
|
+
customform: "customform",
|
|
403
|
+
defaultbankaccount: "account",
|
|
404
|
+
openingbalanceaccount: "account",
|
|
405
|
+
receivablesaccount: "account",
|
|
406
|
+
shippingitem: "item",
|
|
407
|
+
image: "file",
|
|
408
|
+
toplevelparent: "customer",
|
|
409
|
+
category: "category"
|
|
410
|
+
};
|
|
411
|
+
function inferRelationsFromCatalog(schema, _catalogRecords) {
|
|
412
|
+
inferRelationsFromConventions(schema);
|
|
413
|
+
}
|
|
414
|
+
function inferRelationsFromConventions(schema) {
|
|
415
|
+
for (const [recordName, record] of Object.entries(schema.records)) {
|
|
416
|
+
for (const [fieldId, field] of Object.entries(record.fields)) {
|
|
417
|
+
if (field.type !== "select" || field.nativeType !== "reference") continue;
|
|
418
|
+
if (record.relations[fieldId]) continue;
|
|
419
|
+
const targetType = KNOWN_FIELD_MAPPINGS[fieldId.toLowerCase()];
|
|
420
|
+
if (targetType === void 0) continue;
|
|
421
|
+
const target = targetType === "" ? recordName : targetType;
|
|
422
|
+
if (schema.records[target]) {
|
|
423
|
+
addRelation(record, {
|
|
424
|
+
name: fieldId,
|
|
425
|
+
type: "many-to-one",
|
|
426
|
+
target,
|
|
427
|
+
foreignKey: fieldId
|
|
428
|
+
});
|
|
429
|
+
if (target !== recordName) {
|
|
430
|
+
const targetRecord = schema.records[target];
|
|
431
|
+
if (targetRecord) {
|
|
432
|
+
addRelation(targetRecord, {
|
|
433
|
+
name: `${recordName}_via_${fieldId}`,
|
|
434
|
+
type: "one-to-many",
|
|
435
|
+
target: recordName,
|
|
436
|
+
foreignKey: fieldId
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
function addRelation(record, relation) {
|
|
445
|
+
if (!record.relations[relation.name]) {
|
|
446
|
+
record.relations[relation.name] = relation;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// src/schema/writer.ts
|
|
451
|
+
var import_promises = require("fs/promises");
|
|
452
|
+
var import_node_path = require("path");
|
|
453
|
+
async function writeSchemaFiles(outputDir, schema, rawMetadata) {
|
|
454
|
+
await (0, import_promises.mkdir)(outputDir, { recursive: true });
|
|
455
|
+
const schemaPath = (0, import_node_path.join)(outputDir, "schema.json");
|
|
456
|
+
const relationsPath = (0, import_node_path.join)(outputDir, "relations.json");
|
|
457
|
+
const rawPath = (0, import_node_path.join)(outputDir, "raw-metadata.json");
|
|
458
|
+
const relationsMap = {};
|
|
459
|
+
for (const [name, record] of Object.entries(schema.records)) {
|
|
460
|
+
if (Object.keys(record.relations).length > 0) {
|
|
461
|
+
relationsMap[name] = record.relations;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
await Promise.all([
|
|
465
|
+
(0, import_promises.writeFile)(schemaPath, JSON.stringify(schema, null, 2), "utf-8"),
|
|
466
|
+
(0, import_promises.writeFile)(relationsPath, JSON.stringify(relationsMap, null, 2), "utf-8"),
|
|
467
|
+
(0, import_promises.writeFile)(
|
|
468
|
+
rawPath,
|
|
469
|
+
JSON.stringify(Object.fromEntries(rawMetadata), null, 2),
|
|
470
|
+
"utf-8"
|
|
471
|
+
)
|
|
472
|
+
]);
|
|
473
|
+
return { schemaPath, relationsPath, rawPath };
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// src/introspect.ts
|
|
477
|
+
async function introspect(client, accountId, options, onProgress) {
|
|
478
|
+
const outputDir = options?.outputDir ?? ".suiteportal";
|
|
479
|
+
const fetchDetails = options?.fetchDetails ?? true;
|
|
480
|
+
const includeCustomRecords = options?.includeCustomRecords ?? true;
|
|
481
|
+
const log = onProgress ?? (() => {
|
|
482
|
+
});
|
|
483
|
+
log("Fetching record catalog index...");
|
|
484
|
+
const catalog = await fetchCatalogIndex(client);
|
|
485
|
+
log(`Found ${catalog.items.length} record types in catalog`);
|
|
486
|
+
let catalogRecords = /* @__PURE__ */ new Map();
|
|
487
|
+
if (fetchDetails) {
|
|
488
|
+
log("Fetching detailed record metadata...");
|
|
489
|
+
catalogRecords = await fetchRecordMetadataBatch(client, catalog, options?.recordTypes);
|
|
490
|
+
log(`Fetched metadata for ${catalogRecords.size} records`);
|
|
491
|
+
}
|
|
492
|
+
let customRecordTypes = [];
|
|
493
|
+
let customFields = [];
|
|
494
|
+
if (includeCustomRecords) {
|
|
495
|
+
log("Fetching custom record types via SuiteQL...");
|
|
496
|
+
const [crt, cf] = await Promise.all([
|
|
497
|
+
fetchCustomRecordTypes(client),
|
|
498
|
+
fetchCustomFields(client)
|
|
499
|
+
]);
|
|
500
|
+
customRecordTypes = crt;
|
|
501
|
+
customFields = cf;
|
|
502
|
+
log(`Found ${customRecordTypes.length} custom record types and ${customFields.length} custom fields`);
|
|
503
|
+
}
|
|
504
|
+
log("Normalizing schema...");
|
|
505
|
+
const schema = normalizeSchema(accountId, catalogRecords, customRecordTypes, customFields);
|
|
506
|
+
log("Inferring relationships...");
|
|
507
|
+
inferRelationsFromCatalog(schema, catalogRecords);
|
|
508
|
+
inferRelationsFromConventions(schema);
|
|
509
|
+
log(`Writing schema files to ${outputDir}/...`);
|
|
510
|
+
const files = await writeSchemaFiles(outputDir, schema, catalogRecords);
|
|
511
|
+
let totalFields = 0;
|
|
512
|
+
let totalRelations = 0;
|
|
513
|
+
for (const record of Object.values(schema.records)) {
|
|
514
|
+
totalFields += Object.keys(record.fields).length;
|
|
515
|
+
totalRelations += Object.keys(record.relations).length;
|
|
516
|
+
}
|
|
517
|
+
return {
|
|
518
|
+
schema,
|
|
519
|
+
files,
|
|
520
|
+
stats: {
|
|
521
|
+
totalRecords: Object.keys(schema.records).length,
|
|
522
|
+
totalFields,
|
|
523
|
+
totalRelations,
|
|
524
|
+
customRecordTypes: customRecordTypes.length,
|
|
525
|
+
customFields: customFields.length
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
530
|
+
0 && (module.exports = {
|
|
531
|
+
fetchCatalogIndex,
|
|
532
|
+
fetchCustomFields,
|
|
533
|
+
fetchCustomRecordTypes,
|
|
534
|
+
fetchRecordMetadata,
|
|
535
|
+
fetchRecordMetadataBatch,
|
|
536
|
+
getCanonicalHref,
|
|
537
|
+
inferRelationsFromCatalog,
|
|
538
|
+
inferRelationsFromConventions,
|
|
539
|
+
introspect,
|
|
540
|
+
mapCustomFieldType,
|
|
541
|
+
mapFieldType,
|
|
542
|
+
normalizeSchema,
|
|
543
|
+
parseFieldSchema,
|
|
544
|
+
parseRecordMetadata,
|
|
545
|
+
writeSchemaFiles
|
|
546
|
+
});
|
|
547
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/catalog/record-catalog.ts","../src/suiteql/custom-records.ts","../src/suiteql/custom-fields.ts","../src/catalog/catalog-parser.ts","../src/schema/normalizer.ts","../src/schema/relations.ts","../src/schema/writer.ts","../src/introspect.ts"],"sourcesContent":["// Pipeline\nexport { introspect } from './introspect.js';\nexport type { IntrospectResult } from './introspect.js';\n\n// Catalog\nexport { fetchCatalogIndex, fetchRecordMetadata, fetchRecordMetadataBatch, getCanonicalHref } from './catalog/record-catalog.js';\nexport { parseRecordMetadata, parseFieldSchema, mapFieldType } from './catalog/catalog-parser.js';\n\n// SuiteQL discovery\nexport { fetchCustomRecordTypes } from './suiteql/custom-records.js';\nexport { fetchCustomFields, mapCustomFieldType } from './suiteql/custom-fields.js';\n\n// Schema\nexport { normalizeSchema } from './schema/normalizer.js';\nexport { inferRelationsFromCatalog, inferRelationsFromConventions } from './schema/relations.js';\nexport { writeSchemaFiles } from './schema/writer.js';\n\n// Types\nexport type {\n NormalizedSchema,\n RecordDefinition,\n FieldDefinition,\n FieldType,\n RelationDefinition,\n RelationType,\n SublistDefinition,\n CatalogIndex,\n CatalogLink,\n CatalogRecordMetadata,\n CatalogFieldSchema,\n CatalogSublistSchema,\n CustomRecordTypeRow,\n CustomFieldRow,\n IntrospectOptions,\n} from './types.js';\n","import type { NetSuiteClient } from '@suiteportal/connector';\nimport type { CatalogIndex, CatalogLink, CatalogRecordMetadata } from '../types.js';\n\nconst CATALOG_PATH = '/services/rest/record/v1/metadata-catalog';\n\n/**\n * Fetch the top-level record catalog index.\n * Returns a list of all record types with their metadata URLs.\n */\nexport async function fetchCatalogIndex(client: NetSuiteClient): Promise<CatalogIndex> {\n const response = await client.request<CatalogIndex>({\n method: 'GET',\n path: CATALOG_PATH,\n });\n return response.data;\n}\n\n/**\n * Extract the canonical schema href from a links array.\n * Falls back to the first link if no canonical is found.\n */\nexport function getCanonicalHref(links: CatalogLink[]): string | undefined {\n const canonical = links.find(\n (l) => l.rel === 'canonical' && l.mediaType === 'application/schema+json',\n );\n if (canonical) return canonical.href;\n\n // Fallback: any canonical link\n const anyCanonical = links.find((l) => l.rel === 'canonical');\n if (anyCanonical) return anyCanonical.href;\n\n // Last resort: first link\n return links[0]?.href;\n}\n\n/**\n * Fetch detailed metadata for a single record type.\n * The href comes from the catalog index (relative or absolute).\n */\nexport async function fetchRecordMetadata(\n client: NetSuiteClient,\n recordHref: string,\n): Promise<CatalogRecordMetadata> {\n // The href from the catalog may be absolute or relative.\n // If it contains \"://\" it's absolute — extract the path portion.\n let path: string;\n if (recordHref.includes('://')) {\n const url = new URL(recordHref);\n path = url.pathname + url.search;\n } else {\n path = recordHref;\n }\n\n const response = await client.request<CatalogRecordMetadata>({\n method: 'GET',\n path,\n headers: {\n Accept: 'application/schema+json',\n },\n });\n return response.data;\n}\n\n/**\n * Fetch metadata for multiple record types in parallel.\n * Filters the catalog index by the given names (or fetches all if not specified).\n */\nexport async function fetchRecordMetadataBatch(\n client: NetSuiteClient,\n catalog: CatalogIndex,\n recordTypes?: string[],\n): Promise<Map<string, CatalogRecordMetadata>> {\n let entries = catalog.items;\n if (recordTypes?.length) {\n const filter = new Set(recordTypes.map((r) => r.toLowerCase()));\n entries = entries.filter((e) => filter.has(e.name.toLowerCase()));\n }\n\n const results = new Map<string, CatalogRecordMetadata>();\n const promises = entries.map(async (entry) => {\n const href = getCanonicalHref(entry.links);\n if (!href) return; // Skip entries without a usable link\n\n try {\n const metadata = await fetchRecordMetadata(client, href);\n results.set(entry.name.toLowerCase(), metadata);\n } catch {\n // Individual record fetch failures shouldn't abort the whole batch.\n // Some record types may not be accessible.\n }\n });\n\n await Promise.all(promises);\n return results;\n}\n","import { executeSuiteQLPaginated, type NetSuiteClient } from '@suiteportal/connector';\nimport type { CustomRecordTypeRow } from '../types.js';\n\nconst QUERY = `\n SELECT\n internalid,\n scriptid,\n name\n FROM customrecordtype\n ORDER BY scriptid\n`;\n\n/**\n * Fetch all custom record type definitions via SuiteQL.\n */\nexport async function fetchCustomRecordTypes(\n client: NetSuiteClient,\n): Promise<CustomRecordTypeRow[]> {\n return executeSuiteQLPaginated<CustomRecordTypeRow>(client, QUERY);\n}\n","import { executeSuiteQLPaginated, type NetSuiteClient } from '@suiteportal/connector';\nimport type { CustomFieldRow } from '../types.js';\n\n/**\n * Build the SuiteQL query for custom fields.\n * Available columns: internalid, scriptid, fieldtype, name, description,\n * ismandatory, owner, recordtype, id.\n */\nfunction buildQuery(): string {\n return `\n SELECT\n internalid,\n scriptid,\n fieldtype,\n name,\n ismandatory,\n recordtype\n FROM customfield\n ORDER BY scriptid\n `;\n}\n\n/**\n * Fetch all custom field definitions via SuiteQL.\n */\nexport async function fetchCustomFields(\n client: NetSuiteClient,\n): Promise<CustomFieldRow[]> {\n return executeSuiteQLPaginated<CustomFieldRow>(client, buildQuery());\n}\n\n/**\n * Map a NetSuite custom field type string to our normalized FieldType.\n */\nexport function mapCustomFieldType(fieldType: string): string {\n const map: Record<string, string> = {\n CHECKBOX: 'boolean',\n CLOBTEXT: 'text',\n CURRENCY: 'currency',\n DATE: 'date',\n DATETIMETZ: 'datetime',\n DECIMALNUM: 'float',\n EMAIL: 'email',\n FLOAT: 'float',\n HELP: 'richtext',\n IMAGE: 'url',\n INLINEHTML: 'richtext',\n INTEGER: 'integer',\n LIST: 'select',\n MULTISELECT: 'multiselect',\n PASSWORD: 'string',\n PERCENT: 'percent',\n PHONE: 'phone',\n RECORD: 'select',\n RICHTEXT: 'richtext',\n SCRIPT: 'string',\n SELECT: 'select',\n TEXT: 'string',\n TEXTAREA: 'text',\n TIMEOFDAY: 'string',\n URL: 'url',\n };\n return map[fieldType.toUpperCase()] ?? 'unknown';\n}\n","import type {\n CatalogRecordMetadata,\n CatalogFieldSchema,\n RecordDefinition,\n FieldDefinition,\n FieldType,\n SublistDefinition,\n RelationDefinition,\n} from '../types.js';\n\n/**\n * Reference fields in the NetSuite catalog are `type: \"object\"` with exactly\n * these sub-properties: { id, refName, externalId, links }.\n * This distinguishes them from sublists which have { totalResults, count, hasMore, ... }.\n */\nconst REFERENCE_FIELD_KEYS = new Set(['id', 'refName', 'externalId', 'links']);\n\nfunction isReferenceField(schema: CatalogFieldSchema): boolean {\n if (schema.type !== 'object' || !schema.properties) return false;\n const keys = Object.keys(schema.properties);\n // A reference field has a subset of { id, refName, externalId, links }\n return keys.length <= 4 && keys.every((k) => REFERENCE_FIELD_KEYS.has(k));\n}\n\nfunction isSublist(schema: CatalogFieldSchema): boolean {\n if (schema.type !== 'object' || !schema.properties) return false;\n // Sublists have pagination keys: totalResults, count, hasMore\n return 'totalResults' in schema.properties || 'count' in schema.properties || 'hasMore' in schema.properties;\n}\n\n/**\n * Parse a single record's catalog metadata into a RecordDefinition.\n * Handles the real NetSuite JSON Schema format where:\n * - Scalar fields have `type: \"string\" | \"integer\" | \"number\" | \"boolean\"`\n * - Reference fields are `type: \"object\"` with { id, refName, externalId, links }\n * - Sublists are `type: \"object\"` with { totalResults, count, hasMore, items, ... }\n * - Enums are `type: \"object\"` with { id, refName } (same as refs, detected by convention)\n */\nexport function parseRecordMetadata(\n name: string,\n metadata: CatalogRecordMetadata,\n): RecordDefinition {\n const fields: Record<string, FieldDefinition> = {};\n const relations: Record<string, RelationDefinition> = {};\n const sublists: Record<string, SublistDefinition> = {};\n\n if (metadata.properties) {\n for (const [fieldId, schema] of Object.entries(metadata.properties)) {\n // Skip link/navigation properties\n if (fieldId === 'links' || fieldId === '_links') continue;\n\n if (isReferenceField(schema)) {\n // It's a reference to another record — treat as a select field + relation\n fields[fieldId] = {\n id: fieldId,\n label: schema.title ?? fieldId,\n type: 'select',\n required: schema.nullable === false,\n readOnly: schema.readOnly === true,\n nativeType: 'reference',\n };\n // We can't know the target record type from the schema alone,\n // so relation inference is handled separately via conventions.\n } else if (isSublist(schema)) {\n sublists[fieldId] = parseSublistSchema(fieldId, schema);\n } else {\n fields[fieldId] = parseFieldSchema(fieldId, schema);\n }\n }\n }\n\n return {\n id: name.toLowerCase(),\n label: metadata.label ?? metadata.name ?? name,\n isCustom: name.startsWith('customrecord'),\n fields,\n relations,\n sublists,\n };\n}\n\n/** Parse a catalog field schema into a FieldDefinition. */\nexport function parseFieldSchema(fieldId: string, schema: CatalogFieldSchema): FieldDefinition {\n const type = mapFieldType(schema);\n\n const def: FieldDefinition = {\n id: fieldId,\n label: schema.title ?? fieldId,\n type,\n required: schema.nullable === false,\n readOnly: schema.readOnly === true,\n nativeType: schema.type ?? schema.format ?? 'unknown',\n };\n\n if (schema.enum && schema.enum.length > 0) {\n def.enumValues = schema.enum.map((v) => ({ value: v, label: v }));\n }\n\n return def;\n}\n\n/** Map NetSuite catalog type/format to our normalized FieldType. */\nexport function mapFieldType(schema: CatalogFieldSchema): FieldType {\n // Object with { id, refName } = reference/select\n if (schema.type === 'object' && schema.properties) {\n if (isReferenceField(schema)) return 'select';\n return 'unknown';\n }\n\n if (schema.enum) return 'select';\n\n const type = schema.type?.toLowerCase();\n const format = schema.format?.toLowerCase();\n\n if (format === 'date') return 'date';\n if (format === 'date-time') return 'datetime';\n if (format === 'email') return 'email';\n if (format === 'uri' || format === 'url') return 'url';\n if (format === 'phone') return 'phone';\n if (format === 'currency') return 'currency';\n if (format === 'percent') return 'percent';\n if (format === 'rich-text' || format === 'richtext') return 'richtext';\n if (format === 'float' || format === 'double') return 'float';\n if (format === 'int64' || format === 'int32') return 'integer';\n\n if (type === 'string') return 'string';\n if (type === 'integer') return 'integer';\n if (type === 'number') return 'float';\n if (type === 'boolean') return 'boolean';\n if (type === 'array') return 'multiselect';\n\n return 'unknown';\n}\n\n/** Parse a sublist schema into a SublistDefinition. */\nfunction parseSublistSchema(sublistId: string, schema: CatalogFieldSchema): SublistDefinition {\n const fields: Record<string, FieldDefinition> = {};\n\n // Sublist items are nested under \"items.properties\" or sometimes \"properties.items.properties\"\n const itemsSchema = schema.properties?.['items'];\n const propsSource = itemsSchema?.properties ?? schema.properties;\n\n if (propsSource) {\n for (const [fieldId, fieldSchema] of Object.entries(propsSource)) {\n // Skip pagination and link fields\n if (['links', '_links', 'totalResults', 'count', 'hasMore', 'offset', 'items'].includes(fieldId)) continue;\n\n if (isReferenceField(fieldSchema)) {\n fields[fieldId] = {\n id: fieldId,\n label: fieldSchema.title ?? fieldId,\n type: 'select',\n required: fieldSchema.nullable === false,\n readOnly: fieldSchema.readOnly === true,\n nativeType: 'reference',\n };\n } else if (!isSublist(fieldSchema)) {\n fields[fieldId] = parseFieldSchema(fieldId, fieldSchema);\n }\n }\n }\n\n return {\n id: sublistId,\n label: schema.title ?? sublistId,\n fields,\n };\n}\n","import type {\n NormalizedSchema,\n RecordDefinition,\n CatalogRecordMetadata,\n CustomRecordTypeRow,\n CustomFieldRow,\n FieldDefinition,\n FieldType,\n} from '../types.js';\nimport { parseRecordMetadata } from '../catalog/catalog-parser.js';\nimport { mapCustomFieldType } from '../suiteql/custom-fields.js';\n\n/**\n * Merge REST catalog metadata and SuiteQL custom record/field data\n * into a unified NormalizedSchema.\n */\nexport function normalizeSchema(\n accountId: string,\n catalogRecords: Map<string, CatalogRecordMetadata>,\n customRecordTypes: CustomRecordTypeRow[],\n customFields: CustomFieldRow[],\n): NormalizedSchema {\n const records: Record<string, RecordDefinition> = {};\n\n // 1. Parse all catalog records\n for (const [name, metadata] of catalogRecords) {\n records[name] = parseRecordMetadata(name, metadata);\n }\n\n // 2. Ensure custom record types from SuiteQL exist in the schema\n for (const crt of customRecordTypes) {\n const key = crt.scriptid.toLowerCase();\n if (!records[key]) {\n records[key] = {\n id: key,\n label: crt.name,\n isCustom: true,\n fields: {},\n relations: {},\n sublists: {},\n };\n } else {\n // Enrich existing record with SuiteQL label if catalog didn't provide one\n if (!records[key].label || records[key].label === key) {\n records[key].label = crt.name;\n }\n }\n }\n\n // 3. Merge custom fields into their parent records\n const fieldsByPrefix = groupCustomFieldsByPrefix(customFields);\n for (const [prefix, fields] of fieldsByPrefix) {\n mergeCustomFields(records, prefix, fields);\n }\n\n return {\n generatedAt: new Date().toISOString(),\n accountId,\n records,\n };\n}\n\n/**\n * Group custom fields by their script ID prefix to determine which record they belong to.\n * - custbody_ → transaction body fields\n * - custcol_ → transaction column (line item) fields\n * - custrecord_ → custom record fields\n * - custentity_ → entity fields\n * - custitem_ → item fields\n * - custevent_ → event fields\n */\nfunction groupCustomFieldsByPrefix(fields: CustomFieldRow[]): Map<string, CustomFieldRow[]> {\n const groups = new Map<string, CustomFieldRow[]>();\n\n for (const field of fields) {\n const scriptId = field.scriptid.toLowerCase();\n let prefix: string;\n\n if (scriptId.startsWith('custbody')) prefix = 'custbody';\n else if (scriptId.startsWith('custcol')) prefix = 'custcol';\n else if (scriptId.startsWith('custrecord')) prefix = 'custrecord';\n else if (scriptId.startsWith('custentity')) prefix = 'custentity';\n else if (scriptId.startsWith('custitem')) prefix = 'custitem';\n else if (scriptId.startsWith('custevent')) prefix = 'custevent';\n else prefix = 'other';\n\n const existing = groups.get(prefix);\n if (existing) {\n existing.push(field);\n } else {\n groups.set(prefix, [field]);\n }\n }\n\n return groups;\n}\n\n/**\n * Merge custom fields into all matching records.\n * Entity fields go to customer/vendor/employee/etc, body fields go to transactions, etc.\n */\nfunction mergeCustomFields(\n records: Record<string, RecordDefinition>,\n prefix: string,\n fields: CustomFieldRow[],\n): void {\n // Determine which records these fields could belong to\n const targetRecords = getTargetRecordsForPrefix(prefix, records);\n\n for (const field of fields) {\n const fieldDef = customFieldToFieldDefinition(field);\n for (const record of targetRecords) {\n // Only add if the field doesn't already exist from the catalog\n if (!record.fields[fieldDef.id]) {\n record.fields[fieldDef.id] = fieldDef;\n }\n }\n }\n}\n\nfunction getTargetRecordsForPrefix(\n prefix: string,\n records: Record<string, RecordDefinition>,\n): RecordDefinition[] {\n const targets: RecordDefinition[] = [];\n\n const entityTypes = ['customer', 'vendor', 'employee', 'partner', 'contact', 'lead', 'prospect'];\n const transactionTypes = ['salesorder', 'purchaseorder', 'invoice', 'vendorbill', 'estimate', 'opportunity'];\n const itemTypes = ['inventoryitem', 'noninventoryitem', 'serviceitem', 'kititem', 'assemblyitem'];\n\n let typeList: string[];\n switch (prefix) {\n case 'custentity':\n typeList = entityTypes;\n break;\n case 'custbody':\n case 'custcol':\n typeList = transactionTypes;\n break;\n case 'custitem':\n typeList = itemTypes;\n break;\n case 'custrecord':\n // Custom record fields — they belong to their specific custom records\n // We can't definitively match them without more info, so skip global merge\n return [];\n default:\n return [];\n }\n\n for (const typeName of typeList) {\n if (records[typeName]) {\n targets.push(records[typeName]);\n }\n }\n\n return targets;\n}\n\nfunction customFieldToFieldDefinition(field: CustomFieldRow): FieldDefinition {\n return {\n id: field.scriptid.toLowerCase(),\n label: field.name,\n type: mapCustomFieldType(field.fieldtype) as FieldType,\n required: field.ismandatory === 'T',\n readOnly: false,\n nativeType: field.fieldtype,\n };\n}\n","import type {\n NormalizedSchema,\n RecordDefinition,\n RelationDefinition,\n CatalogRecordMetadata,\n} from '../types.js';\n\n/**\n * Well-known NetSuite field→record mappings.\n * When a field with one of these names exists and is a reference (select) type,\n * it references the corresponding record.\n */\nconst KNOWN_FIELD_MAPPINGS: Record<string, string> = {\n entity: 'customer',\n customer: 'customer',\n vendor: 'vendor',\n employee: 'employee',\n partner: 'partner',\n contact: 'contact',\n subsidiary: 'subsidiary',\n department: 'department',\n class: 'classification',\n location: 'location',\n currency: 'currency',\n terms: 'term',\n salesrep: 'employee',\n nexus: 'nexus',\n account: 'account',\n item: 'item',\n units: 'unitstype',\n parent: '', // parent references same record type\n createdby: 'employee',\n lastmodifiedby: 'employee',\n territory: 'territory',\n leadsource: 'leadsource',\n pricelevel: 'pricelevel',\n entitystatus: 'entitystatus',\n customform: 'customform',\n defaultbankaccount: 'account',\n openingbalanceaccount: 'account',\n receivablesaccount: 'account',\n shippingitem: 'item',\n image: 'file',\n toplevelparent: 'customer',\n category: 'category',\n};\n\n/**\n * Infer relations from reference fields detected in catalog metadata.\n * Reference fields have nativeType === 'reference' (set by catalog-parser).\n */\nexport function inferRelationsFromCatalog(\n schema: NormalizedSchema,\n _catalogRecords: Map<string, CatalogRecordMetadata>,\n): void {\n // In the NetSuite catalog, reference fields don't specify their target.\n // We rely on convention-based inference instead.\n // This function exists for future enhancement when we can resolve targets.\n inferRelationsFromConventions(schema);\n}\n\n/**\n * Infer relations from well-known field name conventions.\n */\nexport function inferRelationsFromConventions(schema: NormalizedSchema): void {\n for (const [recordName, record] of Object.entries(schema.records)) {\n for (const [fieldId, field] of Object.entries(record.fields)) {\n // Only consider reference/select fields for relation inference\n if (field.type !== 'select' || field.nativeType !== 'reference') continue;\n\n // Skip if we already have a relation for this field\n if (record.relations[fieldId]) continue;\n\n const targetType = KNOWN_FIELD_MAPPINGS[fieldId.toLowerCase()];\n if (targetType === undefined) continue;\n\n // 'parent' is a self-reference\n const target = targetType === '' ? recordName : targetType;\n\n if (schema.records[target]) {\n addRelation(record, {\n name: fieldId,\n type: 'many-to-one',\n target,\n foreignKey: fieldId,\n });\n\n // Add inverse one-to-many on the target (unless it's a self-ref)\n if (target !== recordName) {\n const targetRecord = schema.records[target];\n if (targetRecord) {\n addRelation(targetRecord, {\n name: `${recordName}_via_${fieldId}`,\n type: 'one-to-many',\n target: recordName,\n foreignKey: fieldId,\n });\n }\n }\n }\n }\n }\n}\n\nfunction addRelation(record: RecordDefinition, relation: RelationDefinition): void {\n if (!record.relations[relation.name]) {\n record.relations[relation.name] = relation;\n }\n}\n","import { mkdir, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport type { NormalizedSchema, CatalogRecordMetadata } from '../types.js';\n\n/**\n * Write introspection output files to the output directory.\n */\nexport async function writeSchemaFiles(\n outputDir: string,\n schema: NormalizedSchema,\n rawMetadata: Map<string, CatalogRecordMetadata>,\n): Promise<{ schemaPath: string; relationsPath: string; rawPath: string }> {\n await mkdir(outputDir, { recursive: true });\n\n const schemaPath = join(outputDir, 'schema.json');\n const relationsPath = join(outputDir, 'relations.json');\n const rawPath = join(outputDir, 'raw-metadata.json');\n\n // Extract just the relations for a focused relations file\n const relationsMap: Record<string, Record<string, unknown>> = {};\n for (const [name, record] of Object.entries(schema.records)) {\n if (Object.keys(record.relations).length > 0) {\n relationsMap[name] = record.relations;\n }\n }\n\n await Promise.all([\n writeFile(schemaPath, JSON.stringify(schema, null, 2), 'utf-8'),\n writeFile(relationsPath, JSON.stringify(relationsMap, null, 2), 'utf-8'),\n writeFile(\n rawPath,\n JSON.stringify(Object.fromEntries(rawMetadata), null, 2),\n 'utf-8',\n ),\n ]);\n\n return { schemaPath, relationsPath, rawPath };\n}\n","import type { NetSuiteClient } from '@suiteportal/connector';\nimport type { NormalizedSchema, IntrospectOptions, CatalogRecordMetadata } from './types.js';\nimport { fetchCatalogIndex, fetchRecordMetadataBatch } from './catalog/record-catalog.js';\nimport { fetchCustomRecordTypes } from './suiteql/custom-records.js';\nimport { fetchCustomFields } from './suiteql/custom-fields.js';\nimport { normalizeSchema } from './schema/normalizer.js';\nimport { inferRelationsFromCatalog, inferRelationsFromConventions } from './schema/relations.js';\nimport { writeSchemaFiles } from './schema/writer.js';\n\nexport interface IntrospectResult {\n schema: NormalizedSchema;\n files: {\n schemaPath: string;\n relationsPath: string;\n rawPath: string;\n };\n stats: {\n totalRecords: number;\n totalFields: number;\n totalRelations: number;\n customRecordTypes: number;\n customFields: number;\n };\n}\n\n/**\n * Run the full introspection pipeline:\n * 1. Fetch catalog index\n * 2. Fetch individual record metadata (optionally filtered)\n * 3. Fetch custom record types via SuiteQL\n * 4. Fetch custom fields via SuiteQL\n * 5. Normalize and merge into unified schema\n * 6. Infer relations\n * 7. Write output files\n */\nexport async function introspect(\n client: NetSuiteClient,\n accountId: string,\n options?: IntrospectOptions,\n onProgress?: (message: string) => void,\n): Promise<IntrospectResult> {\n const outputDir = options?.outputDir ?? '.suiteportal';\n const fetchDetails = options?.fetchDetails ?? true;\n const includeCustomRecords = options?.includeCustomRecords ?? true;\n\n const log = onProgress ?? (() => {});\n\n // Step 1: Fetch catalog index\n log('Fetching record catalog index...');\n const catalog = await fetchCatalogIndex(client);\n log(`Found ${catalog.items.length} record types in catalog`);\n\n // Step 2: Fetch detailed record metadata\n let catalogRecords = new Map<string, CatalogRecordMetadata>();\n if (fetchDetails) {\n log('Fetching detailed record metadata...');\n catalogRecords = await fetchRecordMetadataBatch(client, catalog, options?.recordTypes);\n log(`Fetched metadata for ${catalogRecords.size} records`);\n }\n\n // Step 3 & 4: Fetch custom records and fields via SuiteQL\n let customRecordTypes: Awaited<ReturnType<typeof fetchCustomRecordTypes>> = [];\n let customFields: Awaited<ReturnType<typeof fetchCustomFields>> = [];\n\n if (includeCustomRecords) {\n log('Fetching custom record types via SuiteQL...');\n const [crt, cf] = await Promise.all([\n fetchCustomRecordTypes(client),\n fetchCustomFields(client),\n ]);\n customRecordTypes = crt;\n customFields = cf;\n log(`Found ${customRecordTypes.length} custom record types and ${customFields.length} custom fields`);\n }\n\n // Step 5: Normalize\n log('Normalizing schema...');\n const schema = normalizeSchema(accountId, catalogRecords, customRecordTypes, customFields);\n\n // Step 6: Infer relations\n log('Inferring relationships...');\n inferRelationsFromCatalog(schema, catalogRecords);\n inferRelationsFromConventions(schema);\n\n // Step 7: Write output files\n log(`Writing schema files to ${outputDir}/...`);\n const files = await writeSchemaFiles(outputDir, schema, catalogRecords);\n\n // Compute stats\n let totalFields = 0;\n let totalRelations = 0;\n for (const record of Object.values(schema.records)) {\n totalFields += Object.keys(record.fields).length;\n totalRelations += Object.keys(record.relations).length;\n }\n\n return {\n schema,\n files,\n stats: {\n totalRecords: Object.keys(schema.records).length,\n totalFields,\n totalRelations,\n customRecordTypes: customRecordTypes.length,\n customFields: customFields.length,\n },\n };\n}\n"],"mappings":"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","names":["import_connector"]}
|