@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,10 +1,28 @@
1
- const require_sf_cli = require("./sf-cli-DEVZQTR7.cjs");
1
+ const require_sf_cli = require("./sf-cli-S2BrCbik.cjs");
2
2
  let node_crypto = require("node:crypto");
3
3
  let node_fs = require("node:fs");
4
4
  let node_fs_promises = require("node:fs/promises");
5
5
  let node_path = require("node:path");
6
6
  let jiti = require("jiti");
7
7
  let node_readline = require("node:readline");
8
+ //#region src/codegen/emit.ts
9
+ /** Helpers shared by the modules that emit generated source. */
10
+ const IDENTIFIER$1 = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
11
+ /** Fields sorted by name, `Id` first, so the output is deterministic. */
12
+ function sortedFields(describe) {
13
+ return [...describe.fields].sort((a, b) => a.name === "Id" ? -1 : b.name === "Id" ? 1 : compare(a.name, b.name));
14
+ }
15
+ /** An object key: the bare name when it is an identifier, otherwise a quoted string. */
16
+ function propertyKey(name) {
17
+ return IDENTIFIER$1.test(name) ? name : JSON.stringify(name);
18
+ }
19
+ function escapeComment(value) {
20
+ return String(value ?? "").replace(/\*\//g, "*\\/").replace(/\r?\n/g, " ");
21
+ }
22
+ function compare(a, b) {
23
+ return a < b ? -1 : a > b ? 1 : 0;
24
+ }
25
+ //#endregion
8
26
  //#region src/codegen/type-mapper.ts
9
27
  const ADDRESS_TYPE = "SalesforceAddress";
10
28
  const GEOLOCATION_TYPE = "SalesforceGeolocation";
@@ -39,17 +57,143 @@ const FIELD_TYPE_MAP = {
39
57
  address: ADDRESS_TYPE,
40
58
  location: GEOLOCATION_TYPE
41
59
  };
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";
60
+ /**
61
+ * Returns the TypeScript type for a field, including `| null` for nillable fields. With picklist
62
+ * mode `"const"` or `"enum"`, `typeName` is the field's named picklist type; it replaces the union.
63
+ */
64
+ function mapFieldType(field, picklists = "union", typeName) {
65
+ const base = picklistType(field, picklists, typeName) ?? FIELD_TYPE_MAP[field.type] ?? "unknown";
45
66
  return field.nillable && base !== "unknown" ? `${base} | null` : base;
46
67
  }
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))];
68
+ function picklistType(field, picklists, typeName) {
69
+ if (picklists === "string" || field.type !== "picklist" && field.type !== "combobox") return;
70
+ const values = activePicklistValues(field);
50
71
  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 & {})`;
72
+ const members = picklists !== "union" && typeName !== void 0 ? typeName : values.map((value) => JSON.stringify(value)).join(" | ");
73
+ return field.restrictedPicklist && field.type === "picklist" ? members : `${members} | (string & {})`;
74
+ }
75
+ /** The distinct active values of a picklist, multi-select picklist or combobox, in describe order. */
76
+ function activePicklistValues(field) {
77
+ if (field.type !== "picklist" && field.type !== "multipicklist" && field.type !== "combobox") return [];
78
+ return [...new Set((field.picklistValues ?? []).filter((entry) => entry.active).map((entry) => entry.value))];
79
+ }
80
+ //#endregion
81
+ //#region src/codegen/picklist-types.ts
82
+ function isNamedPicklistMode(mode) {
83
+ return mode === "const" || mode === "enum";
84
+ }
85
+ /** Appended to a picklist type name that is already taken, e.g. by the `CaseStatus` sObject. */
86
+ const CLASH_SUFFIX = "Picklist";
87
+ /**
88
+ * Names the type of every picklist with active values: sObject and field in PascalCase, e.g.
89
+ * `Case.Status` -> `CaseStatus` and `Case.Status__c` -> `CaseStatusCustom`. A name that is in
90
+ * `taken` (the sObjects and other names the file defines) or already used by another picklist gets
91
+ * the suffix `Picklist`; when that is taken too, this throws.
92
+ */
93
+ function picklistTypeNames(describes, taken) {
94
+ const used = new Map([...taken].map((name) => [name, void 0]));
95
+ const result = /* @__PURE__ */ new Map();
96
+ for (const describe of [...describes].sort((a, b) => compare(a.name, b.name))) {
97
+ const names = /* @__PURE__ */ new Map();
98
+ for (const field of sortedFields(describe).filter((item) => activePicklistValues(item).length > 0)) {
99
+ const base = `${pascalApiName(describe.name, "sobject")}${pascalApiName(field.name, "field")}`;
100
+ if (IDENTIFIER$1.test(base)) {
101
+ const picklist = `${describe.name}.${field.name}`;
102
+ const name = resolveName(base, picklist, used);
103
+ used.set(name, picklist);
104
+ names.set(field.name, name);
105
+ }
106
+ }
107
+ if (names.size > 0) result.set(describe.name, names);
108
+ }
109
+ return result;
110
+ }
111
+ function resolveName(base, picklist, used) {
112
+ const name = used.has(base) ? `${base}${CLASH_SUFFIX}` : base;
113
+ if (used.has(name)) {
114
+ const owner = used.get(name);
115
+ const other = owner === void 0 ? "a type the generated file defines" : `the picklist type for ${owner}`;
116
+ throw new Error(`The picklist type "${name}" for ${picklist} clashes with ${other}; exclude one of the sObjects or use picklists: "union".`);
117
+ }
118
+ return name;
119
+ }
120
+ /**
121
+ * An API name in PascalCase without underscores. A field's `__c` becomes `Custom`
122
+ * (`Lead_Source__c` -> `LeadSourceCustom`) and an sObject's is dropped (`My_Object__c` ->
123
+ * `MyObject`). Other suffixes are kept: `Foo__pc` -> `FooPc`, `Config__mdt` -> `ConfigMdt`.
124
+ */
125
+ function pascalApiName(name, kind) {
126
+ const match = /^(.+?)__([a-z]+)$/.exec(name);
127
+ const body = match ? match[1] : name;
128
+ const suffix = match ? match[2] : "";
129
+ const suffixPart = suffix === "c" ? kind === "field" ? "Custom" : "" : capitalize(suffix);
130
+ return `${body.split("_").filter((part) => part.length > 0).map(capitalize).join("")}${suffixPart}`;
131
+ }
132
+ function capitalize(value) {
133
+ return value.charAt(0).toUpperCase() + value.slice(1);
134
+ }
135
+ /**
136
+ * The member names for a picklist's values, in the same order. A value that is an identifier is
137
+ * its own name; other values are sanitized (`"Closed Won"` -> `Closed_Won`, `"3rd Party"` ->
138
+ * `_3rd_Party`). When the sanitized name is empty or taken, the value itself is the (quoted) name.
139
+ */
140
+ function picklistMemberKeys(values, mode) {
141
+ const used = /* @__PURE__ */ new Set();
142
+ const claim = (key) => {
143
+ used.add(key);
144
+ return key;
145
+ };
146
+ const usable = (key) => key.length > 0 && key !== "__proto__" && !used.has(key) && (mode === "const" || !isNumericName(key));
147
+ const own = values.map((value) => IDENTIFIER$1.test(value) && usable(value) ? claim(value) : void 0);
148
+ return values.map((value, index) => {
149
+ const ownKey = own[index];
150
+ if (ownKey !== void 0) return ownKey;
151
+ const sanitized = sanitizeKey(value);
152
+ if (usable(sanitized)) return claim(sanitized);
153
+ if (usable(value)) return claim(value);
154
+ let counter = 2;
155
+ while (!usable(`${sanitized || "_"}_${counter}`)) counter++;
156
+ return claim(`${sanitized || "_"}_${counter}`);
157
+ });
158
+ }
159
+ function sanitizeKey(value) {
160
+ const key = value.normalize("NFKD").replace(/\p{M}/gu, "").replace(/[^A-Za-z0-9_]+/g, "_").replace(/^_+|_+$/g, "");
161
+ return /^[0-9]/.test(key) ? `_${key}` : key;
162
+ }
163
+ /** Names TypeScript rejects as enum members because they are numeric, e.g. `"1"` or `"-1"`. */
164
+ function isNumericName(key) {
165
+ const number = Number(key);
166
+ return Number.isFinite(number) && String(number) === key;
167
+ }
168
+ /** The named type declarations for one sObject's picklists, in field order; `""` when there are none. */
169
+ function picklistDeclarations(describe, names, mode) {
170
+ if (!names) return "";
171
+ return sortedFields(describe).flatMap((field) => {
172
+ const name = names.get(field.name);
173
+ return name === void 0 ? [] : [declaration(describe.name, field, name, mode)];
174
+ }).join("\n\n");
175
+ }
176
+ function declaration(sobject, field, name, mode) {
177
+ const values = activePicklistValues(field);
178
+ const keys = picklistMemberKeys(values, mode);
179
+ const doc = `/** Active values of ${escapeComment(`${sobject}.${field.name}`)} (${escapeComment(field.type)}). */`;
180
+ if (mode === "enum") {
181
+ const members = values.map((value, index) => `\t${propertyKey(keys[index])} = ${JSON.stringify(value)},`);
182
+ return [
183
+ doc,
184
+ `export enum ${name} {`,
185
+ ...members,
186
+ "}"
187
+ ].join("\n");
188
+ }
189
+ const members = values.map((value, index) => `\t${propertyKey(keys[index])}: ${JSON.stringify(value)},`);
190
+ return [
191
+ doc,
192
+ `export const ${name} = {`,
193
+ ...members,
194
+ "} as const;",
195
+ `export type ${name} = (typeof ${name})[keyof typeof ${name}];`
196
+ ].join("\n");
53
197
  }
54
198
  //#endregion
55
199
  //#region src/codegen/generator.ts
@@ -60,24 +204,28 @@ const FIELD_KINDS = /* @__PURE__ */ new Set([
60
204
  "time",
61
205
  "multipicklist"
62
206
  ]);
63
- const IDENTIFIER$1 = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
64
207
  /**
65
208
  * 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.
209
+ * and update input types per sObject, and the `SObjectRegistry` that ties them together. With
210
+ * picklist mode `"const"` or `"enum"`, each picklist also gets a named type before its sObject.
67
211
  * The output is deterministic: sObjects are sorted by name and fields by name (`Id` first).
68
212
  */
69
213
  function generateSource(describes, options = {}) {
70
214
  const sorted = [...describes].sort((a, b) => compare(a.name, b.name));
71
- assertGeneratableNames(sorted);
215
+ const definedNames = assertGeneratableNames(sorted);
72
216
  const names = new Set(sorted.map((describe) => describe.name));
217
+ const picklists = options.picklists ?? "union";
218
+ const picklistTypes = isNamedPicklistMode(picklists) ? picklistTypeNames(sorted, definedNames) : void 0;
73
219
  const sections = [imports(sorted, options.importSource ?? "@cerios/salesforce-sobjectly")];
74
220
  const objectHashes = [];
75
221
  for (const describe of sorted) {
222
+ const typeNames = picklistTypes?.get(describe.name);
76
223
  const objectSections = [
77
- readInterface(describe, options.picklists ?? "union"),
224
+ isNamedPicklistMode(picklists) ? picklistDeclarations(describe, typeNames, picklists) : "",
225
+ readInterface(describe, picklists, typeNames),
78
226
  inputType(describe, "CreateInput", "createable", lookup(options.excludeCreateFields, describe.name)),
79
227
  inputType(describe, "UpdateInput", "updateable", lookup(options.excludeUpdateFields, describe.name))
80
- ];
228
+ ].filter((section) => section.length > 0);
81
229
  sections.push(...objectSections);
82
230
  const fingerprint = [
83
231
  ...objectSections,
@@ -104,7 +252,10 @@ const RESERVED_TYPE_NAMES = /* @__PURE__ */ new Set([
104
252
  ADDRESS_TYPE,
105
253
  GEOLOCATION_TYPE
106
254
  ]);
107
- /** Fails with a clear message for sObject names the generated TypeScript can't represent. */
255
+ /**
256
+ * Fails with a clear message for sObject names the generated TypeScript can't represent. Returns
257
+ * every name the file defines apart from picklist types, so those can avoid them.
258
+ */
108
259
  function assertGeneratableNames(describes) {
109
260
  const seen = /* @__PURE__ */ new Map();
110
261
  const generated = new Set(describes.flatMap((describe) => [`${describe.name}CreateInput`, `${describe.name}UpdateInput`]));
@@ -115,6 +266,11 @@ function assertGeneratableNames(describes) {
115
266
  if (previous !== void 0) throw new Error(`sObjects "${previous}" and "${name}" differ only in case; list each sObject once.`);
116
267
  seen.set(name.toLowerCase(), name);
117
268
  }
269
+ return /* @__PURE__ */ new Set([
270
+ ...RESERVED_TYPE_NAMES,
271
+ ...generated,
272
+ ...describes.map((describe) => describe.name)
273
+ ]);
118
274
  }
119
275
  /** Looks up a per-sObject option case-insensitively (sObject names are case-insensitive in Salesforce). */
120
276
  function lookup(map, name) {
@@ -162,12 +318,11 @@ function imports(describes, importSource) {
162
318
  if (fields.some((field) => field.type === "location")) used.push(GEOLOCATION_TYPE);
163
319
  return used.length > 0 ? `import type { ${used.join(", ")} } from ${JSON.stringify(importSource)};` : "";
164
320
  }
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) {
321
+ function readInterface(describe, picklists, typeNames) {
169
322
  const lines = sortedFields(describe).map((field) => {
170
- return `${`\t/** ${escapeComment(field.label)} (${escapeComment(field.type)}) */`}\n\t${propertyKey(field.name)}: ${mapFieldType(field, picklists)};`;
323
+ const comment = `\t/** ${escapeComment(field.label)} (${escapeComment(field.type)}) */`;
324
+ const type = mapFieldType(field, picklists, typeNames?.get(field.name));
325
+ return `${comment}\n\t${propertyKey(field.name)}: ${type};`;
171
326
  });
172
327
  return `${describe.label ? `/** ${escapeComment(describe.label)} */\n` : ""}export interface ${describe.name} {\n${lines.join("\n")}\n}`;
173
328
  }
@@ -254,10 +409,6 @@ function recordTypeConstants(describes) {
254
409
  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
410
  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
411
  }
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
412
  function unionOrNever(values) {
262
413
  return values.length > 0 ? values.map((value) => JSON.stringify(value)).join(" | ") : "never";
263
414
  }
@@ -280,15 +431,6 @@ function relationshipMap(entries) {
280
431
  function keyUnion(names) {
281
432
  return names.map((name) => JSON.stringify(name)).join(" | ");
282
433
  }
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
434
  //#endregion
293
435
  //#region src/codegen/validate-config.ts
294
436
  /** Thrown when a codegen config is invalid. Lists every problem found. */
@@ -302,15 +444,28 @@ var CodegenConfigError = class extends Error {
302
444
  this.problems = problems;
303
445
  }
304
446
  };
305
- /** The env-variable options each env-based auth type accepts. */
447
+ /** The options each auth type accepts: its credential keys, then the deprecated `*Env` keys. */
306
448
  const AUTH_OPTIONS = {
307
449
  clientCredentials: [
450
+ "loginUrl",
451
+ "clientId",
452
+ "clientSecret",
308
453
  "loginUrlEnv",
309
454
  "clientIdEnv",
310
455
  "clientSecretEnv"
311
456
  ],
312
- accessToken: ["accessTokenEnv", "instanceUrlEnv"],
457
+ accessToken: [
458
+ "accessToken",
459
+ "instanceUrl",
460
+ "accessTokenEnv",
461
+ "instanceUrlEnv"
462
+ ],
313
463
  jwtBearer: [
464
+ "loginUrl",
465
+ "clientId",
466
+ "username",
467
+ "privateKey",
468
+ "privateKeyPath",
314
469
  "loginUrlEnv",
315
470
  "clientIdEnv",
316
471
  "usernameEnv",
@@ -319,7 +474,23 @@ const AUTH_OPTIONS = {
319
474
  ],
320
475
  sfCli: ["targetOrg"]
321
476
  };
322
- const PICKLIST_MODES = ["union", "string"];
477
+ /** The deprecated `*Env` auth options, each mapped to the credential key that replaces it. */
478
+ const DEPRECATED_AUTH_OPTIONS = {
479
+ loginUrlEnv: "loginUrl",
480
+ clientIdEnv: "clientId",
481
+ clientSecretEnv: "clientSecret",
482
+ accessTokenEnv: "accessToken",
483
+ instanceUrlEnv: "instanceUrl",
484
+ usernameEnv: "username",
485
+ privateKeyEnv: "privateKey",
486
+ privateKeyPathEnv: "privateKeyPath"
487
+ };
488
+ const PICKLIST_MODES = [
489
+ "union",
490
+ "string",
491
+ "const",
492
+ "enum"
493
+ ];
323
494
  const IDENTIFIER = /^[A-Za-z][A-Za-z0-9_]*$/;
324
495
  const VALIDATORS = {
325
496
  $schema: (value, { problem }) => {
@@ -339,7 +510,7 @@ const VALIDATORS = {
339
510
  excludeCreateFields: (value, context) => validateFieldMap("excludeCreateFields", value, context),
340
511
  excludeUpdateFields: (value, context) => validateFieldMap("excludeUpdateFields", value, context),
341
512
  picklists: (value, { problem }) => {
342
- if (!PICKLIST_MODES.includes(value)) problem(`picklists: must be "union" or "string", got ${show(value)}.`);
513
+ if (!PICKLIST_MODES.includes(value)) problem(`picklists: must be one of ${PICKLIST_MODES.join(", ")}, got ${show(value)}.`);
343
514
  },
344
515
  constants: (value, { problem }) => {
345
516
  if (typeof value !== "boolean") problem(`constants: must be true or false, got ${show(value)}.`);
@@ -425,8 +596,17 @@ function validateAuth(value, { json, problem }) {
425
596
  problem(`auth.type: must be one of ${Object.keys(AUTH_OPTIONS).join(", ")}, got ${show(type)}.`);
426
597
  return;
427
598
  }
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)}.`);
599
+ for (const [key, option] of Object.entries(rest)) if (!allowed.includes(key)) {
600
+ const valid = allowed.filter((name) => !(name in DEPRECATED_AUTH_OPTIONS));
601
+ problem(`auth.${key}: not an option of "${String(type)}". Valid options: ${valid.join(", ")}.`);
602
+ } else validateAuthOption(key, option, rest, problem);
603
+ }
604
+ function validateAuthOption(key, option, setting, problem) {
605
+ const replacement = DEPRECATED_AUTH_OPTIONS[key];
606
+ if (replacement !== void 0 && replacement in setting) problem(`auth.${key}: use either ${replacement} or ${key}, not both.`);
607
+ else if (replacement !== void 0) {
608
+ if (typeof option !== "string" || option.length === 0) problem(`auth.${key}: must be a non-empty string, got ${show(option)}.`);
609
+ } else if (option !== void 0 && typeof option !== "string") problem(`auth.${key}: must be a string, got ${show(option)}.`);
430
610
  }
431
611
  function show(value) {
432
612
  if (value === void 0) return "undefined";
@@ -438,24 +618,6 @@ function show(value) {
438
618
  }
439
619
  }
440
620
  //#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
621
  //#region src/codegen/load-config.ts
460
622
  /** Config file names, in lookup order. */
461
623
  const CONFIG_FILE_NAMES = [
@@ -507,25 +669,50 @@ function readJson(path) {
507
669
  }
508
670
  }
509
671
  //#endregion
672
+ //#region src/codegen/config.ts
673
+ /**
674
+ * Type-checks a `sobjectly.config.ts`. The listed `sobjects` become a literal union, so a typo in
675
+ * a per-sObject option is a compile error:
676
+ *
677
+ * ```ts
678
+ * export default defineConfig({
679
+ * apiVersion: "v66.0",
680
+ * output: "src/generated/sobjects.ts",
681
+ * sobjects: ["Account", "Contact"],
682
+ * excludeCreateFields: { Acount: ["Name"] }, // ❌ "Acount" is not in sobjects
683
+ * });
684
+ * ```
685
+ */
686
+ function defineConfig(config) {
687
+ return config;
688
+ }
689
+ //#endregion
510
690
  //#region src/codegen/init.ts
511
691
  const SOBJECT_NAME = /^[A-Za-z][A-Za-z0-9_]*$/;
512
692
  const NPM_SCRIPT = "sobjects:generate";
513
- /** The environment variables each auth type reads by default. */
514
- const AUTH_ENV_VARS = {
693
+ /** The credential keys `init` writes per auth type, each with the environment variable it suggests. */
694
+ const AUTH_FIELDS = {
515
695
  clientCredentials: [
516
- "SF_LOGIN_URL",
517
- "SF_CLIENT_ID",
518
- "SF_CLIENT_SECRET"
696
+ ["loginUrl", "SF_LOGIN_URL"],
697
+ ["clientId", "SF_CLIENT_ID"],
698
+ ["clientSecret", "SF_CLIENT_SECRET"]
519
699
  ],
520
- accessToken: ["SF_ACCESS_TOKEN", "SF_INSTANCE_URL"],
700
+ accessToken: [["accessToken", "SF_ACCESS_TOKEN"], ["instanceUrl", "SF_INSTANCE_URL"]],
521
701
  jwtBearer: [
522
- "SF_LOGIN_URL",
523
- "SF_CLIENT_ID",
524
- "SF_USERNAME",
525
- "SF_PRIVATE_KEY_PATH"
702
+ ["loginUrl", "SF_LOGIN_URL"],
703
+ ["clientId", "SF_CLIENT_ID"],
704
+ ["username", "SF_USERNAME"],
705
+ ["privateKeyPath", "SF_PRIVATE_KEY_PATH"]
526
706
  ],
527
707
  sfCli: []
528
708
  };
709
+ /** The environment variables the config written by `init` reads, per auth type. */
710
+ const AUTH_ENV_VARS = {
711
+ clientCredentials: AUTH_FIELDS.clientCredentials.map(([, envName]) => envName),
712
+ accessToken: AUTH_FIELDS.accessToken.map(([, envName]) => envName),
713
+ jwtBearer: AUTH_FIELDS.jwtBearer.map(([, envName]) => envName),
714
+ sfCli: []
715
+ };
529
716
  const AUTH_CHOICES = [
530
717
  {
531
718
  value: "clientCredentials",
@@ -544,13 +731,24 @@ const AUTH_CHOICES = [
544
731
  label: "JWT bearer (certificate-based, runs as a user)"
545
732
  }
546
733
  ];
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
- }];
734
+ const PICKLIST_CHOICES = [
735
+ {
736
+ value: "union",
737
+ label: "Union of values, e.g. \"New\" | \"Closed\" (type-safe)"
738
+ },
739
+ {
740
+ value: "string",
741
+ label: "Plain string"
742
+ },
743
+ {
744
+ value: "const",
745
+ label: "Union plus a named constant per picklist, e.g. CaseStatus.Closed"
746
+ },
747
+ {
748
+ value: "enum",
749
+ label: "TypeScript enum per picklist (restricted picklists only accept enum members)"
750
+ }
751
+ ];
554
752
  const FORMAT_CHOICES = [{
555
753
  value: "ts",
556
754
  label: "TypeScript (sobjectly.config.ts, type-checked with defineConfig)"
@@ -673,7 +871,6 @@ function renderJsonConfig(answers) {
673
871
  return `${JSON.stringify(config, null, " ")}\n`;
674
872
  }
675
873
  function renderTypeScriptConfig(answers) {
676
- const envVars = AUTH_ENV_VARS[answers.auth];
677
874
  const sobjects = answers.sobjects.map((name) => JSON.stringify(name)).join(", ");
678
875
  return [
679
876
  "import { defineConfig } from \"@cerios/salesforce-sobjectly/codegen\";",
@@ -685,26 +882,38 @@ function renderTypeScriptConfig(answers) {
685
882
  `\toutput: ${JSON.stringify(answers.output)},`,
686
883
  " // The sObjects to generate types for.",
687
884
  `\tsobjects: [${sobjects}],`,
688
- `\t// ${authComment(answers.auth, envVars)}`,
885
+ `\t// ${authComment(answers.auth)}`,
689
886
  `\tauth: ${renderAuth(answers)},`,
690
887
  `\tpicklists: ${JSON.stringify(answers.picklists)},`,
691
888
  "});",
692
889
  ""
693
890
  ].join("\n");
694
891
  }
892
+ /** The JSON auth setting: credentials are `"${NAME}"` placeholders, read from the environment. */
695
893
  function authSetting(answers) {
696
- return answers.auth === "sfCli" && answers.targetOrg ? {
894
+ if (answers.auth === "sfCli") return answers.targetOrg ? {
697
895
  type: "sfCli",
698
896
  targetOrg: answers.targetOrg
699
- } : { type: answers.auth };
897
+ } : { type: "sfCli" };
898
+ const fields = AUTH_FIELDS[answers.auth].map(([key, envName]) => [key, `\${${envName}}`]);
899
+ return {
900
+ type: answers.auth,
901
+ ...Object.fromEntries(fields)
902
+ };
700
903
  }
904
+ /** The TypeScript auth setting: credentials are read with `process.env`. */
701
905
  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} }`;
906
+ if (answers.auth === "sfCli") return `{ type: "sfCli"${answers.targetOrg ? `, targetOrg: ${JSON.stringify(answers.targetOrg)}` : ""} }`;
907
+ const fields = AUTH_FIELDS[answers.auth].map(([key, envName]) => `\t\t${key}: process.env.${envName},`);
908
+ return [
909
+ "{",
910
+ `\t\ttype: ${JSON.stringify(answers.auth)},`,
911
+ ...fields,
912
+ " }"
913
+ ].join("\n");
705
914
  }
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.`;
915
+ function authComment(auth) {
916
+ 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.";
708
917
  }
709
918
  async function maybeUpdateEnvExample(options, auth) {
710
919
  const path = (0, node_path.join)(options.cwd, ".env.example");
@@ -754,9 +963,10 @@ function printNextSteps(log, answers, scriptAdded, cwd) {
754
963
  "Next steps:",
755
964
  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
965
  ` 2. Generate the types: ${command}`,
757
- " 3. Use them:",
966
+ " 3. Use them, logging in with the config's auth:",
967
+ " import { loadAuth } from \"@cerios/salesforce-sobjectly/codegen\";",
758
968
  ` import { API_VERSION, type SObjectRegistry } from "./${answers.output.replace(/\.(ts|mts|cts)$/, "")}";`,
759
- " const sf = new SalesforceClient<SObjectRegistry>({ apiVersion: API_VERSION, auth });"
969
+ " const sf = new SalesforceClient<SObjectRegistry>({ apiVersion: API_VERSION, auth: await loadAuth() });"
760
970
  ].join("\n"));
761
971
  }
762
972
  function formatList(items) {
@@ -923,7 +1133,7 @@ async function buildSource(input, options) {
923
1133
  const env = options.env ?? process.env;
924
1134
  const outputPath = (0, node_path.isAbsolute)(config.output) ? config.output : (0, node_path.resolve)(options.cwd ?? process.cwd(), config.output);
925
1135
  const client = new require_sf_cli.SalesforceClient({
926
- auth: resolveAuth(config.auth, env),
1136
+ auth: resolveAuth(config.auth, env, { warn: (message) => logger.warn(message) }),
927
1137
  apiVersion,
928
1138
  transport: config.transport,
929
1139
  retry: true
@@ -976,39 +1186,118 @@ function normalizeSource(source) {
976
1186
  }
977
1187
  return result;
978
1188
  }
979
- /** Turns the config's auth setting into an auth provider, reading env-based credentials. */
980
- function resolveAuth(auth, env) {
1189
+ /** Where the JWT private key comes from, in order of preference, with each default variable. */
1190
+ const PRIVATE_KEY_SOURCES = [["privateKey", "SF_PRIVATE_KEY"], ["privateKeyPath", "SF_PRIVATE_KEY_PATH"]];
1191
+ /** A `"${NAME}"` placeholder. Only a whole value counts, so a secret that contains `${` is kept as-is. */
1192
+ const ENV_PLACEHOLDER = /^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/;
1193
+ /**
1194
+ * Turns the config's auth setting into an auth provider. Each credential comes from its key in
1195
+ * the setting (a `"${NAME}"` placeholder is read from `env`), else from the variable a deprecated
1196
+ * `*Env` key names, else from its default `SF_*` variable. Throws one error listing every
1197
+ * missing credential.
1198
+ */
1199
+ function resolveAuth(auth, env, options = {}) {
981
1200
  const setting = auth ?? { type: "clientCredentials" };
982
1201
  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.`);
1202
+ const problems = [];
1203
+ const lookup = (key, defaultEnv) => readCredential(setting, key, defaultEnv, env, options.warn);
1204
+ const read = (key, defaultEnv) => {
1205
+ const { value, problem } = lookup(key, defaultEnv);
1206
+ if (problem) problems.push(problem);
986
1207
  return value;
987
1208
  };
988
1209
  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
- });
1210
+ case "accessToken": {
1211
+ const token = {
1212
+ accessToken: read("accessToken", "SF_ACCESS_TOKEN"),
1213
+ instanceUrl: read("instanceUrl", "SF_INSTANCE_URL")
1214
+ };
1215
+ throwIfMissing(setting.type, problems);
1216
+ return require_sf_cli.accessToken(token);
1217
+ }
1218
+ case "jwtBearer": {
1219
+ const loginUrl = read("loginUrl", "SF_LOGIN_URL");
1220
+ const clientId = read("clientId", "SF_CLIENT_ID");
1221
+ const username = read("username", "SF_USERNAME");
1222
+ const configured = PRIVATE_KEY_SOURCES.filter(([key]) => key in setting);
1223
+ const found = (configured.length > 0 ? configured : PRIVATE_KEY_SOURCES).map(([key, defaultEnv]) => ({
1224
+ key,
1225
+ ...lookup(key, defaultEnv)
1226
+ }));
1227
+ const source = found.find((item) => item.value);
1228
+ if (source === void 0) problems.push(`no private key: ${found.map((item) => item.problem).join(", and ")}`);
1229
+ throwIfMissing(setting.type, problems);
1230
+ const privateKey = source?.key === "privateKeyPath" ? (0, node_fs.readFileSync)(source.value, "utf8") : source?.value ?? "";
1231
+ return require_sf_cli.jwtBearer({
1232
+ loginUrl,
1233
+ clientId,
1234
+ username,
1235
+ privateKey
1236
+ });
1237
+ }
1238
+ case "sfCli": {
1239
+ const targetOrg = "targetOrg" in setting ? read("targetOrg", "") : void 0;
1240
+ throwIfMissing(setting.type, problems);
1241
+ return require_sf_cli.sfCli({ targetOrg });
1242
+ }
1243
+ case "clientCredentials": {
1244
+ const credentials = {
1245
+ loginUrl: read("loginUrl", "SF_LOGIN_URL"),
1246
+ clientId: read("clientId", "SF_CLIENT_ID"),
1247
+ clientSecret: read("clientSecret", "SF_CLIENT_SECRET")
1248
+ };
1249
+ throwIfMissing(setting.type, problems);
1250
+ return require_sf_cli.clientCredentials(credentials);
1251
+ }
1000
1252
  default: throw new Error(`Unknown codegen auth type "${setting.type}".`);
1001
1253
  }
1002
1254
  }
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
- });
1255
+ /**
1256
+ * Loads the sobjectly config and returns an auth provider for its `auth` setting, so the client
1257
+ * logs in the same way as `sobjectly generate`:
1258
+ * `new SalesforceClient({ apiVersion: API_VERSION, auth: await loadAuth() })`.
1259
+ * Throws when no config file is found, or one error listing every missing credential.
1260
+ */
1261
+ async function loadAuth(options = {}) {
1262
+ const cwd = options.cwd ?? process.cwd();
1263
+ let configPath = options.configPath === void 0 ? void 0 : (0, node_path.resolve)(cwd, options.configPath);
1264
+ if (configPath === void 0) {
1265
+ const found = findConfigFiles(cwd);
1266
+ if (found.length > 1) options.warn?.(`found ${found.join(" and ")}; using ${found[0]}. Remove the other one.`);
1267
+ configPath = found[0];
1268
+ }
1269
+ 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.`);
1270
+ const { config } = await loadConfig(configPath);
1271
+ return resolveAuth(config.auth, options.env ?? process.env, { warn: options.warn });
1272
+ }
1273
+ function readCredential(setting, key, defaultEnv, env, warn) {
1274
+ const values = setting;
1275
+ if (key in values) {
1276
+ const configured = values[key];
1277
+ const name = typeof configured === "string" ? ENV_PLACEHOLDER.exec(configured)?.[1] : void 0;
1278
+ if (name !== void 0) return fromEnv(env, name, ` (from auth.${key} "${String(configured)}")`);
1279
+ return typeof configured === "string" && configured.length > 0 ? { value: configured } : {
1280
+ value: "",
1281
+ problem: `auth.${key} is empty or undefined (if it reads process.env, that variable is not set)`
1282
+ };
1283
+ }
1284
+ const envKey = `${key}Env`;
1285
+ const legacy = values[envKey];
1286
+ if (typeof legacy === "string") {
1287
+ 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.`);
1288
+ return fromEnv(env, legacy, "");
1289
+ }
1290
+ return fromEnv(env, defaultEnv, "");
1291
+ }
1292
+ function fromEnv(env, name, origin) {
1293
+ const value = env[name];
1294
+ return value ? { value } : {
1295
+ value: "",
1296
+ problem: `environment variable ${name}${origin} is not set`
1297
+ };
1298
+ }
1299
+ function throwIfMissing(type, problems) {
1300
+ 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).`);
1012
1301
  }
1013
1302
  async function resolveSObjectNames(client, config, logger) {
1014
1303
  const exclude = new Set((config.exclude ?? []).map((name) => name.toLowerCase()));
@@ -1109,12 +1398,24 @@ Object.defineProperty(exports, "CodegenConfigError", {
1109
1398
  return CodegenConfigError;
1110
1399
  }
1111
1400
  });
1401
+ Object.defineProperty(exports, "DEPRECATED_AUTH_OPTIONS", {
1402
+ enumerable: true,
1403
+ get: function() {
1404
+ return DEPRECATED_AUTH_OPTIONS;
1405
+ }
1406
+ });
1112
1407
  Object.defineProperty(exports, "FIELD_TYPE_MAP", {
1113
1408
  enumerable: true,
1114
1409
  get: function() {
1115
1410
  return FIELD_TYPE_MAP;
1116
1411
  }
1117
1412
  });
1413
+ Object.defineProperty(exports, "PICKLIST_MODES", {
1414
+ enumerable: true,
1415
+ get: function() {
1416
+ return PICKLIST_MODES;
1417
+ }
1418
+ });
1118
1419
  Object.defineProperty(exports, "checkGenerated", {
1119
1420
  enumerable: true,
1120
1421
  get: function() {
@@ -1151,6 +1452,12 @@ Object.defineProperty(exports, "generateSource", {
1151
1452
  return generateSource;
1152
1453
  }
1153
1454
  });
1455
+ Object.defineProperty(exports, "loadAuth", {
1456
+ enumerable: true,
1457
+ get: function() {
1458
+ return loadAuth;
1459
+ }
1460
+ });
1154
1461
  Object.defineProperty(exports, "loadConfig", {
1155
1462
  enumerable: true,
1156
1463
  get: function() {
@@ -1206,4 +1513,4 @@ Object.defineProperty(exports, "validateConfig", {
1206
1513
  }
1207
1514
  });
1208
1515
 
1209
- //# sourceMappingURL=codegen-BgDgU2RR.cjs.map
1516
+ //# sourceMappingURL=codegen-Daud-Grz.cjs.map