frappe-codegen 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/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +158 -0
- package/dist/cli.js +531 -0
- package/dist/index.d.mts +115 -0
- package/dist/index.d.ts +115 -0
- package/dist/index.js +387 -0
- package/dist/index.mjs +373 -0
- package/package.json +88 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { FrappeClient } from 'frappe-js-client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @module config
|
|
5
|
+
* @description Loads `frappe-codegen.config.json` (no secrets). CLI flags and FRAPPE_* env override.
|
|
6
|
+
*/
|
|
7
|
+
declare const DEFAULT_CONFIG_NAME = "frappe-codegen.config.json";
|
|
8
|
+
interface CodegenFileConfig {
|
|
9
|
+
url?: string;
|
|
10
|
+
out?: string;
|
|
11
|
+
includeHidden?: boolean;
|
|
12
|
+
followTables?: boolean;
|
|
13
|
+
doctypes?: string[];
|
|
14
|
+
modules?: string[];
|
|
15
|
+
}
|
|
16
|
+
interface ResolvedCodegenConfig {
|
|
17
|
+
url?: string;
|
|
18
|
+
out: string;
|
|
19
|
+
includeHidden: boolean;
|
|
20
|
+
followTables: boolean;
|
|
21
|
+
doctypes: string[];
|
|
22
|
+
modules: string[];
|
|
23
|
+
apiKey?: string;
|
|
24
|
+
apiSecret?: string;
|
|
25
|
+
dryRun: boolean;
|
|
26
|
+
emitDocTypeMap: boolean;
|
|
27
|
+
includeLabels: boolean;
|
|
28
|
+
}
|
|
29
|
+
declare function loadConfigFile(path: string): CodegenFileConfig;
|
|
30
|
+
declare function findDefaultConfigPath(cwd?: string): string | undefined;
|
|
31
|
+
interface CliOverlay {
|
|
32
|
+
url?: string;
|
|
33
|
+
out?: string;
|
|
34
|
+
includeHidden?: boolean;
|
|
35
|
+
followTables?: boolean;
|
|
36
|
+
doctypes?: string[];
|
|
37
|
+
modules?: string[];
|
|
38
|
+
apiKey?: string;
|
|
39
|
+
apiSecret?: string;
|
|
40
|
+
dryRun?: boolean;
|
|
41
|
+
emitDocTypeMap?: boolean;
|
|
42
|
+
includeLabels?: boolean;
|
|
43
|
+
configPath?: string;
|
|
44
|
+
}
|
|
45
|
+
/** Merge file config, then env, then CLI flags (last wins). */
|
|
46
|
+
declare function mergeConfig(file: CodegenFileConfig | undefined, overlay: CliOverlay): ResolvedCodegenConfig;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @module metadata
|
|
50
|
+
* @description Fetches DocType metadata from a live Frappe site via `frappe-js-client`'s
|
|
51
|
+
* `db.getMeta()` (v2-only — `GET /api/v2/doctype/{doctype}/meta`) and normalizes it into the
|
|
52
|
+
* small shape `generate.ts` needs.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
interface DocField {
|
|
56
|
+
fieldname: string;
|
|
57
|
+
fieldtype: string;
|
|
58
|
+
label?: string;
|
|
59
|
+
options?: string;
|
|
60
|
+
reqd?: 0 | 1;
|
|
61
|
+
hidden?: 0 | 1;
|
|
62
|
+
description?: string;
|
|
63
|
+
is_virtual?: 0 | 1;
|
|
64
|
+
}
|
|
65
|
+
interface DocTypeMeta {
|
|
66
|
+
name: string;
|
|
67
|
+
fields: DocField[];
|
|
68
|
+
istable?: 0 | 1;
|
|
69
|
+
/** True for the small set of built-in single-instance DocTypes (e.g. `System Settings`). */
|
|
70
|
+
issingle?: 0 | 1;
|
|
71
|
+
}
|
|
72
|
+
/** Fetches and normalizes the metadata for one DocType. Requires a v2 client. */
|
|
73
|
+
declare function fetchDocTypeMeta(client: FrappeClient, doctype: string): Promise<DocTypeMeta>;
|
|
74
|
+
/** Fetches metadata for several DocTypes with a small concurrency pool. */
|
|
75
|
+
declare function fetchDocTypeMetas(client: FrappeClient, doctypes: readonly string[]): Promise<DocTypeMeta[]>;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @module generate
|
|
79
|
+
* @description Turns normalized `DocTypeMeta` into TypeScript source: one `FrappeDoc<...>`
|
|
80
|
+
* type per DocType, an insert alias, plus `GeneratedDocTypes` / `GeneratedInserts` maps.
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
interface GenerateOptions {
|
|
84
|
+
/**
|
|
85
|
+
* Include fields marked `hidden` in the meta. Default `false`.
|
|
86
|
+
* Frappe `hidden` is form visibility (e.g. Reminder `user` / `notified`), not "absent from the document".
|
|
87
|
+
*/
|
|
88
|
+
includeHidden?: boolean;
|
|
89
|
+
/** Emit a `/** label *\/` doc comment above each field. Default `true`. */
|
|
90
|
+
includeLabels?: boolean;
|
|
91
|
+
/** Emit the GeneratedDocTypes / GeneratedInserts lookup maps. Default `true`. */
|
|
92
|
+
emitDocTypeMap?: boolean;
|
|
93
|
+
}
|
|
94
|
+
/** `Sales Order` -> `SalesOrder`. Not injective for pathological names — collisions are rejected. */
|
|
95
|
+
declare function toInterfaceName(doctype: string): string;
|
|
96
|
+
declare function assertUniqueInterfaceNames(metas: readonly DocTypeMeta[]): void;
|
|
97
|
+
/** Generates one read type plus its insert alias for a single DocType. */
|
|
98
|
+
declare function generateInterface(meta: DocTypeMeta, allMetas?: readonly DocTypeMeta[], opts?: GenerateOptions): string;
|
|
99
|
+
/** Generates a full, self-contained `.ts` module. */
|
|
100
|
+
declare function generateModule(metas: readonly DocTypeMeta[], opts?: GenerateOptions): string;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @module resolve
|
|
104
|
+
* @description Collect DocType names from flags/config/modules and follow Table children.
|
|
105
|
+
*/
|
|
106
|
+
|
|
107
|
+
declare function resolveDocTypes(client: FrappeClient, seeds: {
|
|
108
|
+
doctypes: readonly string[];
|
|
109
|
+
modules: readonly string[];
|
|
110
|
+
}): Promise<string[]>;
|
|
111
|
+
/** Fetch seed metas, then recursively fetch Table / Table MultiSelect children. */
|
|
112
|
+
declare function followChildTables(client: FrappeClient, seeds: readonly DocTypeMeta[]): Promise<DocTypeMeta[]>;
|
|
113
|
+
declare function fetchWithOptionalFollow(client: FrappeClient, doctypes: readonly string[], followTables: boolean): Promise<DocTypeMeta[]>;
|
|
114
|
+
|
|
115
|
+
export { type CliOverlay, type CodegenFileConfig, DEFAULT_CONFIG_NAME, type DocField, type DocTypeMeta, type GenerateOptions, type ResolvedCodegenConfig, assertUniqueInterfaceNames, fetchDocTypeMeta, fetchDocTypeMetas, fetchWithOptionalFollow, findDefaultConfigPath, followChildTables, generateInterface, generateModule, loadConfigFile, mergeConfig, resolveDocTypes, toInterfaceName };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var fs = require('fs');
|
|
4
|
+
var path = require('path');
|
|
5
|
+
|
|
6
|
+
// src/config.ts
|
|
7
|
+
var DEFAULT_CONFIG_NAME = "frappe-codegen.config.json";
|
|
8
|
+
function loadConfigFile(path) {
|
|
9
|
+
const raw = fs.readFileSync(path, "utf8");
|
|
10
|
+
const parsed = JSON.parse(raw);
|
|
11
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
12
|
+
throw new Error(`frappe-codegen: ${path} must be a JSON object`);
|
|
13
|
+
}
|
|
14
|
+
const obj = parsed;
|
|
15
|
+
if ("apiKey" in obj || "apiSecret" in obj || "api-key" in obj) {
|
|
16
|
+
throw new Error("frappe-codegen: do not put API secrets in the config file; use --api-key / FRAPPE_API_KEY");
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
url: typeof obj.url === "string" ? obj.url : void 0,
|
|
20
|
+
out: typeof obj.out === "string" ? obj.out : void 0,
|
|
21
|
+
includeHidden: typeof obj.includeHidden === "boolean" ? obj.includeHidden : void 0,
|
|
22
|
+
followTables: typeof obj.followTables === "boolean" ? obj.followTables : void 0,
|
|
23
|
+
doctypes: Array.isArray(obj.doctypes) ? obj.doctypes.filter((d) => typeof d === "string") : void 0,
|
|
24
|
+
modules: Array.isArray(obj.modules) ? obj.modules.filter((d) => typeof d === "string") : void 0
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function findDefaultConfigPath(cwd = process.cwd()) {
|
|
28
|
+
const candidate = path.resolve(cwd, DEFAULT_CONFIG_NAME);
|
|
29
|
+
return fs.existsSync(candidate) ? candidate : void 0;
|
|
30
|
+
}
|
|
31
|
+
function envString(name) {
|
|
32
|
+
const value = process.env[name];
|
|
33
|
+
return value && value.length > 0 ? value : void 0;
|
|
34
|
+
}
|
|
35
|
+
function mergeConfig(file, overlay) {
|
|
36
|
+
const envUrl = envString("FRAPPE_URL");
|
|
37
|
+
const envKey = envString("FRAPPE_API_KEY");
|
|
38
|
+
const envSecret = envString("FRAPPE_API_SECRET");
|
|
39
|
+
const doctypes = [...file?.doctypes ?? [], ...overlay.doctypes ?? []];
|
|
40
|
+
const modules = [...file?.modules ?? [], ...overlay.modules ?? []];
|
|
41
|
+
return {
|
|
42
|
+
url: overlay.url ?? envUrl ?? file?.url,
|
|
43
|
+
out: overlay.out ?? file?.out ?? "./frappe-types.generated.ts",
|
|
44
|
+
includeHidden: overlay.includeHidden ?? file?.includeHidden ?? false,
|
|
45
|
+
followTables: overlay.followTables ?? file?.followTables ?? true,
|
|
46
|
+
doctypes: [...new Set(doctypes)],
|
|
47
|
+
modules: [...new Set(modules)],
|
|
48
|
+
apiKey: overlay.apiKey ?? envKey,
|
|
49
|
+
apiSecret: overlay.apiSecret ?? envSecret,
|
|
50
|
+
dryRun: overlay.dryRun ?? false,
|
|
51
|
+
emitDocTypeMap: overlay.emitDocTypeMap ?? true,
|
|
52
|
+
includeLabels: overlay.includeLabels ?? true
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/field-types.ts
|
|
57
|
+
var NUMBER_FIELD_TYPES = /* @__PURE__ */ new Set(["Int", "Float", "Currency", "Percent", "Rating", "Duration"]);
|
|
58
|
+
var STRING_FIELD_TYPES = /* @__PURE__ */ new Set([
|
|
59
|
+
"Data",
|
|
60
|
+
"Small Text",
|
|
61
|
+
"Text",
|
|
62
|
+
"Long Text",
|
|
63
|
+
"Text Editor",
|
|
64
|
+
"Code",
|
|
65
|
+
"HTML Editor",
|
|
66
|
+
"Markdown Editor",
|
|
67
|
+
"Password",
|
|
68
|
+
"Read Only",
|
|
69
|
+
"Dynamic Link",
|
|
70
|
+
"Date",
|
|
71
|
+
"Datetime",
|
|
72
|
+
"Time",
|
|
73
|
+
"Attach",
|
|
74
|
+
"Attach Image",
|
|
75
|
+
"Barcode",
|
|
76
|
+
"Color",
|
|
77
|
+
"Signature",
|
|
78
|
+
"Phone",
|
|
79
|
+
"Icon",
|
|
80
|
+
"Autocomplete"
|
|
81
|
+
]);
|
|
82
|
+
var UNKNOWN_FIELD_TYPES = /* @__PURE__ */ new Set(["JSON", "Geolocation"]);
|
|
83
|
+
var LAYOUT_FIELD_TYPES = /* @__PURE__ */ new Set([
|
|
84
|
+
"Section Break",
|
|
85
|
+
"Column Break",
|
|
86
|
+
"Tab Break",
|
|
87
|
+
"Fold",
|
|
88
|
+
"Heading",
|
|
89
|
+
"Button",
|
|
90
|
+
"HTML",
|
|
91
|
+
"Image"
|
|
92
|
+
]);
|
|
93
|
+
function isTableFieldType(fieldtype) {
|
|
94
|
+
return fieldtype === "Table" || fieldtype === "Table MultiSelect";
|
|
95
|
+
}
|
|
96
|
+
function selectLiterals(options) {
|
|
97
|
+
if (!options) return [];
|
|
98
|
+
return options.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).map(selectValue);
|
|
99
|
+
}
|
|
100
|
+
function selectValue(line) {
|
|
101
|
+
const colon = line.indexOf(":");
|
|
102
|
+
const comma = line.indexOf(",");
|
|
103
|
+
if (colon > 0 && (comma < 0 || colon < comma)) {
|
|
104
|
+
return line.slice(0, colon).trim();
|
|
105
|
+
}
|
|
106
|
+
if (comma > 0) {
|
|
107
|
+
return line.slice(0, comma).trim();
|
|
108
|
+
}
|
|
109
|
+
return line;
|
|
110
|
+
}
|
|
111
|
+
function mapFieldType(field, ctx = {}) {
|
|
112
|
+
const { fieldtype, options } = field;
|
|
113
|
+
if (fieldtype === "Check") {
|
|
114
|
+
return "0 | 1";
|
|
115
|
+
}
|
|
116
|
+
if (fieldtype === "Select") {
|
|
117
|
+
const literals = selectLiterals(options);
|
|
118
|
+
if (literals.length === 0) {
|
|
119
|
+
return "string";
|
|
120
|
+
}
|
|
121
|
+
return literals.map((literal) => JSON.stringify(literal)).join(" | ");
|
|
122
|
+
}
|
|
123
|
+
if (isTableFieldType(fieldtype)) {
|
|
124
|
+
const childInterface = options ? ctx.resolveChildInterfaceName?.(options) : void 0;
|
|
125
|
+
return childInterface ? `${childInterface}[]` : "FrappeDoc<Record<string, unknown>>[]";
|
|
126
|
+
}
|
|
127
|
+
if (fieldtype === "Link") {
|
|
128
|
+
if (options && options.trim().length > 0) {
|
|
129
|
+
return `Link<${JSON.stringify(options.trim())}>`;
|
|
130
|
+
}
|
|
131
|
+
return "string";
|
|
132
|
+
}
|
|
133
|
+
if (NUMBER_FIELD_TYPES.has(fieldtype)) {
|
|
134
|
+
return "number";
|
|
135
|
+
}
|
|
136
|
+
if (UNKNOWN_FIELD_TYPES.has(fieldtype)) {
|
|
137
|
+
return "unknown";
|
|
138
|
+
}
|
|
139
|
+
if (STRING_FIELD_TYPES.has(fieldtype)) {
|
|
140
|
+
return "string";
|
|
141
|
+
}
|
|
142
|
+
return "unknown";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/generate.ts
|
|
146
|
+
var DEFAULTS = {
|
|
147
|
+
includeHidden: false,
|
|
148
|
+
includeLabels: true,
|
|
149
|
+
emitDocTypeMap: true
|
|
150
|
+
};
|
|
151
|
+
function toInterfaceName(doctype) {
|
|
152
|
+
const cleaned = doctype.replace(/[^a-zA-Z0-9 ]/g, " ").split(" ").filter(Boolean).map((word) => word[0].toUpperCase() + word.slice(1)).join("");
|
|
153
|
+
return /^[A-Za-z_]/.test(cleaned) ? cleaned : `_${cleaned}`;
|
|
154
|
+
}
|
|
155
|
+
function assertUniqueInterfaceNames(metas) {
|
|
156
|
+
const byName = /* @__PURE__ */ new Map();
|
|
157
|
+
for (const meta of metas) {
|
|
158
|
+
const id = toInterfaceName(meta.name);
|
|
159
|
+
const list = byName.get(id) ?? [];
|
|
160
|
+
list.push(meta.name);
|
|
161
|
+
byName.set(id, list);
|
|
162
|
+
}
|
|
163
|
+
const collisions = [...byName.entries()].filter(([, names]) => names.length > 1);
|
|
164
|
+
if (collisions.length === 0) return;
|
|
165
|
+
const detail = collisions.map(([id, names]) => `${id} <= ${names.map((n) => JSON.stringify(n)).join(", ")}`).join("; ");
|
|
166
|
+
throw new Error(`frappe-codegen: interface name collision: ${detail}`);
|
|
167
|
+
}
|
|
168
|
+
function shouldEmitField(field, options) {
|
|
169
|
+
if (LAYOUT_FIELD_TYPES.has(field.fieldtype)) return false;
|
|
170
|
+
if (field.is_virtual) return false;
|
|
171
|
+
if (field.hidden && !options.includeHidden) return false;
|
|
172
|
+
if (!field.fieldname) return false;
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
function isValidIdentifier(name) {
|
|
176
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name);
|
|
177
|
+
}
|
|
178
|
+
function propertyKey(fieldname) {
|
|
179
|
+
return isValidIdentifier(fieldname) ? fieldname : JSON.stringify(fieldname);
|
|
180
|
+
}
|
|
181
|
+
function isCheckRequiredOnRead(field) {
|
|
182
|
+
return field.fieldtype === "Check";
|
|
183
|
+
}
|
|
184
|
+
function fieldComment(field, includeLabels) {
|
|
185
|
+
if (field.fieldtype === "Password") {
|
|
186
|
+
const secret = "GET usually returns empty or masked (`*`); use db.getPassword to read the secret.";
|
|
187
|
+
if (includeLabels && field.label) {
|
|
188
|
+
return ` /** ${field.label.replace(/\*\//g, "*\u2215")} \u2014 ${secret} */
|
|
189
|
+
`;
|
|
190
|
+
}
|
|
191
|
+
return ` /** ${secret} */
|
|
192
|
+
`;
|
|
193
|
+
}
|
|
194
|
+
if (includeLabels && field.label) {
|
|
195
|
+
return ` /** ${field.label.replace(/\*\//g, "*\u2215")} */
|
|
196
|
+
`;
|
|
197
|
+
}
|
|
198
|
+
return "";
|
|
199
|
+
}
|
|
200
|
+
function generateInterface(meta, allMetas = [meta], opts = {}) {
|
|
201
|
+
const options = { ...DEFAULTS, ...opts };
|
|
202
|
+
const interfaceName = toInterfaceName(meta.name);
|
|
203
|
+
const knownDoctypes = new Set(allMetas.map((m) => m.name));
|
|
204
|
+
const emitted = meta.fields.filter((field) => shouldEmitField(field, options));
|
|
205
|
+
const checkKeys = emitted.filter((field) => field.fieldtype === "Check").map((field) => field.fieldname);
|
|
206
|
+
const fieldLines = emitted.map((field) => {
|
|
207
|
+
const optional = field.reqd || isCheckRequiredOnRead(field) ? "" : "?";
|
|
208
|
+
const type = mapFieldType(field, {
|
|
209
|
+
resolveChildInterfaceName: (childDoctype) => knownDoctypes.has(childDoctype) ? toInterfaceName(childDoctype) : void 0
|
|
210
|
+
});
|
|
211
|
+
return `${fieldComment(field, options.includeLabels)} ${propertyKey(field.fieldname)}${optional}: ${type}`;
|
|
212
|
+
});
|
|
213
|
+
const doctypeLine = ` doctype: ${JSON.stringify(meta.name)}`;
|
|
214
|
+
const inner = [doctypeLine, ...fieldLines].join("\n");
|
|
215
|
+
const body = `{
|
|
216
|
+
${inner}
|
|
217
|
+
}`;
|
|
218
|
+
const insertAlias = checkKeys.length === 0 ? `export type ${interfaceName}Insert = FrappeInsert<${interfaceName}>` : `export type ${interfaceName}Insert = FrappeInsert<Omit<${interfaceName}, ${checkKeys.map((k) => JSON.stringify(k)).join(" | ")}> & Partial<Pick<${interfaceName}, ${checkKeys.map((k) => JSON.stringify(k)).join(" | ")}>>>`;
|
|
219
|
+
return [
|
|
220
|
+
`/** Generated from DocType \`${meta.name}\`. Do not edit by hand \u2014 regenerate with \`frappe-codegen --help\`. */`,
|
|
221
|
+
`export type ${interfaceName} = FrappeDoc<${body}>`,
|
|
222
|
+
insertAlias
|
|
223
|
+
].join("\n");
|
|
224
|
+
}
|
|
225
|
+
function collectImports(source) {
|
|
226
|
+
const names = ["FrappeDoc", "FrappeInsert"];
|
|
227
|
+
if (source.includes("Link<")) {
|
|
228
|
+
names.push("Link");
|
|
229
|
+
}
|
|
230
|
+
return `import type { ${names.join(", ")} } from 'frappe-js-client/types'`;
|
|
231
|
+
}
|
|
232
|
+
function generateModule(metas, opts = {}) {
|
|
233
|
+
const options = { ...DEFAULTS, ...opts };
|
|
234
|
+
assertUniqueInterfaceNames(metas);
|
|
235
|
+
const interfaces = metas.map((meta) => generateInterface(meta, metas, options)).join("\n\n");
|
|
236
|
+
const header = [
|
|
237
|
+
"/**",
|
|
238
|
+
" * This file was generated by frappe-codegen. Do not edit by hand \u2014",
|
|
239
|
+
' * regenerate it instead: `frappe-codegen --url <site> --doctype "..." --out <this file>`.',
|
|
240
|
+
" */",
|
|
241
|
+
collectImports(interfaces),
|
|
242
|
+
""
|
|
243
|
+
].join("\n");
|
|
244
|
+
if (!options.emitDocTypeMap) {
|
|
245
|
+
return `${header}
|
|
246
|
+
${interfaces}
|
|
247
|
+
`;
|
|
248
|
+
}
|
|
249
|
+
const mapEntries = metas.map((meta) => ` ${JSON.stringify(meta.name)}: ${toInterfaceName(meta.name)}`).join("\n");
|
|
250
|
+
const insertEntries = metas.map((meta) => ` ${JSON.stringify(meta.name)}: ${toInterfaceName(meta.name)}Insert`).join("\n");
|
|
251
|
+
const map = [
|
|
252
|
+
"",
|
|
253
|
+
"/** Maps a DocType name to its generated read type. Pass as `createFrappeClient<GeneratedDocTypes>(...)`. */",
|
|
254
|
+
"export interface GeneratedDocTypes {",
|
|
255
|
+
mapEntries,
|
|
256
|
+
"}",
|
|
257
|
+
"",
|
|
258
|
+
"/** Maps a DocType name to its insert payload type. */",
|
|
259
|
+
"export interface GeneratedInserts {",
|
|
260
|
+
insertEntries,
|
|
261
|
+
"}"
|
|
262
|
+
].join("\n");
|
|
263
|
+
return `${header}
|
|
264
|
+
${interfaces}
|
|
265
|
+
${map}
|
|
266
|
+
`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// src/metadata.ts
|
|
270
|
+
async function fetchDocTypeMeta(client, doctype) {
|
|
271
|
+
try {
|
|
272
|
+
const raw = await client.db.getMeta(doctype);
|
|
273
|
+
return {
|
|
274
|
+
name: raw.name ?? doctype,
|
|
275
|
+
fields: raw.fields ?? [],
|
|
276
|
+
istable: raw.istable,
|
|
277
|
+
issingle: raw.issingle
|
|
278
|
+
};
|
|
279
|
+
} catch (error) {
|
|
280
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
281
|
+
const name = error instanceof Error ? error.name : "";
|
|
282
|
+
if (name === "FeatureNotSupportedError" || name === "NotFoundError") {
|
|
283
|
+
throw new Error(
|
|
284
|
+
`frappe-codegen requires Frappe v15+ REST API v2 (GET /api/v2/doctype/{doctype}/meta). Could not fetch metadata for ${JSON.stringify(doctype)}: ${message}`,
|
|
285
|
+
{ cause: error }
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
var FETCH_CONCURRENCY = 4;
|
|
292
|
+
async function mapPool(items, concurrency, mapper) {
|
|
293
|
+
if (items.length === 0) return [];
|
|
294
|
+
const results = new Array(items.length);
|
|
295
|
+
let next = 0;
|
|
296
|
+
async function worker() {
|
|
297
|
+
while (next < items.length) {
|
|
298
|
+
const index = next;
|
|
299
|
+
next += 1;
|
|
300
|
+
results[index] = await mapper(items[index]);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => worker());
|
|
304
|
+
await Promise.all(workers);
|
|
305
|
+
return results;
|
|
306
|
+
}
|
|
307
|
+
async function fetchDocTypeMetas(client, doctypes) {
|
|
308
|
+
return mapPool(doctypes, FETCH_CONCURRENCY, (doctype) => fetchDocTypeMeta(client, doctype));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// src/resolve.ts
|
|
312
|
+
async function listDocTypesInModule(client, moduleName) {
|
|
313
|
+
const names = [];
|
|
314
|
+
for await (const row of client.db.paginate("DocType", {
|
|
315
|
+
filters: [["module", "=", moduleName]],
|
|
316
|
+
fields: ["name"],
|
|
317
|
+
limit: 100
|
|
318
|
+
})) {
|
|
319
|
+
if (typeof row.name === "string") {
|
|
320
|
+
names.push(row.name);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return names;
|
|
324
|
+
}
|
|
325
|
+
async function resolveDocTypes(client, seeds) {
|
|
326
|
+
const names = new Set(seeds.doctypes);
|
|
327
|
+
for (const moduleName of seeds.modules) {
|
|
328
|
+
for (const name of await listDocTypesInModule(client, moduleName)) {
|
|
329
|
+
names.add(name);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return [...names];
|
|
333
|
+
}
|
|
334
|
+
function childTableDoctypes(meta) {
|
|
335
|
+
const children = [];
|
|
336
|
+
for (const field of meta.fields) {
|
|
337
|
+
if (isTableFieldType(field.fieldtype) && field.options) {
|
|
338
|
+
children.push(field.options);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return children;
|
|
342
|
+
}
|
|
343
|
+
async function followChildTables(client, seeds) {
|
|
344
|
+
const byName = /* @__PURE__ */ new Map();
|
|
345
|
+
for (const meta of seeds) {
|
|
346
|
+
byName.set(meta.name, meta);
|
|
347
|
+
}
|
|
348
|
+
const pending = /* @__PURE__ */ new Set();
|
|
349
|
+
for (const meta of seeds) {
|
|
350
|
+
for (const child of childTableDoctypes(meta)) {
|
|
351
|
+
if (!byName.has(child)) pending.add(child);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
while (pending.size > 0) {
|
|
355
|
+
const batch = [...pending];
|
|
356
|
+
pending.clear();
|
|
357
|
+
const fetched = await fetchDocTypeMetas(client, batch);
|
|
358
|
+
for (const meta of fetched) {
|
|
359
|
+
byName.set(meta.name, meta);
|
|
360
|
+
for (const child of childTableDoctypes(meta)) {
|
|
361
|
+
if (!byName.has(child)) pending.add(child);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return [...byName.values()];
|
|
366
|
+
}
|
|
367
|
+
async function fetchWithOptionalFollow(client, doctypes, followTables) {
|
|
368
|
+
const seeds = await fetchDocTypeMetas(client, doctypes);
|
|
369
|
+
if (!followTables) return seeds;
|
|
370
|
+
return followChildTables(client, seeds);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
exports.DEFAULT_CONFIG_NAME = DEFAULT_CONFIG_NAME;
|
|
374
|
+
exports.assertUniqueInterfaceNames = assertUniqueInterfaceNames;
|
|
375
|
+
exports.fetchDocTypeMeta = fetchDocTypeMeta;
|
|
376
|
+
exports.fetchDocTypeMetas = fetchDocTypeMetas;
|
|
377
|
+
exports.fetchWithOptionalFollow = fetchWithOptionalFollow;
|
|
378
|
+
exports.findDefaultConfigPath = findDefaultConfigPath;
|
|
379
|
+
exports.followChildTables = followChildTables;
|
|
380
|
+
exports.generateInterface = generateInterface;
|
|
381
|
+
exports.generateModule = generateModule;
|
|
382
|
+
exports.loadConfigFile = loadConfigFile;
|
|
383
|
+
exports.mergeConfig = mergeConfig;
|
|
384
|
+
exports.resolveDocTypes = resolveDocTypes;
|
|
385
|
+
exports.toInterfaceName = toInterfaceName;
|
|
386
|
+
//# sourceMappingURL=index.js.map
|
|
387
|
+
//# sourceMappingURL=index.js.map
|