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