322 lines
8.5 KiB
JavaScript
Executable File
322 lines
8.5 KiB
JavaScript
Executable File
#!/usr/bin/env -S node -r esbin
|
|
import "dotenv/config"
|
|
import { promises as fs } from "fs"
|
|
import { resolve } from "path"
|
|
import { assign, camelCase } from "lodash"
|
|
import yaml from "js-yaml"
|
|
import { writeFormatted } from "./utils"
|
|
|
|
// type Outputs = "partial input" | "input" | "object" | "inputAndObject" | "args" | "zod";
|
|
type Output =
|
|
| ["input", { partial?: boolean; name?: string }] //
|
|
| ["object"]
|
|
| ["args"]
|
|
| ["zod"]
|
|
|
|
interface Model {
|
|
as: Output[]
|
|
extends?: string
|
|
fields: string
|
|
extra?: Record<string, string>
|
|
}
|
|
|
|
const FIELD_REGEX =
|
|
/^(?<name>\w+)(?<optional>\?)?(:\s*(?<type>[\w\s[\]|]+))?(\s*=\s*(?<defaultValue>.+))?$/
|
|
|
|
function parseField(field: string) {
|
|
const groups = field.match(FIELD_REGEX)?.groups
|
|
if (!groups) {
|
|
throw new Error(`Invalid field: ${field}`)
|
|
}
|
|
|
|
if (!groups.type && groups.defaultValue) {
|
|
const v = groups.defaultValue
|
|
if (v === "true" || v === "false") {
|
|
groups.type = "boolean"
|
|
}
|
|
}
|
|
|
|
return groups as {
|
|
name: string
|
|
optional?: "?"
|
|
type: string
|
|
defaultValue: string
|
|
}
|
|
}
|
|
|
|
function parseFields(value: any) {
|
|
if (typeof value === "object") {
|
|
value = Object.entries(value)
|
|
.map(([name, type]) => `${name}: ${type}`)
|
|
.join("\n")
|
|
}
|
|
|
|
return (value as string).trim().split("\n").map(parseField)
|
|
}
|
|
|
|
const getInputName = (name: string) => (name.startsWith("Input") ? name : `Input${name}`)
|
|
|
|
type Generate = Generator<string | false | undefined, void, unknown>
|
|
|
|
async function main() {
|
|
const folder = resolve(__dirname, "../src/shared/models")
|
|
const source = await fs.readFile(resolve(folder, "models.yml"), "utf8")
|
|
const json = yaml.load(source) as {
|
|
enums: Record<string, string[]>
|
|
imports?: string
|
|
models: Record<string, Model>
|
|
}
|
|
|
|
const models = Object.entries(json.models).map(
|
|
([name, { as, extends: _, fields, extra }]) => ({
|
|
name,
|
|
as: as.map(t =>
|
|
Array.isArray(t) ? [t[0], (assign as any)(...t.slice(1))] : [t, {}],
|
|
) as Output[],
|
|
extends: _,
|
|
fields: parseFields(fields),
|
|
extra,
|
|
}),
|
|
)
|
|
|
|
for (const model of models) {
|
|
if (model.extends) {
|
|
const parent = models.find(m => m.name === model.extends)
|
|
if (!parent) {
|
|
throw new Error(`Could not find parent ${model.extends}`)
|
|
}
|
|
model.fields = parent.fields.concat(model.fields)
|
|
}
|
|
}
|
|
|
|
const importedEnums = new Set<string>()
|
|
const enumNames = Object.keys(json.enums)
|
|
|
|
function getReifiedType(type: string): string {
|
|
if (type.endsWith("| null")) {
|
|
type = type.slice(0, -6).trim()
|
|
}
|
|
if (type.endsWith("[]")) {
|
|
return `[${getReifiedType(type.slice(0, -2))}]`
|
|
}
|
|
switch (type) {
|
|
case "string":
|
|
case "String":
|
|
return "String"
|
|
case "int":
|
|
case "Int":
|
|
return "Int"
|
|
case "boolean":
|
|
case "Boolean":
|
|
return "Boolean"
|
|
default:
|
|
if (enumNames.includes(type)) {
|
|
importedEnums.add(type)
|
|
}
|
|
if (/^\d+$/.test(type)) {
|
|
return "Int"
|
|
}
|
|
return type
|
|
}
|
|
}
|
|
|
|
function getVirtualType(type: string): string {
|
|
if (type.endsWith("[]")) {
|
|
return `${getVirtualType(type.slice(0, -2))}[]`
|
|
} else if (type.endsWith("| null")) {
|
|
return `${getVirtualType(type.slice(0, -6).trim())} | null`
|
|
}
|
|
switch (type) {
|
|
case "int":
|
|
case "Int":
|
|
return "number"
|
|
case "ID":
|
|
case "String":
|
|
return "string"
|
|
case "Boolean":
|
|
return "boolean"
|
|
default:
|
|
if (enumNames.includes(type)) {
|
|
importedEnums.add(type)
|
|
}
|
|
return type
|
|
}
|
|
}
|
|
|
|
function getZodType(type: string): string {
|
|
if (type.endsWith("[]")) {
|
|
return `z.array(${getZodType(type.slice(0, -2))})`
|
|
} else if (type.endsWith("| null")) {
|
|
return `${getZodType(type.slice(0, -6).trim())}.optional()`
|
|
}
|
|
switch (type) {
|
|
case "int":
|
|
case "Int":
|
|
return "z.number()"
|
|
case "string":
|
|
return "z.string()"
|
|
case "boolean":
|
|
return "z.boolean()"
|
|
default:
|
|
if (/^[\d.]+$/.test(type)) {
|
|
return `z.literal(${type})`
|
|
}
|
|
|
|
if (enumNames.includes(type)) {
|
|
return `z.nativeEnum(${type})`
|
|
}
|
|
return `z.instanceof(${type})`
|
|
}
|
|
}
|
|
|
|
const banners: string[] = [
|
|
`import { ArgsType, Field, ID, InputType, Int, ObjectType } from "@aet/gql-tools/macro";`,
|
|
// `import { z } from "zod";`,
|
|
]
|
|
|
|
/* eslint-disable @typescript-eslint/consistent-type-imports, @typescript-eslint/no-unused-vars, @typescript-eslint/no-inferrable-types */
|
|
const getModels = function* (): Generate {
|
|
for (const model of models) {
|
|
let attachInputToObject = false
|
|
for (const type of model.as) {
|
|
let { name } = model
|
|
let isInput = false
|
|
let isPartialInput = false
|
|
|
|
switch (type[0]) {
|
|
case "input":
|
|
isInput = true
|
|
isPartialInput = type[1].partial ?? false
|
|
|
|
if (
|
|
Object.keys(type[1]).length === 0 &&
|
|
model.as.some(t => t[0] === "object")
|
|
) {
|
|
attachInputToObject = true
|
|
continue
|
|
} else {
|
|
name = type[1].name ?? getInputName(name)
|
|
yield `@InputType("${name}")\n`
|
|
}
|
|
break
|
|
|
|
case "object":
|
|
if (attachInputToObject) {
|
|
yield `/** Input and object type for ${name} */\n`
|
|
yield `@InputType("${getInputName(name)}")\n`
|
|
}
|
|
yield `@ObjectType("${name}")\n`
|
|
break
|
|
|
|
case "args":
|
|
yield `@ArgsType()\n`
|
|
break
|
|
|
|
case "zod":
|
|
continue
|
|
}
|
|
|
|
yield `export class ${name} {`
|
|
for (const field of model.fields) {
|
|
const optional = field.optional || isPartialInput
|
|
yield "@Field("
|
|
|
|
yield `() => ${getReifiedType(field.type)},`
|
|
if (optional) {
|
|
yield "{ nullable: true },"
|
|
}
|
|
yield ")\n"
|
|
if (!field.defaultValue) {
|
|
yield "declare "
|
|
}
|
|
yield `${field.name}`
|
|
if (optional) {
|
|
yield "?"
|
|
}
|
|
yield ":"
|
|
yield getVirtualType(field.type)
|
|
if (isInput && field.defaultValue) {
|
|
yield ` = ${field.defaultValue}`
|
|
}
|
|
yield ";\n\n"
|
|
}
|
|
|
|
let constructorType = name
|
|
if (model.extra) {
|
|
const omits: string[] = []
|
|
for (const [name, type] of Object.entries(model.extra)) {
|
|
if (type.includes("=")) {
|
|
omits.push(name.replace(/\?$/, ""))
|
|
}
|
|
yield `${name}: ${type}\n`
|
|
}
|
|
constructorType = `Omit<${name}, ${omits.map(o => `"${o}"`).join(" | ")}>`
|
|
}
|
|
|
|
yield /* js */ `
|
|
/**
|
|
* Create an instance of ${name} from a plain object.
|
|
*/
|
|
constructor(fields: ${constructorType}) {
|
|
Object.assign(this, fields);
|
|
}
|
|
`
|
|
|
|
// if (1 === 3) {
|
|
// yield "\n";
|
|
// yield `/** Zod schema for ${name} */\n`;
|
|
// yield "static zod = z.object({\n";
|
|
// for (const field of model.fields) {
|
|
// yield `${field.name}: ${getZodType(field.type)},\n`;
|
|
// }
|
|
// yield `}).strict();`;
|
|
// }
|
|
|
|
yield "}\n\n"
|
|
}
|
|
}
|
|
}
|
|
|
|
function* getEnums() {
|
|
for (const [name, values] of Object.entries(json.enums)) {
|
|
yield `export const ${name} = {`
|
|
for (const value of values) {
|
|
yield `${value}: "${value}",\n`
|
|
}
|
|
yield "} as const;\n"
|
|
yield
|
|
yield `export type ${name} = typeof ${name}[keyof typeof ${name}];\n`
|
|
yield
|
|
yield `export const ${camelCase(name)}s = [${values
|
|
.map(v => `"${v}"`)
|
|
.join(", ")}] as ${name}[];\n`
|
|
yield
|
|
}
|
|
}
|
|
|
|
await writeTS(resolve(folder, "models.generated.ts"), getModels, () =>
|
|
banners.concat(
|
|
importedEnums.size
|
|
? `import { ${Array.from(importedEnums)
|
|
.sort()
|
|
.join(", ")} } from "./enums.generated";`
|
|
: [],
|
|
),
|
|
)
|
|
await writeTS(resolve(folder, "enums.generated.ts"), getEnums, () => [])
|
|
}
|
|
|
|
async function writeTS(path: string, generator: () => Generate, banner: () => string[]) {
|
|
const code = Array.from(generator(), x => x ?? "\n").join("")
|
|
await writeFormatted(path, banner().join("\n") + "\n\n" + code)
|
|
}
|
|
|
|
if (require.main === module) {
|
|
Promise.resolve()
|
|
.then(() => main())
|
|
.catch(e => {
|
|
console.error(e)
|
|
process.exit(1)
|
|
})
|
|
}
|