From 578ed03e471f94bbbf92cdcc3c737493a2e809ee Mon Sep 17 00:00:00 2001 From: Rory& Date: Thu, 23 Oct 2025 22:27:46 +0200 Subject: [PATCH] Improve openapi schema generation script --- scripts/schema.js | 561 +++++++++++++++++++++++----------------------- 1 file changed, 277 insertions(+), 284 deletions(-) diff --git a/scripts/schema.js b/scripts/schema.js index 67c5563c3..07274994a 100644 --- a/scripts/schema.js +++ b/scripts/schema.js @@ -24,10 +24,10 @@ const totalSw = Stopwatch.startNew(); const conWarn = console.warn; console.warn = (...args) => { - // silence some expected warnings - if (args[0] === "initializer is expression for property id") return; - if (args[0].startsWith("unknown initializer for property ") && args[0].endsWith("[object Object]")) return; - conWarn(...args); + // silence some expected warnings + if (args[0] === "initializer is expression for property id") return; + if (args[0].startsWith("unknown initializer for property ") && args[0].endsWith("[object Object]")) return; + conWarn(...args); }; const path = require("path"); @@ -41,27 +41,27 @@ const exclusionList = JSON.parse(fs.readFileSync(path.join(__dirname, "schemaExc // @type {TJS.PartialArgs} const settings = { - required: true, - ignoreErrors: true, - excludePrivate: true, - defaultNumberType: "integer", - noExtraProps: true, - defaultProps: false, - useTypeOfKeyword: true, // should help catch functions? + required: true, + ignoreErrors: true, + excludePrivate: true, + defaultNumberType: "integer", + noExtraProps: true, + defaultProps: false, + useTypeOfKeyword: true, // should help catch functions? }; const baseClassProperties = [ - // BaseClass methods - "toJSON", - "hasId", - "save", - "remove", - "softRemove", - "recover", - "reload", - "assign", - "_do_validate", // ? - "hasId", // ? + // BaseClass methods + "toJSON", + "hasId", + "save", + "remove", + "softRemove", + "recover", + "reload", + "assign", + "_do_validate", // ? + "hasId", // ? ]; const ExcludeAndWarn = [...exclusionList.manualWarn, ...exclusionList.manualWarnRe.map((r) => new RegExp(r))]; @@ -69,315 +69,308 @@ const Excluded = [...exclusionList.manual, ...exclusionList.manualRe.map((r) => const Included = [...exclusionList.include, ...exclusionList.includeRe.map((r) => new RegExp(r))]; const excludedLambdas = [ - (n, s) => { - // attempt to import - if (JSON.stringify(s).includes(`#/definitions/import(`)) { - console.log(`\r${redBright("[WARN]")} Omitting schema ${n} as it attempted to use import().`); - exclusionList.auto.push({ value: n, reason: "Uses import()" }); - return true; - } - }, - (n, s) => { - if (JSON.stringify(s).includes(process.cwd())) { - console.log(`\r${redBright("[WARN]")} Omitting schema ${n} as it leaked $PWD.`); - exclusionList.auto.push({ value: n, reason: "Leaked $PWD" }); - return true; - } - }, - (n, s) => { - if (JSON.stringify(s).includes(process.env.HOME)) { - console.log(`\r${redBright("[WARN]")} Omitting schema ${n} as it leaked a $HOME path.`); - exclusionList.auto.push({ value: n, reason: "Leaked $HOME" }); - return true; - } - }, - (n, s) => { - if (s["$ref"] === `#/definitions/${n}`) { - console.log(`\r${redBright("[WARN]")} Omitting schema ${n} as it is a self-reference only schema.`); - exclusionList.auto.push({ value: n, reason: "Self-reference only schema" }); - // fs.writeFileSync(`fucked/${n}.json`, JSON.stringify(s, null, 4)); - return true; - } - }, - (n, s) => { - if (s.description?.match(/Smithy/)) { - console.log(`\r${redBright("[WARN]")} Omitting schema ${n} as it appears to be an AWS Smithy schema.`); - exclusionList.auto.push({ value: n, reason: "AWS Smithy schema" }); - return true; - } - }, - (n, s) => { - if (s.description?.startsWith("

")) { - console.log(`\r${redBright("[WARN]")} Omitting schema ${n} as we don't use HTML paragraphs for descriptions.`); - exclusionList.auto.push({ value: n, reason: "HTML paragraph in description" }); - return true; - } - }, - (n, s) => { - if (s.properties && Object.keys(s.properties).every((x) => x[0] === x[0].toUpperCase())) { - console.log(`\r${redBright("[WARN]")} Omitting schema ${n} as all its properties have uppercase characters.`); - exclusionList.auto.push({ value: n, reason: "Schema with only uppercase properties" }); - return true; - } - }, - // (n, s) => { - // if (JSON.stringify(s).length <= 300) { - // console.log({n, s}); - // } - // } + (n, s) => { + // attempt to import + if (JSON.stringify(s).includes(`#/definitions/import(`)) { + console.log(`\r${redBright("[WARN]")} Omitting schema ${n} as it attempted to use import().`); + exclusionList.auto.push({ value: n, reason: "Uses import()" }); + return true; + } + }, + (n, s) => { + if (JSON.stringify(s).includes(process.cwd())) { + console.log(`\r${redBright("[WARN]")} Omitting schema ${n} as it leaked $PWD.`); + exclusionList.auto.push({ value: n, reason: "Leaked $PWD" }); + return true; + } + }, + (n, s) => { + if (JSON.stringify(s).includes(process.env.HOME)) { + console.log(`\r${redBright("[WARN]")} Omitting schema ${n} as it leaked a $HOME path.`); + exclusionList.auto.push({ value: n, reason: "Leaked $HOME" }); + return true; + } + }, + (n, s) => { + if (s["$ref"] === `#/definitions/${n}`) { + console.log(`\r${redBright("[WARN]")} Omitting schema ${n} as it is a self-reference only schema.`); + exclusionList.auto.push({ value: n, reason: "Self-reference only schema" }); + // fs.writeFileSync(`fucked/${n}.json`, JSON.stringify(s, null, 4)); + return true; + } + }, + (n, s) => { + if (s.description?.match(/Smithy/)) { + console.log(`\r${redBright("[WARN]")} Omitting schema ${n} as it appears to be an AWS Smithy schema.`); + exclusionList.auto.push({ value: n, reason: "AWS Smithy schema" }); + return true; + } + }, + (n, s) => { + if (s.description?.startsWith("

")) { + console.log(`\r${redBright("[WARN]")} Omitting schema ${n} as we don't use HTML paragraphs for descriptions.`); + exclusionList.auto.push({ value: n, reason: "HTML paragraph in description" }); + return true; + } + }, + // (n, s) => { + // if (JSON.stringify(s).length <= 300) { + // console.log({n, s}); + // } + // } ]; function includesMatch(haystack, needles, log = false) { - for (const needle of needles) { - const match = needle instanceof RegExp ? needle.test(haystack) : haystack === needle; - if (match) { - if (log) console.warn(redBright("[WARN]:"), "Excluding schema", haystack, "due to match with", needle); - return needle; - } - } - return null; + for (const needle of needles) { + const match = needle instanceof RegExp ? needle.test(haystack) : haystack === needle; + if (match) { + if (log) console.warn(redBright("[WARN]:"), "Excluding schema", haystack, "due to match with", needle); + return needle; + } + } + return null; } async function main() { - const stepSw = Stopwatch.startNew(); + const stepSw = Stopwatch.startNew(); - process.stdout.write("Loading program... "); - const program = TJS.programFromConfig(path.join(__dirname, "..", "tsconfig.json"), walk(path.join(__dirname, "..", "src", "schemas"))); - const generator = TJS.buildGenerator(program, settings); - if (!generator || !program) { - console.log(redBright("Failed to create schema generator.")); - return; - } + process.stdout.write("Loading program... "); + const program = TJS.programFromConfig(path.join(__dirname, "..", "tsconfig.json"), walk(path.join(__dirname, "..", "src", "schemas"))); + const generator = TJS.buildGenerator(program, settings); + if (!generator || !program) { + console.log(redBright("Failed to create schema generator.")); + return; + } - const elapsedLoad = stepSw.getElapsedAndReset(); - process.stdout.write("Done in " + yellowBright(elapsedLoad.totalMilliseconds + "." + elapsedLoad.microseconds) + " ms\n"); + const elapsedLoad = stepSw.getElapsedAndReset(); + process.stdout.write("Done in " + yellowBright(elapsedLoad.totalMilliseconds + "." + elapsedLoad.microseconds) + " ms\n"); - process.stdout.write("Generating schema list... "); - let schemas = generator.getUserSymbols().filter((x) => { - return ( - (x.endsWith("Schema") || x.endsWith("Response") || x.startsWith("API")) && - // !ExcludeAndWarn.some((exc) => { - // const match = exc instanceof RegExp ? exc.test(x) : x === exc; - // if (match) console.warn("Warning: Excluding schema", x); - // return match; - // }) && - // !Excluded.some((exc) => (exc instanceof RegExp ? exc.test(x) : x === exc)) - (includesMatch(x, Included) || (!includesMatch(x, ExcludeAndWarn, true) && !includesMatch(x, Excluded))) - ); - }); - //.sort((a,b) => a.localeCompare(b)); + process.stdout.write("Generating schema list... "); + let schemas = generator.getUserSymbols().filter((x) => { + return ( + (x.endsWith("Schema") || x.endsWith("Response") || x.startsWith("API")) && + // !ExcludeAndWarn.some((exc) => { + // const match = exc instanceof RegExp ? exc.test(x) : x === exc; + // if (match) console.warn("Warning: Excluding schema", x); + // return match; + // }) && + // !Excluded.some((exc) => (exc instanceof RegExp ? exc.test(x) : x === exc)) + (includesMatch(x, Included) || (!includesMatch(x, ExcludeAndWarn, true) && !includesMatch(x, Excluded))) + ); + }); + //.sort((a,b) => a.localeCompare(b)); - const elapsedList = stepSw.getElapsedAndReset(); - process.stdout.write("Done in " + yellowBright(elapsedList.totalMilliseconds + "." + elapsedList.microseconds) + " ms\n"); - console.log("Found", yellowBright(schemas.length), "schemas to process."); + const elapsedList = stepSw.getElapsedAndReset(); + process.stdout.write("Done in " + yellowBright(elapsedList.totalMilliseconds + "." + elapsedList.microseconds) + " ms\n"); + console.log("Found", yellowBright(schemas.length), "schemas to process."); - let definitions = {}; - let nestedDefinitions = {}; - let writePromises = []; + let definitions = {}; + let nestedDefinitions = {}; + let writePromises = []; - if (process.env.WRITE_SCHEMA_DIR === "true") { - fs.rmSync("schemas_orig", { recursive: true, force: true }); - fs.mkdirSync("schemas_orig"); + if (process.env.WRITE_SCHEMA_DIR === "true") { + fs.rmSync("schemas_orig", { recursive: true, force: true }); + fs.mkdirSync("schemas_orig"); - fs.rmSync("schemas_nested", { recursive: true, force: true }); - fs.mkdirSync("schemas_nested"); + fs.rmSync("schemas_nested", { recursive: true, force: true }); + fs.mkdirSync("schemas_nested"); - fs.rmSync("schemas_final", { recursive: true, force: true }); - fs.mkdirSync("schemas_final"); - } + fs.rmSync("schemas_final", { recursive: true, force: true }); + fs.mkdirSync("schemas_final"); + } - const schemaSw = Stopwatch.startNew(); - for (const name of schemas) { - process.stdout.write(`Processing schema ${name}... `); - const part = TJS.generateSchema(program, name, settings, [], generator); - if (!part) continue; + const schemaSw = Stopwatch.startNew(); + for (const name of schemas) { + process.stdout.write(`Processing schema ${name}... `); + const part = TJS.generateSchema(program, name, settings, [], generator); + if (!part) continue; - if (definitions[name]) { - process.stdout.write(yellow(` [ERROR] Duplicate schema name detected: ${name}. Overwriting previous schema.`)); - } + if (definitions[name]) { + process.stdout.write(yellow(` [ERROR] Duplicate schema name detected: ${name}. Overwriting previous schema.`)); + } - if (!includesMatch(name, Included) && excludedLambdas.some((fn) => fn(name, part))) { - continue; - } + if (!includesMatch(name, Included) && excludedLambdas.some((fn) => fn(name, part))) { + continue; + } - if (process.env.WRITE_SCHEMA_DIR === "true") writePromises.push(async () => await fsp.writeFile(path.join("schemas_orig", `${name}.json`), JSON.stringify(part, null, 4))); + if (process.env.WRITE_SCHEMA_DIR === "true") writePromises.push(async () => await fsp.writeFile(path.join("schemas_orig", `${name}.json`), JSON.stringify(part, null, 4))); - // testing: - function mergeDefs(schemaName, schema) { - if (schema.definitions) { - // schema["x-sb-defs"] = Object.keys(schema.definitions); - process.stdout.write(cyanBright("Processing nested... ")); - for (const defKey in schema.definitions) { - if (definitions[defKey] && deepEqual(definitions[defKey], schema.definitions[defKey])) { - // console.log("Definition", defKey, "from schema", schemaName, "is identical to existing definition, skipping."); - schema.definitions = Object.fromEntries(Object.entries(schema.definitions).filter(([k, v]) => k !== defKey)); - process.stdout.write(greenBright("T")); - } else if (!nestedDefinitions[defKey]) { - nestedDefinitions[defKey] = schema.definitions[defKey]; - schema.definitions = Object.fromEntries(Object.entries(schema.definitions).filter(([k, v]) => k !== defKey)); - // console.log("Tracking sub-definition", defKey, "from schema", schemaName); - process.stdout.write(green("N")); - } else if (!deepEqual(nestedDefinitions[defKey], schema.definitions[defKey])) { - console.log(redBright("[ERROR]"), "Conflicting nested definition for", defKey, "found in schema", schemaName); - console.log(columnizedObjectDiff(nestedDefinitions[defKey], schema.definitions[defKey], true)); - } else { - // console.log("Definition", defKey, "from schema", schemaName, "is identical to existing definition, skipping."); - schema.definitions = Object.fromEntries(Object.entries(schema.definitions).filter(([k, v]) => k !== defKey)); - process.stdout.write(greenBright("M")); - } - } - if (Object.keys(schema.definitions).length === 0) { - process.stdout.write(greenBright("✓ ")); - delete schema.definitions; - } else { - console.log("Remaining definitions in schema", schemaName, "after merge:", Object.keys(schema.definitions)); - } - } - } - mergeDefs(name, part); + // testing: + function mergeDefs(schemaName, schema) { + if (schema.definitions) { + // schema["x-sb-defs"] = Object.keys(schema.definitions); + process.stdout.write(cyanBright("Processing nested... ")); + for (const defKey in schema.definitions) { + if (definitions[defKey] && deepEqual(definitions[defKey], schema.definitions[defKey])) { + // console.log("Definition", defKey, "from schema", schemaName, "is identical to existing definition, skipping."); + schema.definitions = Object.fromEntries(Object.entries(schema.definitions).filter(([k, v]) => k !== defKey)); + process.stdout.write(greenBright("T")); + } else if (!nestedDefinitions[defKey]) { + nestedDefinitions[defKey] = schema.definitions[defKey]; + schema.definitions = Object.fromEntries(Object.entries(schema.definitions).filter(([k, v]) => k !== defKey)); + // console.log("Tracking sub-definition", defKey, "from schema", schemaName); + process.stdout.write(green("N")); + } else if (!deepEqual(nestedDefinitions[defKey], schema.definitions[defKey])) { + console.log(redBright("[ERROR]"), "Conflicting nested definition for", defKey, "found in schema", schemaName); + console.log(columnizedObjectDiff(nestedDefinitions[defKey], schema.definitions[defKey], true)); + } else { + // console.log("Definition", defKey, "from schema", schemaName, "is identical to existing definition, skipping."); + schema.definitions = Object.fromEntries(Object.entries(schema.definitions).filter(([k, v]) => k !== defKey)); + process.stdout.write(greenBright("M")); + } + } + if (Object.keys(schema.definitions).length === 0) { + process.stdout.write(greenBright("✓ ")); + delete schema.definitions; + } else { + console.log("Remaining definitions in schema", schemaName, "after merge:", Object.keys(schema.definitions)); + } + } + } + mergeDefs(name, part); - const elapsed = schemaSw.getElapsedAndReset(); - process.stdout.write( - "Done in " + yellowBright(elapsed.totalMilliseconds + "." + elapsed.microseconds) + " ms, " + yellowBright(JSON.stringify(part).length) + " bytes (unformatted) ", - ); - if (elapsed.totalMilliseconds >= 20) console.log(bgRedBright("\x1b[5m[SLOW]\x1b[25m")); - else console.log(); + const elapsed = schemaSw.getElapsedAndReset(); + process.stdout.write( + "Done in " + yellowBright(elapsed.totalMilliseconds + "." + elapsed.microseconds) + " ms, " + yellowBright(JSON.stringify(part).length) + " bytes (unformatted) ", + ); + if (elapsed.totalMilliseconds >= 20) console.log(bgRedBright("\x1b[5m[SLOW]\x1b[25m")); + else console.log(); - definitions = { ...definitions, [name]: { ...part } }; - } - console.log("Processed", Object.keys(definitions).length, "schemas in", Number(stepSw.elapsed().totalMilliseconds + "." + stepSw.elapsed().microseconds), "ms."); + definitions = { ...definitions, [name]: { ...part } }; + } + console.log("Processed", Object.keys(definitions).length, "schemas in", Number(stepSw.elapsed().totalMilliseconds + "." + stepSw.elapsed().microseconds), "ms."); - console.log("Merging nested definitions into main definitions..."); - let isNewLine = true; - for (const defKey in nestedDefinitions) { - if (!includesMatch(defKey, Included, false)) { - const bannedMatch = includesMatch(defKey, ExcludeAndWarn, false) ?? includesMatch(defKey, Excluded, false); - if (bannedMatch !== null) { - // console.log(yellowBright("\n[WARN]"), "Skipping nested definition", defKey, "as it matched a banned format."); - console.log((isNewLine ? "" : "\n") + redBright("WARNING") + " Excluding schema " + yellowBright(defKey) + " due to match with " + redBright(bannedMatch)); - isNewLine = true; - continue; - } - } + console.log("Merging nested definitions into main definitions..."); + let isNewLine = true; + for (const defKey in nestedDefinitions) { + if (!includesMatch(defKey, Included, false)) { + const bannedMatch = includesMatch(defKey, ExcludeAndWarn, false) ?? includesMatch(defKey, Excluded, false); + if (bannedMatch !== null) { + // console.log(yellowBright("\n[WARN]"), "Skipping nested definition", defKey, "as it matched a banned format."); + console.log((isNewLine ? "" : "\n") + redBright("WARNING") + " Excluding schema " + yellowBright(defKey) + " due to match with " + redBright(bannedMatch)); + isNewLine = true; + continue; + } + } - nestedDefinitions[defKey]["$schema"] = "http://json-schema.org/draft-07/schema#"; - if (definitions[defKey]) { - if (!deepEqual(definitions[defKey], nestedDefinitions[defKey])) { - if (Object.keys(definitions[defKey]).every((k) => k === "$ref" || k === "$schema")) { - definitions[defKey] = nestedDefinitions[defKey]; - console.log(yellowBright("\nWARNING"), "Overwriting definition for", defKey, "with nested definition (ref/schema only)."); - isNewLine = true; - } else { - console.log(redBright("\nERROR"), "Conflicting definition for", defKey, "found in main definitions."); - console.log(columnizedObjectDiff(definitions[defKey], nestedDefinitions[defKey], true)); - console.log("Keys:", Object.keys(definitions[defKey]), Object.keys(nestedDefinitions[defKey])); - isNewLine = true; - } - } else { - // console.log("Definition", defKey, "is identical to existing definition, skipping."); - } - } else { - definitions[defKey] = nestedDefinitions[defKey]; - if (isNewLine) { - process.stdout.write("Adding nested definitions to main definitions: "); - isNewLine = false; - } else process.stdout.write("\x1b[4D, "); - process.stdout.write(yellowBright(defKey) + "... "); - } - } + nestedDefinitions[defKey]["$schema"] = "http://json-schema.org/draft-07/schema#"; + if (definitions[defKey]) { + if (!deepEqual(definitions[defKey], nestedDefinitions[defKey])) { + if (Object.keys(definitions[defKey]).every((k) => k === "$ref" || k === "$schema")) { + definitions[defKey] = nestedDefinitions[defKey]; + console.log(yellowBright("\nWARNING"), "Overwriting definition for", defKey, "with nested definition (ref/schema only)."); + isNewLine = true; + } else { + console.log(redBright("\nERROR"), "Conflicting definition for", defKey, "found in main definitions."); + console.log(columnizedObjectDiff(definitions[defKey], nestedDefinitions[defKey], true)); + console.log("Keys:", Object.keys(definitions[defKey]), Object.keys(nestedDefinitions[defKey])); + isNewLine = true; + } + } else { + // console.log("Definition", defKey, "is identical to existing definition, skipping."); + } + } else { + definitions[defKey] = nestedDefinitions[defKey]; + if (isNewLine) { + process.stdout.write("Adding nested definitions to main definitions: "); + isNewLine = false; + } else process.stdout.write("\x1b[4D, "); + process.stdout.write(yellowBright(defKey) + "... "); + } + } - deleteOneOfKindUndefinedRecursive(definitions, "$"); - for (const defKey in definitions) { - filterSchema(definitions[defKey]); - } + deleteOneOfKindUndefinedRecursive(definitions, "$"); + for (const defKey in definitions) { + filterSchema(definitions[defKey]); + } - if (process.env.WRITE_SCHEMA_DIR === "true") { - await Promise.all(writePromises); - await Promise.all( - Object.keys(definitions).map(async (name) => { - await fsp.writeFile(path.join("schemas_final", `${name}.json`), JSON.stringify(definitions[name], null, 4)); - // console.log("Wrote schema", name, "to schemas/"); - }), - ); - await Promise.all( - Object.keys(nestedDefinitions).map(async (name) => { - await fsp.writeFile(path.join("schemas_nested", `${name}.json`), JSON.stringify(nestedDefinitions[name], null, 4)); - // console.log("Wrote schema", name, "to schemas_nested/"); - }), - ); - } + if (process.env.WRITE_SCHEMA_DIR === "true") { + await Promise.all(writePromises); + await Promise.all( + Object.keys(definitions).map(async (name) => { + await fsp.writeFile(path.join("schemas_final", `${name}.json`), JSON.stringify(definitions[name], null, 4)); + // console.log("Wrote schema", name, "to schemas/"); + }), + ); + await Promise.all( + Object.keys(nestedDefinitions).map(async (name) => { + await fsp.writeFile(path.join("schemas_nested", `${name}.json`), JSON.stringify(nestedDefinitions[name], null, 4)); + // console.log("Wrote schema", name, "to schemas_nested/"); + }), + ); + } - fs.writeFileSync(schemaPath, JSON.stringify(definitions, null, 4)); - fs.writeFileSync(__dirname + "/schemaExclusions.json", JSON.stringify(exclusionList, null, 4)); - const elapsedMs = Number(totalSw.elapsed().totalMilliseconds + "." + totalSw.elapsed().microseconds); - console.log("\nSuccessfully wrote", Object.keys(definitions).length, "schemas to", schemaPath, "in", elapsedMs, "ms,", fs.statSync(schemaPath).size, "bytes."); + fs.writeFileSync(schemaPath, JSON.stringify(definitions, null, 4)); + fs.writeFileSync(__dirname + "/schemaExclusions.json", JSON.stringify(exclusionList, null, 4)); + const elapsedMs = Number(totalSw.elapsed().totalMilliseconds + "." + totalSw.elapsed().microseconds); + console.log("\nSuccessfully wrote", Object.keys(definitions).length, "schemas to", schemaPath, "in", elapsedMs, "ms,", fs.statSync(schemaPath).size, "bytes."); } function deleteOneOfKindUndefinedRecursive(obj, path) { - if (obj?.type === "object" && obj?.properties?.oneofKind?.type === "undefined") return true; + if (obj?.type === "object" && obj?.properties?.oneofKind?.type === "undefined") return true; - for (const key in obj) { - if (typeof obj[key] === "object" && deleteOneOfKindUndefinedRecursive(obj[key], path + "." + key)) { - console.log("Deleting", path, key); - delete obj[key]; - } - } + for (const key in obj) { + if (typeof obj[key] === "object" && deleteOneOfKindUndefinedRecursive(obj[key], path + "." + key)) { + console.log("Deleting", path, key); + delete obj[key]; + } + } - return false; + return false; } function filterSchema(schema) { - // this is a hack. we may want to check if its a @column instead - if (schema.properties) { - for (let key in schema.properties) { - if (baseClassProperties.includes(key)) { - delete schema.properties[key]; - } - } - } + // this is a hack. we may want to check if its a @column instead + if (schema.properties) { + for (let key in schema.properties) { + if (baseClassProperties.includes(key)) { + delete schema.properties[key]; + } + } + } - if (schema.required) schema.required = schema.required.filter((x) => !baseClassProperties.includes(x)); + if (schema.required) schema.required = schema.required.filter((x) => !baseClassProperties.includes(x)); - // recurse into own definitions - if (schema.definitions) { - console.log(redBright("WARNING"), "Schema has own definitions, recursing into them to filter base class properties:", Object.keys(schema.definitions)); - for (const defKey in schema.definitions) { - filterSchema(schema.definitions[defKey]); - } - } + // recurse into own definitions + if (schema.definitions) { + console.log(redBright("WARNING"), "Schema has own definitions, recursing into them to filter base class properties:", Object.keys(schema.definitions)); + for (const defKey in schema.definitions) { + filterSchema(schema.definitions[defKey]); + } + } } function deepEqual(a, b) { - if (a === b) return true; + if (a === b) return true; - if (typeof a !== "object" || typeof b !== "object" || a == null || b == null) { - return false; - } + if (typeof a !== "object" || typeof b !== "object" || a == null || b == null) { + return false; + } - const keysA = Object.keys(a); - const keysB = Object.keys(b); + const keysA = Object.keys(a); + const keysB = Object.keys(b); - if (keysA.length !== keysB.length) return false; + if (keysA.length !== keysB.length) return false; - for (const key of keysA) { - if (!keysB.includes(key) || (typeof a[key] === typeof b[key] && !deepEqual(a[key], b[key]))) { - return false; - } - } + for (const key of keysA) { + if (!keysB.includes(key) || (typeof a[key] === typeof b[key] && !deepEqual(a[key], b[key]))) { + return false; + } + } - return true; + return true; } function columnizedObjectDiff(a, b, trackEqual = false) { - const diffs = { left: {}, right: {}, ...(trackEqual ? { equal: {} } : {}) }; - const keys = new Set([...Object.keys(a), ...Object.keys(b)]); - for (const key of keys) { - if (!deepEqual(a[key], b[key])) { - diffs.left[key] = a[key]; - diffs.right[key] = b[key]; - } else if (trackEqual) diffs.equal[key] = a[key]; - } - return diffs; + const diffs = { left: {}, right: {}, ...(trackEqual ? { equal: {} } : {}) }; + const keys = new Set([...Object.keys(a), ...Object.keys(b)]); + for (const key of keys) { + if (!deepEqual(a[key], b[key])) { + diffs.left[key] = a[key]; + diffs.right[key] = b[key]; + } else if (trackEqual) diffs.equal[key] = a[key]; + } + return diffs; } main();