@cerios/salesforce-sobjectly 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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-_Ua-M_Zi.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",
@@ -397,7 +547,12 @@ var init_validate_config = __esmMin((() => {
397
547
  ],
398
548
  sfCli: ["targetOrg"]
399
549
  };
400
- PICKLIST_MODES = ["union", "string"];
550
+ PICKLIST_MODES = [
551
+ "union",
552
+ "string",
553
+ "const",
554
+ "enum"
555
+ ];
401
556
  IDENTIFIER = /^[A-Za-z][A-Za-z0-9_]*$/;
402
557
  VALIDATORS = {
403
558
  $schema: (value, { problem }) => {
@@ -417,7 +572,7 @@ var init_validate_config = __esmMin((() => {
417
572
  excludeCreateFields: (value, context) => validateFieldMap("excludeCreateFields", value, context),
418
573
  excludeUpdateFields: (value, context) => validateFieldMap("excludeUpdateFields", value, context),
419
574
  picklists: (value, { problem }) => {
420
- if (!PICKLIST_MODES.includes(value)) problem(`picklists: must be "union" or "string", got ${show(value)}.`);
575
+ if (!PICKLIST_MODES.includes(value)) problem(`picklists: must be one of ${PICKLIST_MODES.join(", ")}, got ${show(value)}.`);
421
576
  },
422
577
  constants: (value, { problem }) => {
423
578
  if (typeof value !== "boolean") problem(`constants: must be true or false, got ${show(value)}.`);
@@ -759,13 +914,24 @@ var init_init = __esmMin((() => {
759
914
  label: "JWT bearer (certificate-based, runs as a user)"
760
915
  }
761
916
  ];
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
- }];
917
+ PICKLIST_CHOICES = [
918
+ {
919
+ value: "union",
920
+ label: "Union of values, e.g. \"New\" | \"Closed\" (type-safe)"
921
+ },
922
+ {
923
+ value: "string",
924
+ label: "Plain string"
925
+ },
926
+ {
927
+ value: "const",
928
+ label: "Union plus a named constant per picklist, e.g. CaseStatus.Closed"
929
+ },
930
+ {
931
+ value: "enum",
932
+ label: "TypeScript enum per picklist (restricted picklists only accept enum members)"
933
+ }
934
+ ];
769
935
  FORMAT_CHOICES = [{
770
936
  value: "ts",
771
937
  label: "TypeScript (sobjectly.config.ts, type-checked with defineConfig)"
@@ -1105,6 +1271,6 @@ var init_codegen = __esmMin((() => {
1105
1271
  };
1106
1272
  }));
1107
1273
  //#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 };
1274
+ export { mapFieldType as A, init_validate_config as C, readGeneratedHashes as D, init_generator as E, FIELD_TYPE_MAP as O, PICKLIST_MODES as S, generateSource 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, init_type_mapper 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, validateConfig as w, CodegenConfigError as x, AUTH_OPTIONS as y };
1109
1275
 
1110
- //# sourceMappingURL=codegen-CSx0WAtK.mjs.map
1276
+ //# sourceMappingURL=codegen-C6MEYiWo.mjs.map