@cerios/salesforce-sobjectly 0.0.0-stage → 1.0.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.
@@ -0,0 +1,1209 @@
1
+ const require_sf_cli = require("./sf-cli-DEVZQTR7.cjs");
2
+ let node_crypto = require("node:crypto");
3
+ let node_fs = require("node:fs");
4
+ let node_fs_promises = require("node:fs/promises");
5
+ let node_path = require("node:path");
6
+ let jiti = require("jiti");
7
+ let node_readline = require("node:readline");
8
+ //#region src/codegen/type-mapper.ts
9
+ const ADDRESS_TYPE = "SalesforceAddress";
10
+ const GEOLOCATION_TYPE = "SalesforceGeolocation";
11
+ /** Maps each Salesforce field type to a TypeScript type. Types not listed become `unknown`. */
12
+ const FIELD_TYPE_MAP = {
13
+ boolean: "boolean",
14
+ int: "number",
15
+ long: "number",
16
+ double: "number",
17
+ currency: "number",
18
+ percent: "number",
19
+ /** ISO date, `yyyy-MM-dd`. */
20
+ date: "string",
21
+ /** ISO date-time, e.g. `2026-01-31T12:00:00.000+0000`. */
22
+ datetime: "string",
23
+ /** `HH:mm:ss.SSSZ`. */
24
+ time: "string",
25
+ id: "string",
26
+ reference: "string",
27
+ string: "string",
28
+ textarea: "string",
29
+ picklist: "string",
30
+ /** Values separated by `;`. */
31
+ multipicklist: "string",
32
+ combobox: "string",
33
+ email: "string",
34
+ phone: "string",
35
+ url: "string",
36
+ encryptedstring: "string",
37
+ /** Base64-encoded content. */
38
+ base64: "string",
39
+ address: ADDRESS_TYPE,
40
+ location: GEOLOCATION_TYPE
41
+ };
42
+ /** Returns the TypeScript type for a field, including `| null` for nillable fields. */
43
+ function mapFieldType(field, picklists = "union") {
44
+ const base = picklistType(field, picklists) ?? FIELD_TYPE_MAP[field.type] ?? "unknown";
45
+ return field.nillable && base !== "unknown" ? `${base} | null` : base;
46
+ }
47
+ function picklistType(field, picklists) {
48
+ if (picklists !== "union" || field.type !== "picklist" && field.type !== "combobox") return;
49
+ const values = [...new Set((field.picklistValues ?? []).filter((entry) => entry.active).map((entry) => entry.value))];
50
+ if (values.length === 0) return;
51
+ const union = values.map((value) => JSON.stringify(value)).join(" | ");
52
+ return field.restrictedPicklist && field.type === "picklist" ? union : `${union} | (string & {})`;
53
+ }
54
+ //#endregion
55
+ //#region src/codegen/generator.ts
56
+ /** Field types whose kind is recorded in the registry's `fieldKinds`. */
57
+ const FIELD_KINDS = /* @__PURE__ */ new Set([
58
+ "date",
59
+ "datetime",
60
+ "time",
61
+ "multipicklist"
62
+ ]);
63
+ const IDENTIFIER$1 = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
64
+ /**
65
+ * Generates the TypeScript source for a set of sObject describes: a read interface, create
66
+ * and update input types per sObject, and the `SObjectRegistry` that ties them together.
67
+ * The output is deterministic: sObjects are sorted by name and fields by name (`Id` first).
68
+ */
69
+ function generateSource(describes, options = {}) {
70
+ const sorted = [...describes].sort((a, b) => compare(a.name, b.name));
71
+ assertGeneratableNames(sorted);
72
+ const names = new Set(sorted.map((describe) => describe.name));
73
+ const sections = [imports(sorted, options.importSource ?? "@cerios/salesforce-sobjectly")];
74
+ const objectHashes = [];
75
+ for (const describe of sorted) {
76
+ const objectSections = [
77
+ readInterface(describe, options.picklists ?? "union"),
78
+ inputType(describe, "CreateInput", "createable", lookup(options.excludeCreateFields, describe.name)),
79
+ inputType(describe, "UpdateInput", "updateable", lookup(options.excludeUpdateFields, describe.name))
80
+ ];
81
+ sections.push(...objectSections);
82
+ const fingerprint = [
83
+ ...objectSections,
84
+ registryEntry(describe, names),
85
+ options.constants === false ? "" : picklistEntry(describe)
86
+ ].join("\n");
87
+ objectHashes.push(`${describe.name}:${sha256(fingerprint).slice(0, OBJECT_HASH_LENGTH)}`);
88
+ }
89
+ sections.push(registry(sorted, names));
90
+ if (options.constants !== false) sections.push(picklistConstants(sorted), recordTypeConstants(sorted));
91
+ if (options.apiVersion) sections.push(apiVersionConstant(options.apiVersion));
92
+ const body = sections.filter((section) => section.length > 0).join("\n\n");
93
+ return `${header(sha256(body), objectHashes)}\n\n${body}\n`;
94
+ }
95
+ /** Names the generated file uses itself; an sObject with one of these names would break it. */
96
+ const RESERVED_TYPE_NAMES = /* @__PURE__ */ new Set([
97
+ "Record",
98
+ "Pick",
99
+ "Partial",
100
+ "SObjectRegistry",
101
+ "PICKLIST_VALUES",
102
+ "RECORD_TYPES",
103
+ "API_VERSION",
104
+ ADDRESS_TYPE,
105
+ GEOLOCATION_TYPE
106
+ ]);
107
+ /** Fails with a clear message for sObject names the generated TypeScript can't represent. */
108
+ function assertGeneratableNames(describes) {
109
+ const seen = /* @__PURE__ */ new Map();
110
+ const generated = new Set(describes.flatMap((describe) => [`${describe.name}CreateInput`, `${describe.name}UpdateInput`]));
111
+ for (const { name } of describes) {
112
+ if (!IDENTIFIER$1.test(name)) throw new Error(`sObject name "${name}" is not a valid TypeScript identifier.`);
113
+ if (RESERVED_TYPE_NAMES.has(name) || generated.has(name)) throw new Error(`sObject name "${name}" clashes with a type the generated file defines; exclude it.`);
114
+ const previous = seen.get(name.toLowerCase());
115
+ if (previous !== void 0) throw new Error(`sObjects "${previous}" and "${name}" differ only in case; list each sObject once.`);
116
+ seen.set(name.toLowerCase(), name);
117
+ }
118
+ }
119
+ /** Looks up a per-sObject option case-insensitively (sObject names are case-insensitive in Salesforce). */
120
+ function lookup(map, name) {
121
+ if (!map) return [];
122
+ const key = Object.keys(map).find((candidate) => candidate.toLowerCase() === name.toLowerCase());
123
+ return key === void 0 ? [] : map[key] ?? [];
124
+ }
125
+ const CONTENT_HASH_PREFIX = "// sobjectly-content-hash: sha256:";
126
+ const OBJECT_HASHES_PREFIX = "// sobjectly-sobjects: ";
127
+ const OBJECT_HASH_LENGTH = 12;
128
+ function header(contentHash, objectHashes) {
129
+ return [
130
+ "// ──────────────────────────────────────────────────────────────",
131
+ "// AUTO-GENERATED by @cerios/salesforce-sobjectly (`sobjectly generate`).",
132
+ "// Do not edit manually; re-run the generator instead.",
133
+ "// ──────────────────────────────────────────────────────────────",
134
+ `${CONTENT_HASH_PREFIX}${contentHash}`,
135
+ `${OBJECT_HASHES_PREFIX}${objectHashes.join(",")}`,
136
+ "/* eslint-disable */"
137
+ ].join("\n");
138
+ }
139
+ /** Reads the content hashes from a generated file's header (also after reformatting). */
140
+ function readGeneratedHashes(source) {
141
+ const lines = source.split(/\r?\n/).map((line) => line.trim());
142
+ const hashLine = lines.find((line) => line.startsWith(CONTENT_HASH_PREFIX));
143
+ if (!hashLine) return;
144
+ const objectsLine = lines.find((line) => line.startsWith(OBJECT_HASHES_PREFIX)) ?? OBJECT_HASHES_PREFIX;
145
+ const sobjects = /* @__PURE__ */ new Map();
146
+ for (const entry of objectsLine.slice(23).split(",")) {
147
+ const [name, hash] = entry.split(":");
148
+ if (name && hash) sobjects.set(name, hash);
149
+ }
150
+ return {
151
+ contentHash: hashLine.slice(34),
152
+ sobjects
153
+ };
154
+ }
155
+ function sha256(text) {
156
+ return (0, node_crypto.createHash)("sha256").update(text).digest("hex");
157
+ }
158
+ function imports(describes, importSource) {
159
+ const used = [];
160
+ const fields = describes.flatMap((describe) => describe.fields);
161
+ if (fields.some((field) => field.type === "address")) used.push(ADDRESS_TYPE);
162
+ if (fields.some((field) => field.type === "location")) used.push(GEOLOCATION_TYPE);
163
+ return used.length > 0 ? `import type { ${used.join(", ")} } from ${JSON.stringify(importSource)};` : "";
164
+ }
165
+ function sortedFields(describe) {
166
+ return [...describe.fields].sort((a, b) => a.name === "Id" ? -1 : b.name === "Id" ? 1 : compare(a.name, b.name));
167
+ }
168
+ function readInterface(describe, picklists) {
169
+ const lines = sortedFields(describe).map((field) => {
170
+ return `${`\t/** ${escapeComment(field.label)} (${escapeComment(field.type)}) */`}\n\t${propertyKey(field.name)}: ${mapFieldType(field, picklists)};`;
171
+ });
172
+ return `${describe.label ? `/** ${escapeComment(describe.label)} */\n` : ""}export interface ${describe.name} {\n${lines.join("\n")}\n}`;
173
+ }
174
+ /**
175
+ * Create input: required fields are `createable && !nillable && !defaultedOnCreate`; other
176
+ * createable fields are optional. Validation rules and record types can add more requirements
177
+ * at runtime. Update input: every updateable field is optional.
178
+ */
179
+ function inputType(describe, suffix, flag, excluded) {
180
+ const typeName = `${describe.name}${suffix}`;
181
+ const excludedNames = new Set(excluded.map((name) => name.toLowerCase()));
182
+ const allowed = sortedFields(describe).filter((field) => field[flag] && !excludedNames.has(field.name.toLowerCase()));
183
+ if (allowed.length === 0) return `export type ${typeName} = Record<string, never>;`;
184
+ const required = suffix === "CreateInput" ? allowed.filter((field) => !field.nillable && !field.defaultedOnCreate).map((field) => field.name) : [];
185
+ const optional = allowed.map((field) => field.name).filter((name) => !required.includes(name));
186
+ const parts = [];
187
+ if (required.length > 0) parts.push(`Pick<${describe.name}, ${keyUnion(required)}>`);
188
+ if (optional.length > 0) parts.push(`Partial<Pick<${describe.name}, ${keyUnion(optional)}>>`);
189
+ return `export type ${typeName} = ${parts.join(" & ")};`;
190
+ }
191
+ function registry(describes, names) {
192
+ return [
193
+ "/**",
194
+ " * Maps every generated sObject to its read, create and update types and its relationships.",
195
+ " * Pass it to the client: `new SalesforceClient<SObjectRegistry>({ ... })`.",
196
+ " */",
197
+ "export interface SObjectRegistry {",
198
+ ...describes.map((describe) => registryEntry(describe, names)),
199
+ "}"
200
+ ].join("\n");
201
+ }
202
+ function registryEntry(describe, names) {
203
+ const parents = describe.fields.filter((field) => field.relationshipName && field.referenceTo.length === 1 && names.has(field.referenceTo[0]) && IDENTIFIER$1.test(field.relationshipName)).map((field) => [field.relationshipName, field.referenceTo[0]]);
204
+ const children = describe.childRelationships.filter((relationship) => relationship.relationshipName && names.has(relationship.childSObject) && IDENTIFIER$1.test(relationship.relationshipName)).map((relationship) => [relationship.relationshipName, relationship.childSObject]);
205
+ return [
206
+ `\t${describe.name}: {`,
207
+ `\t\tread: ${describe.name};`,
208
+ `\t\tcreate: ${describe.name}CreateInput;`,
209
+ `\t\tupdate: ${describe.name}UpdateInput;`,
210
+ `\t\tparents: ${relationshipMap(parents)};`,
211
+ `\t\tchildren: ${relationshipMap(children)};`,
212
+ `\t\texternalIds: ${unionOrNever(externalIdFields(describe))};`,
213
+ `\t\tfieldKinds: ${fieldKindMap(describe)};`,
214
+ `\t\trecordTypes: ${unionOrNever(recordTypeNames(describe))};`,
215
+ `\t\tpolymorphicParents: ${polymorphicMap(describe)};`,
216
+ " };"
217
+ ].join("\n");
218
+ }
219
+ /** `Id` plus every field marked `externalId` or `idLookup`: the fields upsert accepts. */
220
+ function externalIdFields(describe) {
221
+ const names = describe.fields.filter((field) => field.name === "Id" || field.externalId === true || field.idLookup === true).map((field) => field.name);
222
+ return [...new Set(names)].sort(compare);
223
+ }
224
+ function recordTypeNames(describe) {
225
+ const names = (describe.recordTypeInfos ?? []).map((info) => info.developerName);
226
+ return [...new Set(names.filter((name) => typeof name === "string" && name.length > 0))].sort(compare);
227
+ }
228
+ function fieldKindMap(describe) {
229
+ const lines = sortedFields(describe).filter((field) => FIELD_KINDS.has(field.type)).map((field) => `\t\t\t${propertyKey(field.name)}: ${JSON.stringify(field.type)};`);
230
+ return lines.length > 0 ? `{\n${lines.join("\n")}\n\t\t}` : "Record<never, never>";
231
+ }
232
+ /** Lookups with more than one target (e.g. `Owner` -> User or Group); `parents` leaves these out. */
233
+ function polymorphicMap(describe) {
234
+ const entries = /* @__PURE__ */ new Map();
235
+ for (const field of sortedFields(describe)) {
236
+ const name = field.relationshipName;
237
+ if (name && field.referenceTo.length > 1 && IDENTIFIER$1.test(name) && !entries.has(name)) entries.set(name, unionOrNever([...field.referenceTo].sort(compare)));
238
+ }
239
+ const lines = [...entries.entries()].sort(([a], [b]) => compare(a, b)).map(([name, targets]) => `\t\t\t${name}: ${targets};`);
240
+ return lines.length > 0 ? `{\n${lines.join("\n")}\n\t\t}` : "Record<never, never>";
241
+ }
242
+ /** `PICKLIST_VALUES`: the active values of every picklist and multi-select picklist field. */
243
+ function picklistConstants(describes) {
244
+ const entries = describes.map(picklistEntry).filter((entry) => entry.length > 0);
245
+ return ["/** Active picklist values per sObject and field, e.g. to loop over every status in a test. */", `export const PICKLIST_VALUES = {${entries.length > 0 ? `\n${entries.join("\n")}\n` : ""}} as const;`].join("\n");
246
+ }
247
+ /** One sObject's entry in `PICKLIST_VALUES`, or `""` when it has no picklists. */
248
+ function picklistEntry(describe) {
249
+ const fields = sortedFields(describe).map((field) => [field.name, activePicklistValues(field)]).filter(([, values]) => values.length > 0).map(([name, values]) => `\t\t${propertyKey(name)}: [${values.map((value) => JSON.stringify(value)).join(", ")}],`);
250
+ return fields.length > 0 ? `\t${describe.name}: {\n${fields.join("\n")}\n\t},` : "";
251
+ }
252
+ /** `RECORD_TYPES`: the record type DeveloperNames per sObject. */
253
+ function recordTypeConstants(describes) {
254
+ const entries = describes.map((describe) => [describe.name, recordTypeNames(describe)]).filter(([, names]) => names.length > 0).map(([name, names]) => `\t${name}: [${names.map((value) => JSON.stringify(value)).join(", ")}],`);
255
+ return ["/** Record type DeveloperNames per sObject. Resolve ids with `sf.sobject(name).recordTypeId(developerName)`. */", `export const RECORD_TYPES = {${entries.length > 0 ? `\n${entries.join("\n")}\n` : ""}} as const;`].join("\n");
256
+ }
257
+ function activePicklistValues(field) {
258
+ if (field.type !== "picklist" && field.type !== "multipicklist" && field.type !== "combobox") return [];
259
+ return [...new Set((field.picklistValues ?? []).filter((entry) => entry.active).map((entry) => entry.value))];
260
+ }
261
+ function unionOrNever(values) {
262
+ return values.length > 0 ? values.map((value) => JSON.stringify(value)).join(" | ") : "never";
263
+ }
264
+ function apiVersionConstant(apiVersion) {
265
+ if (!/^v\d+\.\d+$/.test(apiVersion)) throw new Error(`Invalid apiVersion "${apiVersion}".`);
266
+ return [
267
+ "/**",
268
+ " * The API version these types were generated with. Pass it to the client so requests use the",
269
+ " * same version: `new SalesforceClient<SObjectRegistry>({ apiVersion: API_VERSION, ... })`.",
270
+ " */",
271
+ `export const API_VERSION = ${JSON.stringify(apiVersion)};`
272
+ ].join("\n");
273
+ }
274
+ function relationshipMap(entries) {
275
+ const unique = /* @__PURE__ */ new Map();
276
+ for (const [name, target] of entries) if (!unique.has(name)) unique.set(name, target);
277
+ if (unique.size === 0) return "Record<never, never>";
278
+ return `{\n${[...unique.entries()].sort(([a], [b]) => compare(a, b)).map(([name, target]) => `\t\t\t${name}: ${JSON.stringify(target)};`).join("\n")}\n\t\t}`;
279
+ }
280
+ function keyUnion(names) {
281
+ return names.map((name) => JSON.stringify(name)).join(" | ");
282
+ }
283
+ function propertyKey(name) {
284
+ return IDENTIFIER$1.test(name) ? name : JSON.stringify(name);
285
+ }
286
+ function escapeComment(value) {
287
+ return String(value ?? "").replace(/\*\//g, "*\\/").replace(/\r?\n/g, " ");
288
+ }
289
+ function compare(a, b) {
290
+ return a < b ? -1 : a > b ? 1 : 0;
291
+ }
292
+ //#endregion
293
+ //#region src/codegen/validate-config.ts
294
+ /** Thrown when a codegen config is invalid. Lists every problem found. */
295
+ var CodegenConfigError = class extends Error {
296
+ source;
297
+ problems;
298
+ name = "CodegenConfigError";
299
+ constructor(source, problems) {
300
+ super(`Invalid sobjectly config (${source}):\n${problems.map((problem) => ` - ${problem}`).join("\n")}`);
301
+ this.source = source;
302
+ this.problems = problems;
303
+ }
304
+ };
305
+ /** The env-variable options each env-based auth type accepts. */
306
+ const AUTH_OPTIONS = {
307
+ clientCredentials: [
308
+ "loginUrlEnv",
309
+ "clientIdEnv",
310
+ "clientSecretEnv"
311
+ ],
312
+ accessToken: ["accessTokenEnv", "instanceUrlEnv"],
313
+ jwtBearer: [
314
+ "loginUrlEnv",
315
+ "clientIdEnv",
316
+ "usernameEnv",
317
+ "privateKeyEnv",
318
+ "privateKeyPathEnv"
319
+ ],
320
+ sfCli: ["targetOrg"]
321
+ };
322
+ const PICKLIST_MODES = ["union", "string"];
323
+ const IDENTIFIER = /^[A-Za-z][A-Za-z0-9_]*$/;
324
+ const VALIDATORS = {
325
+ $schema: (value, { problem }) => {
326
+ if (typeof value !== "string") problem("$schema: must be a string.");
327
+ },
328
+ output: (value, { problem }) => {
329
+ if (value === void 0) problem("output: required, e.g. \"src/generated/sobjects.ts\".");
330
+ else if (typeof value !== "string" || !/\.(ts|mts|cts)$/.test(value)) problem(`output: must be a TypeScript file path such as "src/generated/sobjects.ts", got ${show(value)}.`);
331
+ },
332
+ apiVersion: (value, { problem }) => {
333
+ if (value === void 0 || value === "") problem("apiVersion: required, e.g. \"v66.0\".");
334
+ else if (!require_sf_cli.isApiVersion(value)) problem(`apiVersion: must look like "v66.0", got ${show(value)}.`);
335
+ },
336
+ sobjects: (value, { problem }) => validateNames("sobjects", value, problem),
337
+ exclude: (value, { problem }) => validateNames("exclude", value, problem),
338
+ auth: validateAuth,
339
+ excludeCreateFields: (value, context) => validateFieldMap("excludeCreateFields", value, context),
340
+ excludeUpdateFields: (value, context) => validateFieldMap("excludeUpdateFields", value, context),
341
+ picklists: (value, { problem }) => {
342
+ if (!PICKLIST_MODES.includes(value)) problem(`picklists: must be "union" or "string", got ${show(value)}.`);
343
+ },
344
+ constants: (value, { problem }) => {
345
+ if (typeof value !== "boolean") problem(`constants: must be true or false, got ${show(value)}.`);
346
+ },
347
+ concurrency: (value, { problem }) => {
348
+ if (!Number.isInteger(value) || value < 1) problem(`concurrency: must be a positive integer, got ${show(value)}.`);
349
+ },
350
+ continueOnDescribeError: (value, { problem }) => {
351
+ if (typeof value !== "boolean") problem(`continueOnDescribeError: must be true or false, got ${show(value)}.`);
352
+ },
353
+ importSource: (value, { problem }) => {
354
+ if (typeof value !== "string" || value.length === 0) problem(`importSource: must be a non-empty string, got ${show(value)}.`);
355
+ },
356
+ format: (value, { problem, json }) => {
357
+ if (json) problem("format: only supported in a TypeScript config.");
358
+ else if (typeof value !== "function") problem("format: must be a function (source, filePath) => string.");
359
+ },
360
+ transport: (value, { problem, json }) => {
361
+ if (json) problem("transport: only supported in a TypeScript config.");
362
+ else if (typeof value?.send !== "function") problem("transport: must be an HttpTransport (an object with a send() method).");
363
+ }
364
+ };
365
+ /** The options a config may contain. */
366
+ const CONFIG_OPTIONS = Object.keys(VALIDATORS).filter((key) => key !== "$schema");
367
+ /**
368
+ * Checks a loaded config and returns it typed. Throws a `CodegenConfigError` listing every
369
+ * problem. `json` disallows the options that only a TypeScript config can express.
370
+ */
371
+ function validateConfig(value, options) {
372
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new CodegenConfigError(options.source, ["the config must be an object (export default defineConfig({ ... }) or a JSON object)."]);
373
+ const problems = [];
374
+ const context = {
375
+ config: value,
376
+ json: options.json ?? false,
377
+ problem: (message) => problems.push(message)
378
+ };
379
+ for (const required of ["apiVersion", "output"]) if (!(required in value)) VALIDATORS[required]?.(void 0, context);
380
+ for (const [key, option] of Object.entries(value)) {
381
+ const validate = VALIDATORS[key];
382
+ if (!validate) problems.push(`${key}: unknown option. Valid options: ${CONFIG_OPTIONS.join(", ")}.`);
383
+ else if (option !== void 0 || key === "apiVersion" || key === "output") validate(option, context);
384
+ }
385
+ if (problems.length > 0) {
386
+ if (problems.some((problem) => problem.startsWith("apiVersion") || problem.startsWith("output"))) problems.push("Run \"npx sobjectly init\" to create a valid config.");
387
+ throw new CodegenConfigError(options.source, problems);
388
+ }
389
+ const { $schema: _schema, ...config } = value;
390
+ return config;
391
+ }
392
+ function validateNames(key, value, problem) {
393
+ if (!Array.isArray(value)) {
394
+ problem(`${key}: must be an array of sObject API names, got ${show(value)}.`);
395
+ return;
396
+ }
397
+ if (key === "sobjects" && value.length === 0) problem("sobjects: list at least one sObject, or leave it out to generate every queryable sObject.");
398
+ value.forEach((name, index) => {
399
+ if (typeof name !== "string" || !IDENTIFIER.test(name)) problem(`${key}[${index}]: ${show(name)} is not a valid sObject API name.`);
400
+ });
401
+ }
402
+ function validateFieldMap(key, value, { config, problem }) {
403
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
404
+ problem(`${key}: must be an object like { "Account": ["FieldName"] }, got ${show(value)}.`);
405
+ return;
406
+ }
407
+ const listed = Array.isArray(config.sobjects) ? new Set(config.sobjects.map((name) => typeof name === "string" ? name.toLowerCase() : name)) : void 0;
408
+ for (const [sobject, fields] of Object.entries(value)) {
409
+ if (listed && !listed.has(sobject.toLowerCase())) problem(`${key}.${sobject}: "${sobject}" is not listed in sobjects.`);
410
+ if (!Array.isArray(fields) || fields.some((field) => typeof field !== "string" || field.length === 0)) problem(`${key}.${sobject}: must be an array of field API names, got ${show(fields)}.`);
411
+ }
412
+ }
413
+ function validateAuth(value, { json, problem }) {
414
+ if (typeof value !== "object" || value === null) {
415
+ problem(`auth: must be an object like { "type": "clientCredentials" }, got ${show(value)}.`);
416
+ return;
417
+ }
418
+ if (typeof value.getToken === "function") {
419
+ if (json) problem("auth: auth providers are only supported in a TypeScript config.");
420
+ return;
421
+ }
422
+ const { type, ...rest } = value;
423
+ const allowed = AUTH_OPTIONS[type];
424
+ if (!allowed) {
425
+ problem(`auth.type: must be one of ${Object.keys(AUTH_OPTIONS).join(", ")}, got ${show(type)}.`);
426
+ return;
427
+ }
428
+ for (const [key, option] of Object.entries(rest)) if (!allowed.includes(key)) problem(`auth.${key}: not an option of "${String(type)}". Valid options: ${allowed.join(", ")}.`);
429
+ else if (typeof option !== "string" || option.length === 0) problem(`auth.${key}: must be a non-empty string, got ${show(option)}.`);
430
+ }
431
+ function show(value) {
432
+ if (value === void 0) return "undefined";
433
+ if (typeof value === "function") return "a function";
434
+ try {
435
+ return JSON.stringify(value) ?? typeof value;
436
+ } catch {
437
+ return typeof value;
438
+ }
439
+ }
440
+ //#endregion
441
+ //#region src/codegen/config.ts
442
+ /**
443
+ * Type-checks a `sobjectly.config.ts`. The listed `sobjects` become a literal union, so a typo in
444
+ * a per-sObject option is a compile error:
445
+ *
446
+ * ```ts
447
+ * export default defineConfig({
448
+ * apiVersion: "v66.0",
449
+ * output: "src/generated/sobjects.ts",
450
+ * sobjects: ["Account", "Contact"],
451
+ * excludeCreateFields: { Acount: ["Name"] }, // ❌ "Acount" is not in sobjects
452
+ * });
453
+ * ```
454
+ */
455
+ function defineConfig(config) {
456
+ return config;
457
+ }
458
+ //#endregion
459
+ //#region src/codegen/load-config.ts
460
+ /** Config file names, in lookup order. */
461
+ const CONFIG_FILE_NAMES = [
462
+ "sobjectly.config.ts",
463
+ "sobjectly.config.mts",
464
+ "sobjectly.config.cts",
465
+ "sobjectly.config.json"
466
+ ];
467
+ const TYPESCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([
468
+ ".ts",
469
+ ".mts",
470
+ ".cts"
471
+ ]);
472
+ /** Looks for a `sobjectly.config.{ts,mts,cts,json}` file in `cwd`; the first match in lookup order wins. */
473
+ function findConfigFile(cwd = process.cwd()) {
474
+ return findConfigFiles(cwd)[0];
475
+ }
476
+ /** Every `sobjectly.config.*` file in `cwd`, in lookup order. */
477
+ function findConfigFiles(cwd = process.cwd()) {
478
+ return CONFIG_FILE_NAMES.map((name) => (0, node_path.join)(cwd, name)).filter((path) => (0, node_fs.existsSync)(path));
479
+ }
480
+ /**
481
+ * Loads and validates a TypeScript (`.ts`, `.mts`, `.cts`) or JSON config file. Returns the
482
+ * config and the directory it lives in. Throws a `CodegenConfigError` listing every problem.
483
+ */
484
+ async function loadConfig(configPath) {
485
+ const absolutePath = (0, node_path.resolve)(configPath);
486
+ if (!(0, node_fs.existsSync)(absolutePath)) throw new Error(`Config file not found: ${absolutePath}`);
487
+ const extension = (0, node_path.extname)(absolutePath).toLowerCase();
488
+ const directory = (0, node_path.dirname)(absolutePath);
489
+ if (extension === ".json") return {
490
+ config: validateConfig(readJson(absolutePath), {
491
+ source: absolutePath,
492
+ json: true
493
+ }),
494
+ directory
495
+ };
496
+ if (!TYPESCRIPT_EXTENSIONS.has(extension)) throw new Error(`Unsupported config file "${absolutePath}". Use sobjectly.config.ts or sobjectly.config.json.`);
497
+ return {
498
+ config: validateConfig(await (0, jiti.createJiti)((0, node_path.join)(directory, "_sobjectly_loader.js"), { interopDefault: true }).import(absolutePath, { default: true }), { source: absolutePath }),
499
+ directory
500
+ };
501
+ }
502
+ function readJson(path) {
503
+ try {
504
+ return JSON.parse((0, node_fs.readFileSync)(path, "utf8"));
505
+ } catch (error) {
506
+ throw new CodegenConfigError(path, [`not valid JSON: ${error instanceof Error ? error.message : String(error)}`]);
507
+ }
508
+ }
509
+ //#endregion
510
+ //#region src/codegen/init.ts
511
+ const SOBJECT_NAME = /^[A-Za-z][A-Za-z0-9_]*$/;
512
+ const NPM_SCRIPT = "sobjects:generate";
513
+ /** The environment variables each auth type reads by default. */
514
+ const AUTH_ENV_VARS = {
515
+ clientCredentials: [
516
+ "SF_LOGIN_URL",
517
+ "SF_CLIENT_ID",
518
+ "SF_CLIENT_SECRET"
519
+ ],
520
+ accessToken: ["SF_ACCESS_TOKEN", "SF_INSTANCE_URL"],
521
+ jwtBearer: [
522
+ "SF_LOGIN_URL",
523
+ "SF_CLIENT_ID",
524
+ "SF_USERNAME",
525
+ "SF_PRIVATE_KEY_PATH"
526
+ ],
527
+ sfCli: []
528
+ };
529
+ const AUTH_CHOICES = [
530
+ {
531
+ value: "clientCredentials",
532
+ label: "Client credentials (recommended for CI and test automation)"
533
+ },
534
+ {
535
+ value: "sfCli",
536
+ label: "Salesforce CLI org (recommended for local development; no secrets needed)"
537
+ },
538
+ {
539
+ value: "accessToken",
540
+ label: "Access token (e.g. from `sf org display`)"
541
+ },
542
+ {
543
+ value: "jwtBearer",
544
+ label: "JWT bearer (certificate-based, runs as a user)"
545
+ }
546
+ ];
547
+ const PICKLIST_CHOICES = [{
548
+ value: "union",
549
+ label: "Union of values, e.g. \"New\" | \"Closed\" (type-safe)"
550
+ }, {
551
+ value: "string",
552
+ label: "Plain string"
553
+ }];
554
+ const FORMAT_CHOICES = [{
555
+ value: "ts",
556
+ label: "TypeScript (sobjectly.config.ts, type-checked with defineConfig)"
557
+ }, {
558
+ value: "json",
559
+ label: "JSON (sobjectly.config.json, validated with a JSON Schema)"
560
+ }];
561
+ /**
562
+ * Creates a `sobjectly.config` file by asking for the API version, output path, sObjects, auth
563
+ * method, picklist mode and file format. Optionally adds the auth variables to `.env.example`
564
+ * and an npm script to `package.json`.
565
+ */
566
+ async function runInit(options) {
567
+ const { cwd, prompter, log } = options;
568
+ const existing = findConfigFiles(cwd);
569
+ if (existing.length > 0 && !options.force) {
570
+ const names = existing.map((path) => (0, node_path.relative)(cwd, path)).join(" and ");
571
+ if (!(options.yes ? false : await prompter.confirm(`${names} already exists. Replace it?`, false))) {
572
+ log(`Kept the existing ${names}.`);
573
+ return { configPath: void 0 };
574
+ }
575
+ }
576
+ validatePreset(options.preset ?? {});
577
+ const answers = await askQuestions(options);
578
+ const configPath = (0, node_path.join)(cwd, `sobjectly.config.${answers.format}`);
579
+ for (const path of existing.filter((path) => path !== configPath)) {
580
+ (0, node_fs.rmSync)(path);
581
+ log(`Removed ${(0, node_path.relative)(cwd, path)}`);
582
+ }
583
+ (0, node_fs.writeFileSync)(configPath, renderConfig(answers), "utf8");
584
+ log(`\nCreated ${(0, node_path.relative)(cwd, configPath)}`);
585
+ const envExampleUpdated = await maybeUpdateEnvExample(options, answers.auth);
586
+ const scriptAdded = await maybeAddScript(options);
587
+ printNextSteps(log, answers, scriptAdded, cwd);
588
+ return {
589
+ configPath,
590
+ answers,
591
+ envExampleUpdated,
592
+ scriptAdded
593
+ };
594
+ }
595
+ async function askQuestions(options) {
596
+ const { prompter, preset = {}, yes } = options;
597
+ const apiVersion = preset.apiVersion ?? await askApiVersion(prompter, yes);
598
+ const output = preset.output ?? (yes ? "src/generated/sobjects.ts" : await prompter.ask("Where should the generated types be written?", {
599
+ defaultValue: "src/generated/sobjects.ts",
600
+ validate: validateOutput
601
+ }));
602
+ const sobjects = preset.sobjects ?? await askSObjects(prompter, yes);
603
+ const auth = preset.auth ?? (yes ? "clientCredentials" : await prompter.choose("How should the generator log in to Salesforce?", AUTH_CHOICES, "clientCredentials"));
604
+ const targetOrg = auth === "sfCli" ? preset.targetOrg ?? await askTargetOrg(prompter, yes) : void 0;
605
+ const picklists = preset.picklists ?? (yes ? "union" : await prompter.choose("How should picklist fields be typed?", PICKLIST_CHOICES, "union"));
606
+ const format = preset.format ?? (yes ? "ts" : await prompter.choose("Which config file format?", FORMAT_CHOICES, "ts"));
607
+ return {
608
+ apiVersion,
609
+ output,
610
+ sobjects,
611
+ auth,
612
+ ...targetOrg ? { targetOrg } : {},
613
+ picklists,
614
+ format
615
+ };
616
+ }
617
+ async function askTargetOrg(prompter, yes) {
618
+ if (yes) return;
619
+ const answer = await prompter.ask("Which Salesforce CLI org alias? Leave empty to use the CLI's default org", { validate: (value) => value === "" || /^[\w.@+-]+$/.test(value) ? void 0 : "Enter an org alias or username." });
620
+ return answer === "" ? void 0 : answer;
621
+ }
622
+ async function askApiVersion(prompter, yes) {
623
+ if (yes) throw new Error("An API version is required: pass --api-version (e.g. --api-version v66.0).");
624
+ const answer = await prompter.ask("Which Salesforce API version should be used? (e.g. v66.0; your org lists them at https://<my-domain>.my.salesforce.com/services/data/)", { validate: (value) => require_sf_cli.normalizeApiVersion(value) ? void 0 : "Enter a version like \"v66.0\" or \"66\"." });
625
+ return require_sf_cli.normalizeApiVersion(answer);
626
+ }
627
+ async function askSObjects(prompter, yes) {
628
+ const defaultValue = "Account, Contact";
629
+ return parseSObjects(yes ? defaultValue : await prompter.ask("Which sObjects do you want types for? Use comma-separated API names", {
630
+ defaultValue,
631
+ validate: validateSObjects
632
+ }));
633
+ }
634
+ /** Splits a comma/space separated list of sObject names, removing duplicates. */
635
+ function parseSObjects(answer) {
636
+ return [...new Set(answer.split(/[\s,]+/).filter((name) => name.length > 0))];
637
+ }
638
+ function validateSObjects(answer) {
639
+ return validateSObjectNames(parseSObjects(answer));
640
+ }
641
+ function validateSObjectNames(names) {
642
+ if (names.length === 0) return "Enter at least one sObject, e.g. Account.";
643
+ const invalid = names.filter((name) => !SOBJECT_NAME.test(name));
644
+ return invalid.length > 0 ? `Not a valid sObject API name: ${invalid.join(", ")}` : void 0;
645
+ }
646
+ /** Answers given as flags get the same checks as typed answers. */
647
+ function validatePreset(preset) {
648
+ const errors = [
649
+ preset.sobjects === void 0 ? void 0 : validateSObjectNames(preset.sobjects),
650
+ preset.output === void 0 ? void 0 : validateOutput(preset.output),
651
+ preset.targetOrg === void 0 || /^[\w.@+-]+$/.test(preset.targetOrg) ? void 0 : `Invalid org alias "${preset.targetOrg}".`
652
+ ].filter((error) => error !== void 0);
653
+ if (errors.length > 0) throw new Error(errors.join(" "));
654
+ }
655
+ function validateOutput(answer) {
656
+ return /\.(ts|mts|cts)$/.test(answer) ? void 0 : "The output must be a TypeScript file, e.g. src/generated/sobjects.ts.";
657
+ }
658
+ /** The `$schema` reference written into `sobjectly.config.json`. */
659
+ const JSON_SCHEMA_REFERENCE = "./node_modules/@cerios/salesforce-sobjectly/sobjectly.config.schema.json";
660
+ /** Renders the config file in the chosen format. */
661
+ function renderConfig(answers) {
662
+ return answers.format === "json" ? renderJsonConfig(answers) : renderTypeScriptConfig(answers);
663
+ }
664
+ function renderJsonConfig(answers) {
665
+ const config = {
666
+ $schema: JSON_SCHEMA_REFERENCE,
667
+ apiVersion: answers.apiVersion,
668
+ output: answers.output,
669
+ sobjects: answers.sobjects,
670
+ auth: authSetting(answers),
671
+ picklists: answers.picklists
672
+ };
673
+ return `${JSON.stringify(config, null, " ")}\n`;
674
+ }
675
+ function renderTypeScriptConfig(answers) {
676
+ const envVars = AUTH_ENV_VARS[answers.auth];
677
+ const sobjects = answers.sobjects.map((name) => JSON.stringify(name)).join(", ");
678
+ return [
679
+ "import { defineConfig } from \"@cerios/salesforce-sobjectly/codegen\";",
680
+ "",
681
+ "export default defineConfig({",
682
+ " // The Salesforce REST API version. The generated file exports it as API_VERSION for the client.",
683
+ `\tapiVersion: ${JSON.stringify(answers.apiVersion)},`,
684
+ " // Where the generated types are written, relative to this file.",
685
+ `\toutput: ${JSON.stringify(answers.output)},`,
686
+ " // The sObjects to generate types for.",
687
+ `\tsobjects: [${sobjects}],`,
688
+ `\t// ${authComment(answers.auth, envVars)}`,
689
+ `\tauth: ${renderAuth(answers)},`,
690
+ `\tpicklists: ${JSON.stringify(answers.picklists)},`,
691
+ "});",
692
+ ""
693
+ ].join("\n");
694
+ }
695
+ function authSetting(answers) {
696
+ return answers.auth === "sfCli" && answers.targetOrg ? {
697
+ type: "sfCli",
698
+ targetOrg: answers.targetOrg
699
+ } : { type: answers.auth };
700
+ }
701
+ function renderAuth(answers) {
702
+ const setting = authSetting(answers);
703
+ const targetOrg = setting.targetOrg ? `, targetOrg: ${JSON.stringify(setting.targetOrg)}` : "";
704
+ return `{ type: ${JSON.stringify(setting.type)}${targetOrg} }`;
705
+ }
706
+ function authComment(auth, envVars) {
707
+ return auth === "sfCli" ? "Uses an org you are logged into with the Salesforce CLI (sf org login web)." : `Reads ${formatList(envVars)} from the environment.`;
708
+ }
709
+ async function maybeUpdateEnvExample(options, auth) {
710
+ const path = (0, node_path.join)(options.cwd, ".env.example");
711
+ const content = (0, node_fs.existsSync)(path) ? (0, node_fs.readFileSync)(path, "utf8") : "";
712
+ const missing = AUTH_ENV_VARS[auth].filter((name) => !new RegExp(`^${name}=`, "m").test(content));
713
+ if (missing.length === 0) return false;
714
+ if (!(options.yes === true || await options.prompter.confirm(`Add ${formatList(missing)} to .env.example?`, true))) return false;
715
+ const block = ["# Salesforce credentials for `sobjectly generate`", ...missing.map((name) => `${name}=`)].join("\n");
716
+ const separator = content.length === 0 ? "" : content.endsWith("\n") ? "\n" : "\n\n";
717
+ (0, node_fs.writeFileSync)(path, `${content}${separator}${block}\n`, "utf8");
718
+ options.log("Updated .env.example");
719
+ return true;
720
+ }
721
+ async function maybeAddScript(options) {
722
+ const path = (0, node_path.join)(options.cwd, "package.json");
723
+ if (!(0, node_fs.existsSync)(path)) return false;
724
+ const raw = (0, node_fs.readFileSync)(path, "utf8");
725
+ const bom = raw.startsWith("") ? "" : "";
726
+ let pkg;
727
+ try {
728
+ pkg = JSON.parse(raw.slice(bom.length));
729
+ } catch {
730
+ options.log(`Skipped adding the "${NPM_SCRIPT}" script: package.json is not valid JSON.`);
731
+ return false;
732
+ }
733
+ if (pkg.scripts?.[NPM_SCRIPT]) return false;
734
+ if (!(options.yes === true || await options.prompter.confirm(`Add an npm script "${NPM_SCRIPT}"?`, true))) return false;
735
+ pkg.scripts = {
736
+ ...pkg.scripts,
737
+ [NPM_SCRIPT]: "sobjectly generate"
738
+ };
739
+ const indent = /^[ \t]+/m.exec(raw)?.[0] ?? " ";
740
+ const newline = raw.includes("\r\n") ? "\r\n" : "\n";
741
+ const json = JSON.stringify(pkg, null, indent).replace(/\n/g, newline);
742
+ (0, node_fs.writeFileSync)(path, `${bom}${json}${newline}`, "utf8");
743
+ options.log(`Added the "${NPM_SCRIPT}" script to package.json`);
744
+ return true;
745
+ }
746
+ function printNextSteps(log, answers, scriptAdded, cwd) {
747
+ const cli = answers.auth === "sfCli";
748
+ const envFile = cli ? "" : " --env-file .env";
749
+ const command = scriptAdded ? `npm run ${NPM_SCRIPT}${envFile ? ` --${envFile}` : ""}` : `npx sobjectly generate${envFile}`;
750
+ const gitignore = (0, node_path.join)(cwd, ".gitignore");
751
+ const envIgnored = (0, node_fs.existsSync)(gitignore) && /^\.env\b/m.test((0, node_fs.readFileSync)(gitignore, "utf8"));
752
+ log([
753
+ "",
754
+ "Next steps:",
755
+ cli ? ` 1. Log in with the Salesforce CLI: sf org login web${answers.targetOrg ? ` --alias ${answers.targetOrg}` : " --set-default"}` : ` 1. Put ${formatList(AUTH_ENV_VARS[answers.auth])} in a .env file${envIgnored ? "" : " (and add .env to .gitignore)"}.`,
756
+ ` 2. Generate the types: ${command}`,
757
+ " 3. Use them:",
758
+ ` import { API_VERSION, type SObjectRegistry } from "./${answers.output.replace(/\.(ts|mts|cts)$/, "")}";`,
759
+ " const sf = new SalesforceClient<SObjectRegistry>({ apiVersion: API_VERSION, auth });"
760
+ ].join("\n"));
761
+ }
762
+ function formatList(items) {
763
+ if (items.length <= 1) return items.join("");
764
+ return `${items.slice(0, -1).join(", ")} and ${items.at(-1)}`;
765
+ }
766
+ //#endregion
767
+ //#region src/codegen/prompter.ts
768
+ /**
769
+ * A prompter on stdin/stdout (or the given streams). Lines are queued as they arrive, so piped
770
+ * input (`printf "v66.0\n..." | sobjectly init`) works too. If the input ends before a question
771
+ * is answered, the question rejects.
772
+ */
773
+ function readlinePrompter(input = process.stdin, output = process.stdout) {
774
+ const interactive = input.isTTY === true;
775
+ const rl = (0, node_readline.createInterface)({
776
+ input,
777
+ output,
778
+ terminal: interactive
779
+ });
780
+ const lines = [];
781
+ let waiting;
782
+ let closed = false;
783
+ rl.on("line", (line) => {
784
+ if (waiting) {
785
+ const { resolve } = waiting;
786
+ waiting = void 0;
787
+ resolve(line);
788
+ } else lines.push(line);
789
+ });
790
+ rl.on("close", () => {
791
+ closed = true;
792
+ waiting?.reject(/* @__PURE__ */ new Error("Input ended before all questions were answered."));
793
+ waiting = void 0;
794
+ });
795
+ const readLine = (prompt) => {
796
+ if (closed) output.write(prompt);
797
+ else {
798
+ rl.setPrompt(prompt);
799
+ rl.prompt();
800
+ }
801
+ const queued = lines.shift();
802
+ if (queued !== void 0) {
803
+ if (!interactive) output.write(`${queued}\n`);
804
+ return Promise.resolve(queued);
805
+ }
806
+ if (closed) return Promise.reject(/* @__PURE__ */ new Error("Input ended before all questions were answered."));
807
+ return new Promise((resolve, reject) => {
808
+ waiting = {
809
+ resolve: (line) => {
810
+ if (!interactive) output.write(`${line}\n`);
811
+ resolve(line);
812
+ },
813
+ reject
814
+ };
815
+ });
816
+ };
817
+ const ask = async (question, options = {}) => {
818
+ const suffix = options.defaultValue ? ` (${options.defaultValue})` : "";
819
+ for (;;) {
820
+ const raw = (await readLine(`? ${question}${suffix}: `)).trim();
821
+ const answer = raw === "" ? options.defaultValue ?? "" : raw;
822
+ const error = options.validate?.(answer);
823
+ if (error === void 0) return answer;
824
+ output.write(` ${error}\n`);
825
+ }
826
+ };
827
+ return {
828
+ ask,
829
+ async choose(question, choices, defaultValue) {
830
+ output.write(`? ${question}\n`);
831
+ choices.forEach((choice, index) => {
832
+ output.write(` ${index + 1}) ${choice.label}${choice.value === defaultValue ? " (default)" : ""}\n`);
833
+ });
834
+ return (pick(choices, await ask("Choose a number", {
835
+ defaultValue: String(choices.findIndex((choice) => choice.value === defaultValue) + 1),
836
+ validate: (value) => pick(choices, value) ? void 0 : `Enter a number from 1 to ${choices.length}.`
837
+ })) ?? choices[0]).value;
838
+ },
839
+ async confirm(question, defaultValue) {
840
+ const answer = await ask(`${question} ${defaultValue ? "(Y/n)" : "(y/N)"}`, { validate: (value) => value === "" || /^(y|yes|n|no)$/i.test(value) ? void 0 : "Answer y or n." });
841
+ return answer === "" ? defaultValue : /^y/i.test(answer);
842
+ },
843
+ close() {
844
+ rl.close();
845
+ }
846
+ };
847
+ }
848
+ function pick(choices, answer) {
849
+ const index = Number(answer);
850
+ if (Number.isInteger(index) && index >= 1 && index <= choices.length) return choices[index - 1];
851
+ return choices.find((choice) => choice.value.toLowerCase() === answer.toLowerCase());
852
+ }
853
+ //#endregion
854
+ //#region src/codegen/index.ts
855
+ const silentLogger = {
856
+ info: () => void 0,
857
+ warn: () => void 0
858
+ };
859
+ /** Describes the configured sObjects and writes the generated types to `config.output`. */
860
+ async function generate(input, options = {}) {
861
+ const logger = options.logger ?? silentLogger;
862
+ const built = await buildSource(input, options);
863
+ const { config } = built;
864
+ let source = built.raw;
865
+ if (config.format) source = await config.format(source, built.outputPath);
866
+ if (options.write !== false) {
867
+ await writeAtomically(built.outputPath, source);
868
+ logger.info(`Wrote ${built.sobjects.length} sObject type(s) to ${built.outputPath}`);
869
+ }
870
+ const { contentHash } = readGeneratedHashes(built.raw);
871
+ return {
872
+ outputPath: built.outputPath,
873
+ apiVersion: config.apiVersion,
874
+ sobjects: built.sobjects,
875
+ skipped: built.skipped,
876
+ source,
877
+ contentHash
878
+ };
879
+ }
880
+ /**
881
+ * Checks whether the generated file on disk is up to date with the org, without writing it.
882
+ * Only content counts: a file reformatted by Prettier, oxfmt or the `format` hook is up to date.
883
+ */
884
+ async function checkGenerated(input, options = {}) {
885
+ const built = await buildSource(input, options);
886
+ const empty = {
887
+ outputPath: built.outputPath,
888
+ added: [],
889
+ removed: [],
890
+ changed: []
891
+ };
892
+ if (!(0, node_fs.existsSync)(built.outputPath)) return {
893
+ ...empty,
894
+ upToDate: false,
895
+ reason: "missing"
896
+ };
897
+ const existing = await (0, node_fs_promises.readFile)(built.outputPath, "utf8");
898
+ const expected = readGeneratedHashes(built.raw);
899
+ const actual = readGeneratedHashes(existing);
900
+ if (!actual) {
901
+ const upToDate = normalizeSource(existing) === normalizeSource(built.raw);
902
+ return {
903
+ ...empty,
904
+ upToDate,
905
+ reason: upToDate ? void 0 : "changed"
906
+ };
907
+ }
908
+ if (actual.contentHash === expected.contentHash) return {
909
+ ...empty,
910
+ upToDate: true
911
+ };
912
+ return {
913
+ ...empty,
914
+ upToDate: false,
915
+ reason: "changed",
916
+ ...diffObjects(actual.sobjects, expected.sobjects)
917
+ };
918
+ }
919
+ async function buildSource(input, options) {
920
+ const config = validateConfig(input, { source: "generate()" });
921
+ const { apiVersion } = config;
922
+ const logger = options.logger ?? silentLogger;
923
+ const env = options.env ?? process.env;
924
+ const outputPath = (0, node_path.isAbsolute)(config.output) ? config.output : (0, node_path.resolve)(options.cwd ?? process.cwd(), config.output);
925
+ const client = new require_sf_cli.SalesforceClient({
926
+ auth: resolveAuth(config.auth, env),
927
+ apiVersion,
928
+ transport: config.transport,
929
+ retry: true
930
+ });
931
+ const names = await resolveSObjectNames(client, config, logger);
932
+ logger.info(`Describing ${names.length} sObject(s) with API ${apiVersion}...`);
933
+ const { describes, skipped } = await describeAll(client, names, config, logger);
934
+ return {
935
+ config,
936
+ outputPath,
937
+ raw: generateSource(describes, {
938
+ excludeCreateFields: config.excludeCreateFields,
939
+ excludeUpdateFields: config.excludeUpdateFields,
940
+ picklists: config.picklists,
941
+ constants: config.constants,
942
+ importSource: config.importSource,
943
+ apiVersion
944
+ }),
945
+ sobjects: describes.map((describe) => describe.name).sort(),
946
+ skipped
947
+ };
948
+ }
949
+ function diffObjects(actual, expected) {
950
+ return {
951
+ added: [...expected.keys()].filter((name) => !actual.has(name)),
952
+ removed: [...actual.keys()].filter((name) => !expected.has(name)),
953
+ changed: [...expected.entries()].filter(([name, hash]) => actual.has(name) && actual.get(name) !== hash).map(([name]) => name)
954
+ };
955
+ }
956
+ /** Strips comments, whitespace, quote style, semicolons and commas, so only content remains. */
957
+ function normalizeSource(source) {
958
+ let result = "";
959
+ let index = 0;
960
+ while (index < source.length) {
961
+ const char = source[index];
962
+ const next = source[index + 1];
963
+ if (char === "\"" || char === "'") {
964
+ let end = index + 1;
965
+ while (end < source.length && source[end] !== char) end += source[end] === "\\" ? 2 : 1;
966
+ result += `"${source.slice(index + 1, end).replace(/\\'/g, "'").replace(/"/g, "\\\"")}"`;
967
+ index = end + 1;
968
+ } else if (char === "/" && next === "/") index = source.indexOf("\n", index) === -1 ? source.length : source.indexOf("\n", index);
969
+ else if (char === "/" && next === "*") {
970
+ const end = source.indexOf("*/", index + 2);
971
+ index = end === -1 ? source.length : end + 2;
972
+ } else {
973
+ if (!/[\s;,]/.test(char)) result += char;
974
+ index++;
975
+ }
976
+ }
977
+ return result;
978
+ }
979
+ /** Turns the config's auth setting into an auth provider, reading env-based credentials. */
980
+ function resolveAuth(auth, env) {
981
+ const setting = auth ?? { type: "clientCredentials" };
982
+ if ("getToken" in setting) return setting;
983
+ const read = (name) => {
984
+ const value = env[name];
985
+ if (!value) throw new Error(`Environment variable ${name} is not set.`);
986
+ return value;
987
+ };
988
+ switch (setting.type) {
989
+ case "accessToken": return require_sf_cli.accessToken({
990
+ accessToken: read(setting.accessTokenEnv ?? "SF_ACCESS_TOKEN"),
991
+ instanceUrl: read(setting.instanceUrlEnv ?? "SF_INSTANCE_URL")
992
+ });
993
+ case "jwtBearer": return jwtBearerFromEnv(setting, env, read);
994
+ case "sfCli": return require_sf_cli.sfCli({ targetOrg: setting.targetOrg });
995
+ case "clientCredentials": return require_sf_cli.clientCredentials({
996
+ loginUrl: read(setting.loginUrlEnv ?? "SF_LOGIN_URL"),
997
+ clientId: read(setting.clientIdEnv ?? "SF_CLIENT_ID"),
998
+ clientSecret: read(setting.clientSecretEnv ?? "SF_CLIENT_SECRET")
999
+ });
1000
+ default: throw new Error(`Unknown codegen auth type "${setting.type}".`);
1001
+ }
1002
+ }
1003
+ function jwtBearerFromEnv(setting, env, read) {
1004
+ const keyEnv = setting.privateKeyEnv ?? "SF_PRIVATE_KEY";
1005
+ const privateKey = env[keyEnv] ? read(keyEnv) : (0, node_fs.readFileSync)(read(setting.privateKeyPathEnv ?? "SF_PRIVATE_KEY_PATH"), "utf8");
1006
+ return require_sf_cli.jwtBearer({
1007
+ loginUrl: read(setting.loginUrlEnv ?? "SF_LOGIN_URL"),
1008
+ clientId: read(setting.clientIdEnv ?? "SF_CLIENT_ID"),
1009
+ username: read(setting.usernameEnv ?? "SF_USERNAME"),
1010
+ privateKey
1011
+ });
1012
+ }
1013
+ async function resolveSObjectNames(client, config, logger) {
1014
+ const exclude = new Set((config.exclude ?? []).map((name) => name.toLowerCase()));
1015
+ if (config.sobjects && config.sobjects.length > 0) {
1016
+ const unique = /* @__PURE__ */ new Map();
1017
+ for (const name of config.sobjects) if (!unique.has(name.toLowerCase())) unique.set(name.toLowerCase(), name);
1018
+ return [...unique.entries()].filter(([key]) => !exclude.has(key)).map(([, name]) => name);
1019
+ }
1020
+ logger.warn("No `sobjects` configured: generating every queryable sObject. Consider an explicit list.");
1021
+ return (await client.describeGlobal()).sobjects.filter((sobject) => sobject.queryable && !sobject.deprecatedAndHidden && !exclude.has(sobject.name.toLowerCase())).map((sobject) => sobject.name);
1022
+ }
1023
+ async function describeAll(client, names, config, logger) {
1024
+ const concurrency = Math.max(1, config.concurrency ?? 10);
1025
+ const describes = [];
1026
+ const skipped = [];
1027
+ let next = 0;
1028
+ let done = 0;
1029
+ const failed = new AbortController();
1030
+ const worker = async () => {
1031
+ while (next < names.length && !failed.signal.aborted) {
1032
+ const name = names[next++];
1033
+ try {
1034
+ const describe = await client.sobject(name).describe({ signal: failed.signal });
1035
+ describes.push(describe);
1036
+ } catch (error) {
1037
+ if (failed.signal.aborted) return;
1038
+ const message = error instanceof Error ? error.message : String(error);
1039
+ if (!config.continueOnDescribeError) {
1040
+ failed.abort();
1041
+ throw new Error(`Describing sObject "${name}" failed: ${message}`, { cause: error });
1042
+ }
1043
+ logger.warn(`Skipping sObject "${name}": ${message}`);
1044
+ skipped.push({
1045
+ name,
1046
+ error: message
1047
+ });
1048
+ }
1049
+ done++;
1050
+ if (done % 10 === 0 || done === names.length) logger.info(`Described ${done}/${names.length} sObject(s)`);
1051
+ }
1052
+ };
1053
+ await Promise.all(Array.from({ length: Math.min(concurrency, names.length) }, () => worker()));
1054
+ return {
1055
+ describes: [...new Map(describes.map((describe) => [describe.name, describe])).values()],
1056
+ skipped
1057
+ };
1058
+ }
1059
+ async function writeAtomically(filePath, content) {
1060
+ await (0, node_fs_promises.mkdir)((0, node_path.dirname)(filePath), { recursive: true });
1061
+ const temporary = `${filePath}.${process.pid}.${Date.now()}.tmp`;
1062
+ try {
1063
+ await (0, node_fs_promises.writeFile)(temporary, content, "utf8");
1064
+ await renameWithRetry(temporary, filePath);
1065
+ } catch (error) {
1066
+ await (0, node_fs_promises.rm)(temporary, { force: true });
1067
+ throw error;
1068
+ }
1069
+ }
1070
+ /** On Windows a virus scanner or indexer can briefly lock the target file; retry a few times. */
1071
+ async function renameWithRetry(from, to) {
1072
+ for (let attempt = 1;; attempt++) try {
1073
+ await (0, node_fs_promises.rename)(from, to);
1074
+ return;
1075
+ } catch (error) {
1076
+ const code = error.code;
1077
+ if (attempt >= 5 || code !== "EPERM" && code !== "EBUSY" && code !== "EACCES") throw error;
1078
+ await new Promise((resolve) => setTimeout(resolve, 50 * attempt));
1079
+ }
1080
+ }
1081
+ //#endregion
1082
+ Object.defineProperty(exports, "AUTH_ENV_VARS", {
1083
+ enumerable: true,
1084
+ get: function() {
1085
+ return AUTH_ENV_VARS;
1086
+ }
1087
+ });
1088
+ Object.defineProperty(exports, "AUTH_OPTIONS", {
1089
+ enumerable: true,
1090
+ get: function() {
1091
+ return AUTH_OPTIONS;
1092
+ }
1093
+ });
1094
+ Object.defineProperty(exports, "CONFIG_FILE_NAMES", {
1095
+ enumerable: true,
1096
+ get: function() {
1097
+ return CONFIG_FILE_NAMES;
1098
+ }
1099
+ });
1100
+ Object.defineProperty(exports, "CONFIG_OPTIONS", {
1101
+ enumerable: true,
1102
+ get: function() {
1103
+ return CONFIG_OPTIONS;
1104
+ }
1105
+ });
1106
+ Object.defineProperty(exports, "CodegenConfigError", {
1107
+ enumerable: true,
1108
+ get: function() {
1109
+ return CodegenConfigError;
1110
+ }
1111
+ });
1112
+ Object.defineProperty(exports, "FIELD_TYPE_MAP", {
1113
+ enumerable: true,
1114
+ get: function() {
1115
+ return FIELD_TYPE_MAP;
1116
+ }
1117
+ });
1118
+ Object.defineProperty(exports, "checkGenerated", {
1119
+ enumerable: true,
1120
+ get: function() {
1121
+ return checkGenerated;
1122
+ }
1123
+ });
1124
+ Object.defineProperty(exports, "defineConfig", {
1125
+ enumerable: true,
1126
+ get: function() {
1127
+ return defineConfig;
1128
+ }
1129
+ });
1130
+ Object.defineProperty(exports, "findConfigFile", {
1131
+ enumerable: true,
1132
+ get: function() {
1133
+ return findConfigFile;
1134
+ }
1135
+ });
1136
+ Object.defineProperty(exports, "findConfigFiles", {
1137
+ enumerable: true,
1138
+ get: function() {
1139
+ return findConfigFiles;
1140
+ }
1141
+ });
1142
+ Object.defineProperty(exports, "generate", {
1143
+ enumerable: true,
1144
+ get: function() {
1145
+ return generate;
1146
+ }
1147
+ });
1148
+ Object.defineProperty(exports, "generateSource", {
1149
+ enumerable: true,
1150
+ get: function() {
1151
+ return generateSource;
1152
+ }
1153
+ });
1154
+ Object.defineProperty(exports, "loadConfig", {
1155
+ enumerable: true,
1156
+ get: function() {
1157
+ return loadConfig;
1158
+ }
1159
+ });
1160
+ Object.defineProperty(exports, "mapFieldType", {
1161
+ enumerable: true,
1162
+ get: function() {
1163
+ return mapFieldType;
1164
+ }
1165
+ });
1166
+ Object.defineProperty(exports, "parseSObjects", {
1167
+ enumerable: true,
1168
+ get: function() {
1169
+ return parseSObjects;
1170
+ }
1171
+ });
1172
+ Object.defineProperty(exports, "readGeneratedHashes", {
1173
+ enumerable: true,
1174
+ get: function() {
1175
+ return readGeneratedHashes;
1176
+ }
1177
+ });
1178
+ Object.defineProperty(exports, "readlinePrompter", {
1179
+ enumerable: true,
1180
+ get: function() {
1181
+ return readlinePrompter;
1182
+ }
1183
+ });
1184
+ Object.defineProperty(exports, "renderConfig", {
1185
+ enumerable: true,
1186
+ get: function() {
1187
+ return renderConfig;
1188
+ }
1189
+ });
1190
+ Object.defineProperty(exports, "resolveAuth", {
1191
+ enumerable: true,
1192
+ get: function() {
1193
+ return resolveAuth;
1194
+ }
1195
+ });
1196
+ Object.defineProperty(exports, "runInit", {
1197
+ enumerable: true,
1198
+ get: function() {
1199
+ return runInit;
1200
+ }
1201
+ });
1202
+ Object.defineProperty(exports, "validateConfig", {
1203
+ enumerable: true,
1204
+ get: function() {
1205
+ return validateConfig;
1206
+ }
1207
+ });
1208
+
1209
+ //# sourceMappingURL=codegen-BgDgU2RR.cjs.map