Skip to content

Commit 57287c5

Browse files
authored
Merge pull request #128 from Exabyte-io/chore/SOF-7687
SOF-7687: add generateSchemaMixin + node22 build fix
2 parents faf537c + 65068c2 commit 57287c5

File tree

8 files changed

+873
-439
lines changed

8 files changed

+873
-439
lines changed

dist/js/generateSchemaMixin.d.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/usr/bin/env node
2+
import type { JSONSchema7 } from "json-schema";
3+
/**
4+
* Generates mixins for multiple schemas
5+
* @param schemas - Array of JSON schemas to use for generation
6+
* @param outputPaths - Object mapping schema IDs to output file paths
7+
* @param skipFields - Array of field names to skip during generation
8+
* @returns - Object with success and error counts
9+
*/
10+
declare function generateShemaMixin(schemas: JSONSchema7[], outputPaths: Record<string, string>, skipFields?: string[]): {
11+
successCount: number;
12+
errorCount: number;
13+
};
14+
/**
15+
* @example
16+
* ```ts
17+
* import generateShemaMixin from "@mat3ra/code/dist/js/generateSchemaMixin";
18+
* import allSchemas from "@mat3ra/esse/dist/js/schemas.json";
19+
*
20+
* const result = generateShemaMixin(allSchemas, OUTPUT_PATHS, SKIP_FIELDS);
21+
*
22+
* if (result.errorCount > 0) {
23+
* process.exit(1);
24+
* }
25+
* ```
26+
*/
27+
export default generateShemaMixin;

dist/js/generateSchemaMixin.js

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
#!/usr/bin/env node
2+
"use strict";
3+
/* eslint-disable no-restricted-syntax */
4+
var __importDefault = (this && this.__importDefault) || function (mod) {
5+
return (mod && mod.__esModule) ? mod : { "default": mod };
6+
};
7+
Object.defineProperty(exports, "__esModule", { value: true });
8+
/**
9+
* Script to generate mixin properties from JSON schema
10+
*
11+
* This script generates mixin functions for property/holder, property/meta_holder,
12+
* and property/proto_holder schemas automatically.
13+
*
14+
* Usage:
15+
* node scripts/generate-mixin-properties.js
16+
*/
17+
const JSONSchemasInterface_1 = __importDefault(require("@mat3ra/esse/dist/js/esse/JSONSchemasInterface"));
18+
const child_process_1 = require("child_process");
19+
const fs_1 = __importDefault(require("fs"));
20+
/**
21+
* Determines if a property should use requiredProp() or prop()
22+
* @param propertyName - Name of the property
23+
* @param requiredProperties - Array of required property names
24+
* @returns - True if property is required
25+
*/
26+
function isRequiredProperty(propertyName, requiredProperties) {
27+
return requiredProperties.includes(propertyName);
28+
}
29+
/**
30+
* Generates TypeScript type annotation for a property
31+
* @param propertyName - The property name
32+
* @param schemaName - Name of the schema (for type reference)
33+
* @returns - TypeScript type annotation
34+
*/
35+
function generateTypeAnnotation(propertyName, schemaName) {
36+
return `${schemaName}["${propertyName}"]`;
37+
}
38+
/**
39+
* Extracts properties from a schema, handling allOf if present
40+
* @param schema - The JSON schema
41+
* @returns - Object with properties and required fields
42+
*/
43+
function extractSchemaProperties(schema) {
44+
let properties = {};
45+
let required = [];
46+
// Handle allOf by merging properties from all schemas
47+
if (schema.allOf && Array.isArray(schema.allOf)) {
48+
for (const subSchema of schema.allOf) {
49+
const extracted = extractSchemaProperties(subSchema);
50+
properties = { ...properties, ...extracted.properties };
51+
required = [...required, ...extracted.required];
52+
}
53+
}
54+
// Add properties from current schema
55+
if (schema.properties) {
56+
properties = { ...properties, ...schema.properties };
57+
}
58+
if (schema.required) {
59+
required = [...required, ...schema.required];
60+
}
61+
return { properties, required };
62+
}
63+
/**
64+
* Generates the complete mixin function
65+
* @param schema - The JSON schema
66+
* @param schemaName - Name of the schema
67+
* @param mixinTypeName - Name of the mixin type
68+
* @param entityTypeName - Name of the entity type
69+
* @param skipFields - Array of field names to skip
70+
* @returns - Generated TypeScript code
71+
*/
72+
function generateMixinFunction(schema, schemaName, mixinTypeName, entityTypeName, skipFields = []) {
73+
// Convert mixin type name to camelCase for function name
74+
const functionName = mixinTypeName.charAt(0).toLowerCase() + mixinTypeName.slice(1);
75+
// Extract properties, handling allOf if present
76+
const { properties, required } = extractSchemaProperties(schema);
77+
if (Object.keys(properties).length === 0) {
78+
throw new Error("No properties found in schema");
79+
}
80+
// Filter out skip fields
81+
const propertyEntries = Object.entries(properties).filter(([propertyName]) => !skipFields.includes(propertyName));
82+
let code = `import type { InMemoryEntity } from "@mat3ra/code/dist/js/entity";\n`;
83+
code += `import type { ${schemaName} } from "@mat3ra/esse/dist/js/types";\n\n`;
84+
// Generate the mixin type using Omit utility
85+
const skipFieldNames = skipFields.map((field) => `"${field}"`).join(" | ");
86+
code += `export type ${mixinTypeName} = Omit<${schemaName}, ${skipFieldNames}>;\n\n`;
87+
// Generate the entity type
88+
code += `export type ${entityTypeName} = InMemoryEntity & ${mixinTypeName};\n\n`;
89+
code += `export function ${functionName}(item: InMemoryEntity) {\n`;
90+
code += ` // @ts-expect-error\n`;
91+
code += ` const properties: InMemoryEntity & ${mixinTypeName} = {\n`;
92+
for (let i = 0; i < propertyEntries.length; i++) {
93+
const [propertyName] = propertyEntries[i];
94+
const isRequired = isRequiredProperty(propertyName, required);
95+
const methodName = isRequired ? "requiredProp" : "prop";
96+
const typeAnnotation = generateTypeAnnotation(propertyName, schemaName);
97+
code += `get ${propertyName}() {\n`;
98+
code += `return this.${methodName}<${typeAnnotation}>("${propertyName}");\n`;
99+
code += `}`;
100+
// Add comma for all properties except the last one
101+
if (i < propertyEntries.length - 1) {
102+
code += `,\n`;
103+
}
104+
else {
105+
code += `,\n`;
106+
}
107+
}
108+
code += ` };\n\n`;
109+
code += ` Object.defineProperties(item, Object.getOwnPropertyDescriptors(properties));\n`;
110+
code += `}\n`;
111+
return code;
112+
}
113+
/**
114+
* Generates mixin function for a given schema ID
115+
* @param schemaId - The schema ID (e.g., "property/holder")
116+
* @param outputPath - The output file path
117+
* @param skipFields - Array of field names to skip
118+
* @returns - Generated TypeScript code
119+
*/
120+
function generateMixinFromSchemaId(schemaId, outputPath, skipFields = []) {
121+
var _a, _b;
122+
// Get the resolved schema by ID
123+
const schema = JSONSchemasInterface_1.default.getSchemaById(schemaId);
124+
if (!schema) {
125+
throw new Error(`Schema not found with ID: ${schemaId}`);
126+
}
127+
// Extract schema name from title for import
128+
let schemaName;
129+
if (schema.title) {
130+
// Convert title to proper schema name
131+
schemaName = schema.title
132+
.split(/\s+/)
133+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
134+
.join("");
135+
}
136+
else {
137+
// Convert schema ID to proper schema name
138+
schemaName =
139+
schemaId
140+
.split(/[/-]/)
141+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
142+
.join("") + "Schema";
143+
}
144+
// Extract type names from output file path
145+
const fileName = (_b = (_a = outputPath.split("/").pop()) === null || _a === void 0 ? void 0 : _a.replace(".ts", "")) !== null && _b !== void 0 ? _b : "";
146+
if (!fileName) {
147+
throw new Error(`Invalid output path: ${outputPath}`);
148+
}
149+
const mixinTypeName = fileName;
150+
const entityTypeName = fileName.replace("SchemaMixin", "InMemoryEntity");
151+
// Generate the complete mixin function
152+
return generateMixinFunction(schema, schemaName, mixinTypeName, entityTypeName, skipFields);
153+
}
154+
/**
155+
* Runs ESLint autofix on generated files
156+
* @param filePaths - Array of file paths to fix
157+
*/
158+
function runESLintAutofix(filePaths) {
159+
if (filePaths.length === 0)
160+
return;
161+
try {
162+
console.log("Running ESLint autofix on generated files...");
163+
const filesToFix = filePaths.join(" ");
164+
(0, child_process_1.execSync)(`npx eslint --fix ${filesToFix}`, { stdio: "inherit" });
165+
console.log("✓ ESLint autofix completed successfully");
166+
}
167+
catch (error) {
168+
console.warn("⚠ ESLint autofix failed:", error instanceof Error ? error.message : String(error));
169+
// Don't fail the entire process if ESLint autofix fails
170+
}
171+
}
172+
/**
173+
* Generates mixins for multiple schemas
174+
* @param schemas - Array of JSON schemas to use for generation
175+
* @param outputPaths - Object mapping schema IDs to output file paths
176+
* @param skipFields - Array of field names to skip during generation
177+
* @returns - Object with success and error counts
178+
*/
179+
function generateShemaMixin(schemas, outputPaths, skipFields = []) {
180+
// Setup schemas
181+
JSONSchemasInterface_1.default.setSchemas(schemas);
182+
console.log("Generating mixin properties for all schemas...");
183+
const schemaIds = Object.keys(outputPaths);
184+
let successCount = 0;
185+
let errorCount = 0;
186+
const generatedFiles = [];
187+
for (const schemaId of schemaIds) {
188+
try {
189+
console.log(`\nProcessing schema: ${schemaId}`);
190+
const outputPath = outputPaths[schemaId];
191+
if (!outputPath) {
192+
throw new Error(`No output path defined for schema: ${schemaId}`);
193+
}
194+
const generatedCode = generateMixinFromSchemaId(schemaId, outputPath, skipFields);
195+
// Ensure the directory exists
196+
const dir = outputPath.substring(0, outputPath.lastIndexOf("/"));
197+
if (!fs_1.default.existsSync(dir)) {
198+
fs_1.default.mkdirSync(dir, { recursive: true });
199+
}
200+
fs_1.default.writeFileSync(outputPath, generatedCode);
201+
console.log(`✓ Generated mixin written to: ${outputPath}`);
202+
generatedFiles.push(outputPath);
203+
successCount += 1;
204+
}
205+
catch (error) {
206+
console.error(`✗ Error processing schema ${schemaId}: ${error instanceof Error ? error.message : String(error)}`);
207+
errorCount += 1;
208+
}
209+
}
210+
// Run ESLint autofix on generated files
211+
if (generatedFiles.length > 0) {
212+
runESLintAutofix(generatedFiles);
213+
}
214+
console.log(`\n=== Summary ===`);
215+
console.log(`Successfully generated: ${successCount} mixins`);
216+
if (errorCount > 0) {
217+
console.log(`Errors: ${errorCount} schemas failed`);
218+
}
219+
else {
220+
console.log("All mixins generated successfully!");
221+
}
222+
return { successCount, errorCount };
223+
}
224+
/**
225+
* @example
226+
* ```ts
227+
* import generateShemaMixin from "@mat3ra/code/dist/js/generateSchemaMixin";
228+
* import allSchemas from "@mat3ra/esse/dist/js/schemas.json";
229+
*
230+
* const result = generateShemaMixin(allSchemas, OUTPUT_PATHS, SKIP_FIELDS);
231+
*
232+
* if (result.errorCount > 0) {
233+
* process.exit(1);
234+
* }
235+
* ```
236+
*/
237+
exports.default = generateShemaMixin;

0 commit comments

Comments
 (0)