@cerios/salesforce-sobjectly 1.0.0 → 1.2.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.
@@ -1,22 +1,50 @@
1
- import { Mt as init_api_version, Nt as isApiVersion, Pt as normalizeApiVersion, Rt as __esmMin, d as init_client, i as clientCredentials, n as sfCli, o as init_providers, r as accessToken, s as jwtBearer, t as init_sf_cli, u as SalesforceClient } from "./sf-cli-CtWtdkJ0.mjs";
1
+ import { Mt as init_api_version, Nt as isApiVersion, Pt as normalizeApiVersion, Rt as __esmMin, d as init_client, i as clientCredentials, n as sfCli, o as init_providers, r as accessToken, s as jwtBearer, t as init_sf_cli, u as SalesforceClient } from "./sf-cli-D2PgjErx.mjs";
2
2
  import { createHash } from "node:crypto";
3
3
  import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
5
5
  import { dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
6
6
  import { createJiti } from "jiti";
7
7
  import { createInterface } from "node:readline";
8
+ //#region src/codegen/emit.ts
9
+ /** Fields sorted by name, `Id` first, so the output is deterministic. */
10
+ function sortedFields(describe) {
11
+ return [...describe.fields].sort((a, b) => a.name === "Id" ? -1 : b.name === "Id" ? 1 : compare(a.name, b.name));
12
+ }
13
+ /** An object key: the bare name when it is an identifier, otherwise a quoted string. */
14
+ function propertyKey(name) {
15
+ return IDENTIFIER$1.test(name) ? name : JSON.stringify(name);
16
+ }
17
+ function escapeComment(value) {
18
+ return String(value ?? "").replace(/\*\//g, "*\\/").replace(/\r?\n/g, " ");
19
+ }
20
+ function compare(a, b) {
21
+ return a < b ? -1 : a > b ? 1 : 0;
22
+ }
23
+ var IDENTIFIER$1;
24
+ var init_emit = __esmMin((() => {
25
+ IDENTIFIER$1 = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
26
+ }));
27
+ //#endregion
8
28
  //#region src/codegen/type-mapper.ts
9
- /** Returns the TypeScript type for a field, including `| null` for nillable fields. */
10
- function mapFieldType(field, picklists = "union") {
11
- const base = picklistType(field, picklists) ?? FIELD_TYPE_MAP[field.type] ?? "unknown";
29
+ /**
30
+ * Returns the TypeScript type for a field, including `| null` for nillable fields. With picklist
31
+ * mode `"const"` or `"enum"`, `typeName` is the field's named picklist type; it replaces the union.
32
+ */
33
+ function mapFieldType(field, picklists = "union", typeName) {
34
+ const base = picklistType(field, picklists, typeName) ?? FIELD_TYPE_MAP[field.type] ?? "unknown";
12
35
  return field.nillable && base !== "unknown" ? `${base} | null` : base;
13
36
  }
14
- function picklistType(field, picklists) {
15
- if (picklists !== "union" || field.type !== "picklist" && field.type !== "combobox") return;
16
- const values = [...new Set((field.picklistValues ?? []).filter((entry) => entry.active).map((entry) => entry.value))];
37
+ function picklistType(field, picklists, typeName) {
38
+ if (picklists === "string" || field.type !== "picklist" && field.type !== "combobox") return;
39
+ const values = activePicklistValues(field);
17
40
  if (values.length === 0) return;
18
- const union = values.map((value) => JSON.stringify(value)).join(" | ");
19
- return field.restrictedPicklist && field.type === "picklist" ? union : `${union} | (string & {})`;
41
+ const members = picklists !== "union" && typeName !== void 0 ? typeName : values.map((value) => JSON.stringify(value)).join(" | ");
42
+ return field.restrictedPicklist && field.type === "picklist" ? members : `${members} | (string & {})`;
43
+ }
44
+ /** The distinct active values of a picklist, multi-select picklist or combobox, in describe order. */
45
+ function activePicklistValues(field) {
46
+ if (field.type !== "picklist" && field.type !== "multipicklist" && field.type !== "combobox") return [];
47
+ return [...new Set((field.picklistValues ?? []).filter((entry) => entry.active).map((entry) => entry.value))];
20
48
  }
21
49
  var ADDRESS_TYPE, GEOLOCATION_TYPE, FIELD_TYPE_MAP;
22
50
  var init_type_mapper = __esmMin((() => {
@@ -54,24 +82,151 @@ var init_type_mapper = __esmMin((() => {
54
82
  };
55
83
  }));
56
84
  //#endregion
85
+ //#region src/codegen/picklist-types.ts
86
+ function isNamedPicklistMode(mode) {
87
+ return mode === "const" || mode === "enum";
88
+ }
89
+ /**
90
+ * Names the type of every picklist with active values: sObject and field in PascalCase, e.g.
91
+ * `Case.Status` -> `CaseStatus` and `Case.Status__c` -> `CaseStatusCustom`. A name that is in
92
+ * `taken` (the sObjects and other names the file defines) or already used by another picklist gets
93
+ * the suffix `Picklist`; when that is taken too, this throws.
94
+ */
95
+ function picklistTypeNames(describes, taken) {
96
+ const used = new Map([...taken].map((name) => [name, void 0]));
97
+ const result = /* @__PURE__ */ new Map();
98
+ for (const describe of [...describes].sort((a, b) => compare(a.name, b.name))) {
99
+ const names = /* @__PURE__ */ new Map();
100
+ for (const field of sortedFields(describe).filter((item) => activePicklistValues(item).length > 0)) {
101
+ const base = `${pascalApiName(describe.name, "sobject")}${pascalApiName(field.name, "field")}`;
102
+ if (IDENTIFIER$1.test(base)) {
103
+ const picklist = `${describe.name}.${field.name}`;
104
+ const name = resolveName(base, picklist, used);
105
+ used.set(name, picklist);
106
+ names.set(field.name, name);
107
+ }
108
+ }
109
+ if (names.size > 0) result.set(describe.name, names);
110
+ }
111
+ return result;
112
+ }
113
+ function resolveName(base, picklist, used) {
114
+ const name = used.has(base) ? `${base}${CLASH_SUFFIX}` : base;
115
+ if (used.has(name)) {
116
+ const owner = used.get(name);
117
+ const other = owner === void 0 ? "a type the generated file defines" : `the picklist type for ${owner}`;
118
+ throw new Error(`The picklist type "${name}" for ${picklist} clashes with ${other}; exclude one of the sObjects or use picklists: "union".`);
119
+ }
120
+ return name;
121
+ }
122
+ /**
123
+ * An API name in PascalCase without underscores. A field's `__c` becomes `Custom`
124
+ * (`Lead_Source__c` -> `LeadSourceCustom`) and an sObject's is dropped (`My_Object__c` ->
125
+ * `MyObject`). Other suffixes are kept: `Foo__pc` -> `FooPc`, `Config__mdt` -> `ConfigMdt`.
126
+ */
127
+ function pascalApiName(name, kind) {
128
+ const match = /^(.+?)__([a-z]+)$/.exec(name);
129
+ const body = match ? match[1] : name;
130
+ const suffix = match ? match[2] : "";
131
+ const suffixPart = suffix === "c" ? kind === "field" ? "Custom" : "" : capitalize(suffix);
132
+ return `${body.split("_").filter((part) => part.length > 0).map(capitalize).join("")}${suffixPart}`;
133
+ }
134
+ function capitalize(value) {
135
+ return value.charAt(0).toUpperCase() + value.slice(1);
136
+ }
137
+ /**
138
+ * The member names for a picklist's values, in the same order. A value that is an identifier is
139
+ * its own name; other values are sanitized (`"Closed Won"` -> `Closed_Won`, `"3rd Party"` ->
140
+ * `_3rd_Party`). When the sanitized name is empty or taken, the value itself is the (quoted) name.
141
+ */
142
+ function picklistMemberKeys(values, mode) {
143
+ const used = /* @__PURE__ */ new Set();
144
+ const claim = (key) => {
145
+ used.add(key);
146
+ return key;
147
+ };
148
+ const usable = (key) => key.length > 0 && key !== "__proto__" && !used.has(key) && (mode === "const" || !isNumericName(key));
149
+ const own = values.map((value) => IDENTIFIER$1.test(value) && usable(value) ? claim(value) : void 0);
150
+ return values.map((value, index) => {
151
+ const ownKey = own[index];
152
+ if (ownKey !== void 0) return ownKey;
153
+ const sanitized = sanitizeKey(value);
154
+ if (usable(sanitized)) return claim(sanitized);
155
+ if (usable(value)) return claim(value);
156
+ let counter = 2;
157
+ while (!usable(`${sanitized || "_"}_${counter}`)) counter++;
158
+ return claim(`${sanitized || "_"}_${counter}`);
159
+ });
160
+ }
161
+ function sanitizeKey(value) {
162
+ const key = value.normalize("NFKD").replace(/\p{M}/gu, "").replace(/[^A-Za-z0-9_]+/g, "_").replace(/^_+|_+$/g, "");
163
+ return /^[0-9]/.test(key) ? `_${key}` : key;
164
+ }
165
+ /** Names TypeScript rejects as enum members because they are numeric, e.g. `"1"` or `"-1"`. */
166
+ function isNumericName(key) {
167
+ const number = Number(key);
168
+ return Number.isFinite(number) && String(number) === key;
169
+ }
170
+ /** The named type declarations for one sObject's picklists, in field order; `""` when there are none. */
171
+ function picklistDeclarations(describe, names, mode) {
172
+ if (!names) return "";
173
+ return sortedFields(describe).flatMap((field) => {
174
+ const name = names.get(field.name);
175
+ return name === void 0 ? [] : [declaration(describe.name, field, name, mode)];
176
+ }).join("\n\n");
177
+ }
178
+ function declaration(sobject, field, name, mode) {
179
+ const values = activePicklistValues(field);
180
+ const keys = picklistMemberKeys(values, mode);
181
+ const doc = `/** Active values of ${escapeComment(`${sobject}.${field.name}`)} (${escapeComment(field.type)}). */`;
182
+ if (mode === "enum") {
183
+ const members = values.map((value, index) => `\t${propertyKey(keys[index])} = ${JSON.stringify(value)},`);
184
+ return [
185
+ doc,
186
+ `export enum ${name} {`,
187
+ ...members,
188
+ "}"
189
+ ].join("\n");
190
+ }
191
+ const members = values.map((value, index) => `\t${propertyKey(keys[index])}: ${JSON.stringify(value)},`);
192
+ return [
193
+ doc,
194
+ `export const ${name} = {`,
195
+ ...members,
196
+ "} as const;",
197
+ `export type ${name} = (typeof ${name})[keyof typeof ${name}];`
198
+ ].join("\n");
199
+ }
200
+ var CLASH_SUFFIX;
201
+ var init_picklist_types = __esmMin((() => {
202
+ init_emit();
203
+ init_type_mapper();
204
+ CLASH_SUFFIX = "Picklist";
205
+ }));
206
+ //#endregion
57
207
  //#region src/codegen/generator.ts
58
208
  /**
59
209
  * Generates the TypeScript source for a set of sObject describes: a read interface, create
60
- * and update input types per sObject, and the `SObjectRegistry` that ties them together.
210
+ * and update input types per sObject, and the `SObjectRegistry` that ties them together. With
211
+ * picklist mode `"const"` or `"enum"`, each picklist also gets a named type before its sObject.
61
212
  * The output is deterministic: sObjects are sorted by name and fields by name (`Id` first).
62
213
  */
63
214
  function generateSource(describes, options = {}) {
64
215
  const sorted = [...describes].sort((a, b) => compare(a.name, b.name));
65
- assertGeneratableNames(sorted);
216
+ const definedNames = assertGeneratableNames(sorted);
66
217
  const names = new Set(sorted.map((describe) => describe.name));
218
+ const picklists = options.picklists ?? "union";
219
+ const picklistTypes = isNamedPicklistMode(picklists) ? picklistTypeNames(sorted, definedNames) : void 0;
67
220
  const sections = [imports(sorted, options.importSource ?? "@cerios/salesforce-sobjectly")];
68
221
  const objectHashes = [];
69
222
  for (const describe of sorted) {
223
+ const typeNames = picklistTypes?.get(describe.name);
70
224
  const objectSections = [
71
- readInterface(describe, options.picklists ?? "union"),
225
+ isNamedPicklistMode(picklists) ? picklistDeclarations(describe, typeNames, picklists) : "",
226
+ readInterface(describe, picklists, typeNames),
72
227
  inputType(describe, "CreateInput", "createable", lookup(options.excludeCreateFields, describe.name)),
73
228
  inputType(describe, "UpdateInput", "updateable", lookup(options.excludeUpdateFields, describe.name))
74
- ];
229
+ ].filter((section) => section.length > 0);
75
230
  sections.push(...objectSections);
76
231
  const fingerprint = [
77
232
  ...objectSections,
@@ -86,7 +241,10 @@ function generateSource(describes, options = {}) {
86
241
  const body = sections.filter((section) => section.length > 0).join("\n\n");
87
242
  return `${header(sha256(body), objectHashes)}\n\n${body}\n`;
88
243
  }
89
- /** Fails with a clear message for sObject names the generated TypeScript can't represent. */
244
+ /**
245
+ * Fails with a clear message for sObject names the generated TypeScript can't represent. Returns
246
+ * every name the file defines apart from picklist types, so those can avoid them.
247
+ */
90
248
  function assertGeneratableNames(describes) {
91
249
  const seen = /* @__PURE__ */ new Map();
92
250
  const generated = new Set(describes.flatMap((describe) => [`${describe.name}CreateInput`, `${describe.name}UpdateInput`]));
@@ -97,6 +255,11 @@ function assertGeneratableNames(describes) {
97
255
  if (previous !== void 0) throw new Error(`sObjects "${previous}" and "${name}" differ only in case; list each sObject once.`);
98
256
  seen.set(name.toLowerCase(), name);
99
257
  }
258
+ return /* @__PURE__ */ new Set([
259
+ ...RESERVED_TYPE_NAMES,
260
+ ...generated,
261
+ ...describes.map((describe) => describe.name)
262
+ ]);
100
263
  }
101
264
  /** Looks up a per-sObject option case-insensitively (sObject names are case-insensitive in Salesforce). */
102
265
  function lookup(map, name) {
@@ -141,12 +304,11 @@ function imports(describes, importSource) {
141
304
  if (fields.some((field) => field.type === "location")) used.push(GEOLOCATION_TYPE);
142
305
  return used.length > 0 ? `import type { ${used.join(", ")} } from ${JSON.stringify(importSource)};` : "";
143
306
  }
144
- function sortedFields(describe) {
145
- return [...describe.fields].sort((a, b) => a.name === "Id" ? -1 : b.name === "Id" ? 1 : compare(a.name, b.name));
146
- }
147
- function readInterface(describe, picklists) {
307
+ function readInterface(describe, picklists, typeNames) {
148
308
  const lines = sortedFields(describe).map((field) => {
149
- return `${`\t/** ${escapeComment(field.label)} (${escapeComment(field.type)}) */`}\n\t${propertyKey(field.name)}: ${mapFieldType(field, picklists)};`;
309
+ const comment = `\t/** ${escapeComment(field.label)} (${escapeComment(field.type)}) */`;
310
+ const type = mapFieldType(field, picklists, typeNames?.get(field.name));
311
+ return `${comment}\n\t${propertyKey(field.name)}: ${type};`;
150
312
  });
151
313
  return `${describe.label ? `/** ${escapeComment(describe.label)} */\n` : ""}export interface ${describe.name} {\n${lines.join("\n")}\n}`;
152
314
  }
@@ -233,10 +395,6 @@ function recordTypeConstants(describes) {
233
395
  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(", ")}],`);
234
396
  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");
235
397
  }
236
- function activePicklistValues(field) {
237
- if (field.type !== "picklist" && field.type !== "multipicklist" && field.type !== "combobox") return [];
238
- return [...new Set((field.picklistValues ?? []).filter((entry) => entry.active).map((entry) => entry.value))];
239
- }
240
398
  function unionOrNever(values) {
241
399
  return values.length > 0 ? values.map((value) => JSON.stringify(value)).join(" | ") : "never";
242
400
  }
@@ -259,17 +417,10 @@ function relationshipMap(entries) {
259
417
  function keyUnion(names) {
260
418
  return names.map((name) => JSON.stringify(name)).join(" | ");
261
419
  }
262
- function propertyKey(name) {
263
- return IDENTIFIER$1.test(name) ? name : JSON.stringify(name);
264
- }
265
- function escapeComment(value) {
266
- return String(value ?? "").replace(/\*\//g, "*\\/").replace(/\r?\n/g, " ");
267
- }
268
- function compare(a, b) {
269
- return a < b ? -1 : a > b ? 1 : 0;
270
- }
271
- var FIELD_KINDS, IDENTIFIER$1, RESERVED_TYPE_NAMES, CONTENT_HASH_PREFIX, OBJECT_HASHES_PREFIX, OBJECT_HASH_LENGTH;
420
+ var FIELD_KINDS, RESERVED_TYPE_NAMES, CONTENT_HASH_PREFIX, OBJECT_HASHES_PREFIX, OBJECT_HASH_LENGTH;
272
421
  var init_generator = __esmMin((() => {
422
+ init_emit();
423
+ init_picklist_types();
273
424
  init_type_mapper();
274
425
  FIELD_KINDS = /* @__PURE__ */ new Set([
275
426
  "date",
@@ -277,7 +428,6 @@ var init_generator = __esmMin((() => {
277
428
  "time",
278
429
  "multipicklist"
279
430
  ]);
280
- IDENTIFIER$1 = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
281
431
  RESERVED_TYPE_NAMES = /* @__PURE__ */ new Set([
282
432
  "Record",
283
433
  "Pick",
@@ -356,8 +506,17 @@ function validateAuth(value, { json, problem }) {
356
506
  problem(`auth.type: must be one of ${Object.keys(AUTH_OPTIONS).join(", ")}, got ${show(type)}.`);
357
507
  return;
358
508
  }
359
- 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(", ")}.`);
360
- else if (typeof option !== "string" || option.length === 0) problem(`auth.${key}: must be a non-empty string, got ${show(option)}.`);
509
+ for (const [key, option] of Object.entries(rest)) if (!allowed.includes(key)) {
510
+ const valid = allowed.filter((name) => !(name in DEPRECATED_AUTH_OPTIONS));
511
+ problem(`auth.${key}: not an option of "${String(type)}". Valid options: ${valid.join(", ")}.`);
512
+ } else validateAuthOption(key, option, rest, problem);
513
+ }
514
+ function validateAuthOption(key, option, setting, problem) {
515
+ const replacement = DEPRECATED_AUTH_OPTIONS[key];
516
+ if (replacement !== void 0 && replacement in setting) problem(`auth.${key}: use either ${replacement} or ${key}, not both.`);
517
+ else if (replacement !== void 0) {
518
+ if (typeof option !== "string" || option.length === 0) problem(`auth.${key}: must be a non-empty string, got ${show(option)}.`);
519
+ } else if (option !== void 0 && typeof option !== "string") problem(`auth.${key}: must be a string, got ${show(option)}.`);
361
520
  }
362
521
  function show(value) {
363
522
  if (value === void 0) return "undefined";
@@ -368,7 +527,7 @@ function show(value) {
368
527
  return typeof value;
369
528
  }
370
529
  }
371
- var CodegenConfigError, AUTH_OPTIONS, PICKLIST_MODES, IDENTIFIER, VALIDATORS, CONFIG_OPTIONS;
530
+ var CodegenConfigError, AUTH_OPTIONS, DEPRECATED_AUTH_OPTIONS, PICKLIST_MODES, IDENTIFIER, VALIDATORS, CONFIG_OPTIONS;
372
531
  var init_validate_config = __esmMin((() => {
373
532
  init_api_version();
374
533
  CodegenConfigError = class extends Error {
@@ -383,12 +542,25 @@ var init_validate_config = __esmMin((() => {
383
542
  };
384
543
  AUTH_OPTIONS = {
385
544
  clientCredentials: [
545
+ "loginUrl",
546
+ "clientId",
547
+ "clientSecret",
386
548
  "loginUrlEnv",
387
549
  "clientIdEnv",
388
550
  "clientSecretEnv"
389
551
  ],
390
- accessToken: ["accessTokenEnv", "instanceUrlEnv"],
552
+ accessToken: [
553
+ "accessToken",
554
+ "instanceUrl",
555
+ "accessTokenEnv",
556
+ "instanceUrlEnv"
557
+ ],
391
558
  jwtBearer: [
559
+ "loginUrl",
560
+ "clientId",
561
+ "username",
562
+ "privateKey",
563
+ "privateKeyPath",
392
564
  "loginUrlEnv",
393
565
  "clientIdEnv",
394
566
  "usernameEnv",
@@ -397,7 +569,22 @@ var init_validate_config = __esmMin((() => {
397
569
  ],
398
570
  sfCli: ["targetOrg"]
399
571
  };
400
- PICKLIST_MODES = ["union", "string"];
572
+ DEPRECATED_AUTH_OPTIONS = {
573
+ loginUrlEnv: "loginUrl",
574
+ clientIdEnv: "clientId",
575
+ clientSecretEnv: "clientSecret",
576
+ accessTokenEnv: "accessToken",
577
+ instanceUrlEnv: "instanceUrl",
578
+ usernameEnv: "username",
579
+ privateKeyEnv: "privateKey",
580
+ privateKeyPathEnv: "privateKeyPath"
581
+ };
582
+ PICKLIST_MODES = [
583
+ "union",
584
+ "string",
585
+ "const",
586
+ "enum"
587
+ ];
401
588
  IDENTIFIER = /^[A-Za-z][A-Za-z0-9_]*$/;
402
589
  VALIDATORS = {
403
590
  $schema: (value, { problem }) => {
@@ -417,7 +604,7 @@ var init_validate_config = __esmMin((() => {
417
604
  excludeCreateFields: (value, context) => validateFieldMap("excludeCreateFields", value, context),
418
605
  excludeUpdateFields: (value, context) => validateFieldMap("excludeUpdateFields", value, context),
419
606
  picklists: (value, { problem }) => {
420
- if (!PICKLIST_MODES.includes(value)) problem(`picklists: must be "union" or "string", got ${show(value)}.`);
607
+ if (!PICKLIST_MODES.includes(value)) problem(`picklists: must be one of ${PICKLIST_MODES.join(", ")}, got ${show(value)}.`);
421
608
  },
422
609
  constants: (value, { problem }) => {
423
610
  if (typeof value !== "boolean") problem(`constants: must be true or false, got ${show(value)}.`);
@@ -443,25 +630,6 @@ var init_validate_config = __esmMin((() => {
443
630
  CONFIG_OPTIONS = Object.keys(VALIDATORS).filter((key) => key !== "$schema");
444
631
  }));
445
632
  //#endregion
446
- //#region src/codegen/config.ts
447
- /**
448
- * Type-checks a `sobjectly.config.ts`. The listed `sobjects` become a literal union, so a typo in
449
- * a per-sObject option is a compile error:
450
- *
451
- * ```ts
452
- * export default defineConfig({
453
- * apiVersion: "v66.0",
454
- * output: "src/generated/sobjects.ts",
455
- * sobjects: ["Account", "Contact"],
456
- * excludeCreateFields: { Acount: ["Name"] }, // ❌ "Acount" is not in sobjects
457
- * });
458
- * ```
459
- */
460
- function defineConfig(config) {
461
- return config;
462
- }
463
- var init_config = __esmMin((() => {}));
464
- //#endregion
465
633
  //#region src/codegen/load-config.ts
466
634
  /** Looks for a `sobjectly.config.{ts,mts,cts,json}` file in `cwd`; the first match in lookup order wins. */
467
635
  function findConfigFile(cwd = process.cwd()) {
@@ -516,6 +684,25 @@ var init_load_config = __esmMin((() => {
516
684
  ]);
517
685
  }));
518
686
  //#endregion
687
+ //#region src/codegen/config.ts
688
+ /**
689
+ * Type-checks a `sobjectly.config.ts`. The listed `sobjects` become a literal union, so a typo in
690
+ * a per-sObject option is a compile error:
691
+ *
692
+ * ```ts
693
+ * export default defineConfig({
694
+ * apiVersion: "v66.0",
695
+ * output: "src/generated/sobjects.ts",
696
+ * sobjects: ["Account", "Contact"],
697
+ * excludeCreateFields: { Acount: ["Name"] }, // ❌ "Acount" is not in sobjects
698
+ * });
699
+ * ```
700
+ */
701
+ function defineConfig(config) {
702
+ return config;
703
+ }
704
+ var init_config = __esmMin((() => {}));
705
+ //#endregion
519
706
  //#region src/codegen/init.ts
520
707
  /**
521
708
  * Creates a `sobjectly.config` file by asking for the API version, output path, sObjects, auth
@@ -630,7 +817,6 @@ function renderJsonConfig(answers) {
630
817
  return `${JSON.stringify(config, null, " ")}\n`;
631
818
  }
632
819
  function renderTypeScriptConfig(answers) {
633
- const envVars = AUTH_ENV_VARS[answers.auth];
634
820
  const sobjects = answers.sobjects.map((name) => JSON.stringify(name)).join(", ");
635
821
  return [
636
822
  "import { defineConfig } from \"@cerios/salesforce-sobjectly/codegen\";",
@@ -642,26 +828,38 @@ function renderTypeScriptConfig(answers) {
642
828
  `\toutput: ${JSON.stringify(answers.output)},`,
643
829
  " // The sObjects to generate types for.",
644
830
  `\tsobjects: [${sobjects}],`,
645
- `\t// ${authComment(answers.auth, envVars)}`,
831
+ `\t// ${authComment(answers.auth)}`,
646
832
  `\tauth: ${renderAuth(answers)},`,
647
833
  `\tpicklists: ${JSON.stringify(answers.picklists)},`,
648
834
  "});",
649
835
  ""
650
836
  ].join("\n");
651
837
  }
838
+ /** The JSON auth setting: credentials are `"${NAME}"` placeholders, read from the environment. */
652
839
  function authSetting(answers) {
653
- return answers.auth === "sfCli" && answers.targetOrg ? {
840
+ if (answers.auth === "sfCli") return answers.targetOrg ? {
654
841
  type: "sfCli",
655
842
  targetOrg: answers.targetOrg
656
- } : { type: answers.auth };
843
+ } : { type: "sfCli" };
844
+ const fields = AUTH_FIELDS[answers.auth].map(([key, envName]) => [key, `\${${envName}}`]);
845
+ return {
846
+ type: answers.auth,
847
+ ...Object.fromEntries(fields)
848
+ };
657
849
  }
850
+ /** The TypeScript auth setting: credentials are read with `process.env`. */
658
851
  function renderAuth(answers) {
659
- const setting = authSetting(answers);
660
- const targetOrg = setting.targetOrg ? `, targetOrg: ${JSON.stringify(setting.targetOrg)}` : "";
661
- return `{ type: ${JSON.stringify(setting.type)}${targetOrg} }`;
852
+ if (answers.auth === "sfCli") return `{ type: "sfCli"${answers.targetOrg ? `, targetOrg: ${JSON.stringify(answers.targetOrg)}` : ""} }`;
853
+ const fields = AUTH_FIELDS[answers.auth].map(([key, envName]) => `\t\t${key}: process.env.${envName},`);
854
+ return [
855
+ "{",
856
+ `\t\ttype: ${JSON.stringify(answers.auth)},`,
857
+ ...fields,
858
+ " }"
859
+ ].join("\n");
662
860
  }
663
- function authComment(auth, envVars) {
664
- return auth === "sfCli" ? "Uses an org you are logged into with the Salesforce CLI (sf org login web)." : `Reads ${formatList(envVars)} from the environment.`;
861
+ function authComment(auth) {
862
+ return auth === "sfCli" ? "Uses an org you are logged into with the Salesforce CLI (sf org login web)." : "Fill these in from process.env (any variable names), a secret store or elsewhere. Keep secrets out of this file.";
665
863
  }
666
864
  async function maybeUpdateEnvExample(options, auth) {
667
865
  const path = join(options.cwd, ".env.example");
@@ -711,36 +909,43 @@ function printNextSteps(log, answers, scriptAdded, cwd) {
711
909
  "Next steps:",
712
910
  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)"}.`,
713
911
  ` 2. Generate the types: ${command}`,
714
- " 3. Use them:",
912
+ " 3. Use them, logging in with the config's auth:",
913
+ " import { loadAuth } from \"@cerios/salesforce-sobjectly/codegen\";",
715
914
  ` import { API_VERSION, type SObjectRegistry } from "./${answers.output.replace(/\.(ts|mts|cts)$/, "")}";`,
716
- " const sf = new SalesforceClient<SObjectRegistry>({ apiVersion: API_VERSION, auth });"
915
+ " const sf = new SalesforceClient<SObjectRegistry>({ apiVersion: API_VERSION, auth: await loadAuth() });"
717
916
  ].join("\n"));
718
917
  }
719
918
  function formatList(items) {
720
919
  if (items.length <= 1) return items.join("");
721
920
  return `${items.slice(0, -1).join(", ")} and ${items.at(-1)}`;
722
921
  }
723
- var SOBJECT_NAME, NPM_SCRIPT, AUTH_ENV_VARS, AUTH_CHOICES, PICKLIST_CHOICES, FORMAT_CHOICES, JSON_SCHEMA_REFERENCE;
922
+ var SOBJECT_NAME, NPM_SCRIPT, AUTH_FIELDS, AUTH_ENV_VARS, AUTH_CHOICES, PICKLIST_CHOICES, FORMAT_CHOICES, JSON_SCHEMA_REFERENCE;
724
923
  var init_init = __esmMin((() => {
725
924
  init_api_version();
726
925
  init_load_config();
727
926
  SOBJECT_NAME = /^[A-Za-z][A-Za-z0-9_]*$/;
728
927
  NPM_SCRIPT = "sobjects:generate";
729
- AUTH_ENV_VARS = {
928
+ AUTH_FIELDS = {
730
929
  clientCredentials: [
731
- "SF_LOGIN_URL",
732
- "SF_CLIENT_ID",
733
- "SF_CLIENT_SECRET"
930
+ ["loginUrl", "SF_LOGIN_URL"],
931
+ ["clientId", "SF_CLIENT_ID"],
932
+ ["clientSecret", "SF_CLIENT_SECRET"]
734
933
  ],
735
- accessToken: ["SF_ACCESS_TOKEN", "SF_INSTANCE_URL"],
934
+ accessToken: [["accessToken", "SF_ACCESS_TOKEN"], ["instanceUrl", "SF_INSTANCE_URL"]],
736
935
  jwtBearer: [
737
- "SF_LOGIN_URL",
738
- "SF_CLIENT_ID",
739
- "SF_USERNAME",
740
- "SF_PRIVATE_KEY_PATH"
936
+ ["loginUrl", "SF_LOGIN_URL"],
937
+ ["clientId", "SF_CLIENT_ID"],
938
+ ["username", "SF_USERNAME"],
939
+ ["privateKeyPath", "SF_PRIVATE_KEY_PATH"]
741
940
  ],
742
941
  sfCli: []
743
942
  };
943
+ AUTH_ENV_VARS = {
944
+ clientCredentials: AUTH_FIELDS.clientCredentials.map(([, envName]) => envName),
945
+ accessToken: AUTH_FIELDS.accessToken.map(([, envName]) => envName),
946
+ jwtBearer: AUTH_FIELDS.jwtBearer.map(([, envName]) => envName),
947
+ sfCli: []
948
+ };
744
949
  AUTH_CHOICES = [
745
950
  {
746
951
  value: "clientCredentials",
@@ -759,13 +964,24 @@ var init_init = __esmMin((() => {
759
964
  label: "JWT bearer (certificate-based, runs as a user)"
760
965
  }
761
966
  ];
762
- PICKLIST_CHOICES = [{
763
- value: "union",
764
- label: "Union of values, e.g. \"New\" | \"Closed\" (type-safe)"
765
- }, {
766
- value: "string",
767
- label: "Plain string"
768
- }];
967
+ PICKLIST_CHOICES = [
968
+ {
969
+ value: "union",
970
+ label: "Union of values, e.g. \"New\" | \"Closed\" (type-safe)"
971
+ },
972
+ {
973
+ value: "string",
974
+ label: "Plain string"
975
+ },
976
+ {
977
+ value: "const",
978
+ label: "Union plus a named constant per picklist, e.g. CaseStatus.Closed"
979
+ },
980
+ {
981
+ value: "enum",
982
+ label: "TypeScript enum per picklist (restricted picklists only accept enum members)"
983
+ }
984
+ ];
769
985
  FORMAT_CHOICES = [{
770
986
  value: "ts",
771
987
  label: "TypeScript (sobjectly.config.ts, type-checked with defineConfig)"
@@ -932,7 +1148,7 @@ async function buildSource(input, options) {
932
1148
  const env = options.env ?? process.env;
933
1149
  const outputPath = isAbsolute(config.output) ? config.output : resolve(options.cwd ?? process.cwd(), config.output);
934
1150
  const client = new SalesforceClient({
935
- auth: resolveAuth(config.auth, env),
1151
+ auth: resolveAuth(config.auth, env, { warn: (message) => logger.warn(message) }),
936
1152
  apiVersion,
937
1153
  transport: config.transport,
938
1154
  retry: true
@@ -985,39 +1201,114 @@ function normalizeSource(source) {
985
1201
  }
986
1202
  return result;
987
1203
  }
988
- /** Turns the config's auth setting into an auth provider, reading env-based credentials. */
989
- function resolveAuth(auth, env) {
1204
+ /**
1205
+ * Turns the config's auth setting into an auth provider. Each credential comes from its key in
1206
+ * the setting (a `"${NAME}"` placeholder is read from `env`), else from the variable a deprecated
1207
+ * `*Env` key names, else from its default `SF_*` variable. Throws one error listing every
1208
+ * missing credential.
1209
+ */
1210
+ function resolveAuth(auth, env, options = {}) {
990
1211
  const setting = auth ?? { type: "clientCredentials" };
991
1212
  if ("getToken" in setting) return setting;
992
- const read = (name) => {
993
- const value = env[name];
994
- if (!value) throw new Error(`Environment variable ${name} is not set.`);
1213
+ const problems = [];
1214
+ const lookup = (key, defaultEnv) => readCredential(setting, key, defaultEnv, env, options.warn);
1215
+ const read = (key, defaultEnv) => {
1216
+ const { value, problem } = lookup(key, defaultEnv);
1217
+ if (problem) problems.push(problem);
995
1218
  return value;
996
1219
  };
997
1220
  switch (setting.type) {
998
- case "accessToken": return accessToken({
999
- accessToken: read(setting.accessTokenEnv ?? "SF_ACCESS_TOKEN"),
1000
- instanceUrl: read(setting.instanceUrlEnv ?? "SF_INSTANCE_URL")
1001
- });
1002
- case "jwtBearer": return jwtBearerFromEnv(setting, env, read);
1003
- case "sfCli": return sfCli({ targetOrg: setting.targetOrg });
1004
- case "clientCredentials": return clientCredentials({
1005
- loginUrl: read(setting.loginUrlEnv ?? "SF_LOGIN_URL"),
1006
- clientId: read(setting.clientIdEnv ?? "SF_CLIENT_ID"),
1007
- clientSecret: read(setting.clientSecretEnv ?? "SF_CLIENT_SECRET")
1008
- });
1221
+ case "accessToken": {
1222
+ const token = {
1223
+ accessToken: read("accessToken", "SF_ACCESS_TOKEN"),
1224
+ instanceUrl: read("instanceUrl", "SF_INSTANCE_URL")
1225
+ };
1226
+ throwIfMissing(setting.type, problems);
1227
+ return accessToken(token);
1228
+ }
1229
+ case "jwtBearer": {
1230
+ const loginUrl = read("loginUrl", "SF_LOGIN_URL");
1231
+ const clientId = read("clientId", "SF_CLIENT_ID");
1232
+ const username = read("username", "SF_USERNAME");
1233
+ const configured = PRIVATE_KEY_SOURCES.filter(([key]) => key in setting);
1234
+ const found = (configured.length > 0 ? configured : PRIVATE_KEY_SOURCES).map(([key, defaultEnv]) => ({
1235
+ key,
1236
+ ...lookup(key, defaultEnv)
1237
+ }));
1238
+ const source = found.find((item) => item.value);
1239
+ if (source === void 0) problems.push(`no private key: ${found.map((item) => item.problem).join(", and ")}`);
1240
+ throwIfMissing(setting.type, problems);
1241
+ const privateKey = source?.key === "privateKeyPath" ? readFileSync(source.value, "utf8") : source?.value ?? "";
1242
+ return jwtBearer({
1243
+ loginUrl,
1244
+ clientId,
1245
+ username,
1246
+ privateKey
1247
+ });
1248
+ }
1249
+ case "sfCli": {
1250
+ const targetOrg = "targetOrg" in setting ? read("targetOrg", "") : void 0;
1251
+ throwIfMissing(setting.type, problems);
1252
+ return sfCli({ targetOrg });
1253
+ }
1254
+ case "clientCredentials": {
1255
+ const credentials = {
1256
+ loginUrl: read("loginUrl", "SF_LOGIN_URL"),
1257
+ clientId: read("clientId", "SF_CLIENT_ID"),
1258
+ clientSecret: read("clientSecret", "SF_CLIENT_SECRET")
1259
+ };
1260
+ throwIfMissing(setting.type, problems);
1261
+ return clientCredentials(credentials);
1262
+ }
1009
1263
  default: throw new Error(`Unknown codegen auth type "${setting.type}".`);
1010
1264
  }
1011
1265
  }
1012
- function jwtBearerFromEnv(setting, env, read) {
1013
- const keyEnv = setting.privateKeyEnv ?? "SF_PRIVATE_KEY";
1014
- const privateKey = env[keyEnv] ? read(keyEnv) : readFileSync(read(setting.privateKeyPathEnv ?? "SF_PRIVATE_KEY_PATH"), "utf8");
1015
- return jwtBearer({
1016
- loginUrl: read(setting.loginUrlEnv ?? "SF_LOGIN_URL"),
1017
- clientId: read(setting.clientIdEnv ?? "SF_CLIENT_ID"),
1018
- username: read(setting.usernameEnv ?? "SF_USERNAME"),
1019
- privateKey
1020
- });
1266
+ /**
1267
+ * Loads the sobjectly config and returns an auth provider for its `auth` setting, so the client
1268
+ * logs in the same way as `sobjectly generate`:
1269
+ * `new SalesforceClient({ apiVersion: API_VERSION, auth: await loadAuth() })`.
1270
+ * Throws when no config file is found, or one error listing every missing credential.
1271
+ */
1272
+ async function loadAuth(options = {}) {
1273
+ const cwd = options.cwd ?? process.cwd();
1274
+ let configPath = options.configPath === void 0 ? void 0 : resolve(cwd, options.configPath);
1275
+ if (configPath === void 0) {
1276
+ const found = findConfigFiles(cwd);
1277
+ if (found.length > 1) options.warn?.(`found ${found.join(" and ")}; using ${found[0]}. Remove the other one.`);
1278
+ configPath = found[0];
1279
+ }
1280
+ if (configPath === void 0) throw new Error(`No sobjectly config file found in ${cwd} (looked for ${CONFIG_FILE_NAMES.join(", ")}).\nPass { configPath } to loadAuth(), run "npx sobjectly init" to create a config, or pass an auth provider such as clientCredentials() to SalesforceClient directly.`);
1281
+ const { config } = await loadConfig(configPath);
1282
+ return resolveAuth(config.auth, options.env ?? process.env, { warn: options.warn });
1283
+ }
1284
+ function readCredential(setting, key, defaultEnv, env, warn) {
1285
+ const values = setting;
1286
+ if (key in values) {
1287
+ const configured = values[key];
1288
+ const name = typeof configured === "string" ? ENV_PLACEHOLDER.exec(configured)?.[1] : void 0;
1289
+ if (name !== void 0) return fromEnv(env, name, ` (from auth.${key} "${String(configured)}")`);
1290
+ return typeof configured === "string" && configured.length > 0 ? { value: configured } : {
1291
+ value: "",
1292
+ problem: `auth.${key} is empty or undefined (if it reads process.env, that variable is not set)`
1293
+ };
1294
+ }
1295
+ const envKey = `${key}Env`;
1296
+ const legacy = values[envKey];
1297
+ if (typeof legacy === "string") {
1298
+ warn?.(`auth.${envKey} is deprecated and will be removed in 2.0. Use ${key}: process.env.${legacy} in a TypeScript config, or "${key}": "\${${legacy}}" in JSON.`);
1299
+ return fromEnv(env, legacy, "");
1300
+ }
1301
+ return fromEnv(env, defaultEnv, "");
1302
+ }
1303
+ function fromEnv(env, name, origin) {
1304
+ const value = env[name];
1305
+ return value ? { value } : {
1306
+ value: "",
1307
+ problem: `environment variable ${name}${origin} is not set`
1308
+ };
1309
+ }
1310
+ function throwIfMissing(type, problems) {
1311
+ if (problems.length > 0) throw new Error(`Missing Salesforce credentials for auth "${type}":\n${problems.map((problem) => ` - ${problem}`).join("\n")}\nFill them in under auth in the config, or set the environment variables (e.g. with --env-file .env).`);
1021
1312
  }
1022
1313
  async function resolveSObjectNames(client, config, logger) {
1023
1314
  const exclude = new Set((config.exclude ?? []).map((name) => name.toLowerCase()));
@@ -1087,24 +1378,26 @@ async function renameWithRetry(from, to) {
1087
1378
  await new Promise((resolve) => setTimeout(resolve, 50 * attempt));
1088
1379
  }
1089
1380
  }
1090
- var silentLogger;
1381
+ var silentLogger, PRIVATE_KEY_SOURCES, ENV_PLACEHOLDER;
1091
1382
  var init_codegen = __esmMin((() => {
1092
1383
  init_providers();
1093
1384
  init_sf_cli();
1094
1385
  init_client();
1095
1386
  init_generator();
1387
+ init_load_config();
1096
1388
  init_validate_config();
1097
1389
  init_config();
1098
1390
  init_type_mapper();
1099
- init_load_config();
1100
1391
  init_init();
1101
1392
  init_prompter();
1102
1393
  silentLogger = {
1103
1394
  info: () => void 0,
1104
1395
  warn: () => void 0
1105
1396
  };
1397
+ PRIVATE_KEY_SOURCES = [["privateKey", "SF_PRIVATE_KEY"], ["privateKeyPath", "SF_PRIVATE_KEY_PATH"]];
1398
+ ENV_PLACEHOLDER = /^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/;
1106
1399
  }));
1107
1400
  //#endregion
1108
- export { validateConfig as C, FIELD_TYPE_MAP as D, readGeneratedHashes as E, init_type_mapper as O, init_validate_config as S, init_generator as T, defineConfig as _, init_prompter as a, CONFIG_OPTIONS as b, init_init as c, runInit as d, CONFIG_FILE_NAMES as f, loadConfig as g, init_load_config as h, resolveAuth as i, mapFieldType as k, parseSObjects as l, findConfigFiles as m, generate as n, readlinePrompter as o, findConfigFile as p, init_codegen as r, AUTH_ENV_VARS as s, checkGenerated as t, renderConfig as u, init_config as v, generateSource as w, CodegenConfigError as x, AUTH_OPTIONS as y };
1401
+ export { FIELD_TYPE_MAP as A, DEPRECATED_AUTH_OPTIONS as C, generateSource as D, validateConfig as E, mapFieldType as M, init_generator as O, CodegenConfigError as S, init_validate_config as T, findConfigFiles as _, resolveAuth as a, AUTH_OPTIONS as b, AUTH_ENV_VARS as c, renderConfig as d, runInit as f, findConfigFile as g, CONFIG_FILE_NAMES as h, loadAuth as i, init_type_mapper as j, readGeneratedHashes as k, init_init as l, init_config as m, generate as n, init_prompter as o, defineConfig as p, init_codegen as r, readlinePrompter as s, checkGenerated as t, parseSObjects as u, init_load_config as v, PICKLIST_MODES as w, CONFIG_OPTIONS as x, loadConfig as y };
1109
1402
 
1110
- //# sourceMappingURL=codegen-CSx0WAtK.mjs.map
1403
+ //# sourceMappingURL=codegen-0L2ebG9j.mjs.map