UNPKG

@lordfokas/magic-orm

Version:

A class-based ORM in TypeScript. Unorthodox and extremely opinionated, made to fit my specific use cases.

266 lines (265 loc) 12.3 kB
import path from "node:path"; import fs from "node:fs"; import { Logger } from "@lordfokas/loggamus"; const uuidmap = { small: 16, standard: 40, long: 58, huge: 77 }; export class Scaffolder { migration; pool; gens; keys_r = {}; static readJSON(file) { return JSON.parse(fs.readFileSync(file).toString()); } static async start(dir, file, pool, gens) { await new Scaffolder(dir, file, pool, gens).execute(); } constructor(dir, file, pool, gens) { this.migration = Scaffolder.readJSON(path.join(dir, file)); this.pool = pool; this.gens = gens; } async execute() { this.generateUUIDTypeDefs(); this.processRelationships(); this.generateEntityCode(); if (this.gens.includes("TS")) { this.writeModelDefinitionsFile(); this.writeModelConfigurationsFile(); this.writeModelClasses(); } if (this.gens.includes("SQL")) { await this.applyDatabaseChanges(); } Logger.info("Done."); } generateUUIDTypeDefs() { const types = this.migration.types; const models = this.migration.models; for (const model in models) { const spec = models[model]; types[`UUID<${spec.entity.prefix}>`] = { ts: `UUID<K_${model}>`, sql: `VARCHAR(${uuidmap[spec.entity.uuid]})` }; } } processRelationships() { Logger.info("Processing relationships"); const models = this.migration.models; for (const model in models) { Logger.debug("- " + model); if (!this.keys_r[model]) this.keys_r[model] = []; const spec = models[model]; const superclass = spec.entity.extends; if (superclass) { const superspec = models[superclass]; if (!superspec.entity._subclasses) superspec.entity._subclasses = []; superspec.entity._subclasses.push(model); } spec.keys.forEach(v => { if (!this.keys_r[v.entity]) this.keys_r[v.entity] = []; this.keys_r[v.entity].push({ entity: model, key: v.key, exp: v.exp }); }); } } generateEntityCode() { Logger.info("Generating entity code"); const schema = this.migration.schema ?? "public"; const types = this.migration.types; const models = this.migration.models; for (const model in models) { Logger.debug("- " + model); const spec = models[model]; if (this.gens.includes("TS")) { spec.generatedTS = { K: `export type K_${model} = ${spec.entity._subclasses ? spec.entity._subclasses.map(k => `K_${k}`).join(' | ') : `"${spec.entity.prefix}"`}`, T: [ `export interface T_${model}${spec.entity.extends ? ` extends T_${spec.entity.extends}` : ''} {`, ` uuid: UUID<K_${model}>`, ...spec.keys.map(v => ` uuid_${v.key}: UUID<K_${v.entity}>`), ...Object.entries(spec.fields).map(([k, v]) => ` ${k}: ${types[v].ts}`), ...spec.keys.map(v => ` ${v.key}?: T_${v.entity}`), ...this.keys_r[model].filter(r => r.exp && r.exp.length > 0).map(v => ` ${v.exp}?: T_${v.entity}[]`), `}` ].join('\n') }; } if (this.gens.includes("SQL")) { spec.generatedSQL = { T: [ `CREATE TABLE IF NOT EXISTS ${schema}.${spec.entity.table} (`, ` uuid ${types[`UUID<${spec.entity.prefix}>`].sql} PRIMARY KEY,`, ...spec.keys.map(r => ` uuid_${r.key} ${types[`UUID<${models[r.entity].entity.prefix}>`].sql},`), ...Object.entries(spec.fields).map(([k, v]) => ` ${k} ${types[v].sql},`) ].join('\n').replace(/,$/, '\n);'), C: spec.keys.map(r => [ `ALTER TABLE ${schema}.${spec.entity.table}`, `DROP CONSTRAINT IF EXISTS fk_${model}_${r.key};`, `ALTER TABLE ${schema}.${spec.entity.table}`, `ADD CONSTRAINT fk_${model}_${r.key}`, `FOREIGN KEY (uuid_${r.key})`, `REFERENCES ${models[r.entity].entity.table}(uuid);` ].join(' ')).concat(spec.entity.extends ? [[ `ALTER TABLE ${schema}.${spec.entity.table}`, `DROP CONSTRAINT IF EXISTS fk_${model}_extends_${spec.entity.extends};`, `ALTER TABLE ${schema}.${spec.entity.table}`, `ADD CONSTRAINT fk_${model}_extends_${spec.entity.extends}`, `FOREIGN KEY (uuid)`, `REFERENCES ${models[spec.entity.extends].entity.table}(uuid);` ].join('\n')] : []).join('\n').trim() }; } } } writeModelDefinitionsFile() { if (!fs.existsSync(this.migration.files.definitions)) { Logger.warn(this.migration.files.definitions + " does not exist, creating."); fs.mkdirSync(this.migration.files.definitions, { recursive: true }); } const definitionsFile = path.join(this.migration.files.definitions, 'Models.d.ts'); Logger.info("Writing " + definitionsFile); fs.writeFileSync(definitionsFile, [ "// Auto-generated file by magic-orm bin tools", "// Do not overwrite or modify manually\n", "import { UUID, Linkage } from '@lordfokas/magic-orm';", ...this.migration.files.extraImports, "\n\n", ...Object.entries(this.migration.models).map(([model, spec]) => [ "// " + model, spec.generatedTS.K, spec.generatedTS.T, "\n" ].join('\n')) ].join('\n')); const manifestFile = path.join(this.migration.files.definitions, 'models.json'); Logger.info("Writing " + manifestFile); fs.writeFileSync(manifestFile, JSON.stringify(Object.keys(this.migration.models), null, 4)); } getBooleans(spec) { const booleans = Object.entries(spec.fields).filter(([_, v]) => v == "boolean"); if (!spec.entity.extends) return booleans; const parent = this.getBooleans(this.migration.models[spec.entity.extends]); return [...booleans, ...parent]; } writeModelConfigurationsFile() { const configFile = path.join(this.migration.files.definitions, 'ModelConfigs.ts'); Logger.info("Writing " + configFile); const lines = [ "// Auto-generated file by magic-orm bin tools", "// Do not overwrite or modify manually", "", 'import { type EntityConfig } from "@lordfokas/magic-orm";' ]; for (const model in this.migration.models) { const spec = this.migration.models[model]; const booleans = this.getBooleans(spec); const fields = [ `'uuid'`, ...spec.keys.map(v => `'uuid_${v.key}'`), ...Object.keys(spec.fields).map(k => `'${k}'`) ].join(', '); const chain = spec.entity.extends ? [" chain: { '*': '*' },"] : []; const inherits = spec.entity.extends ? [ ` inherits: {`, ` parentClass: "${spec.entity.extends}",`, ` parentField: "uuid",`, ` childClass: "${model}",`, ` childField: "uuid",`, ` },` ] : []; const parents = spec.keys.map(r => [ ` ${r.key}: {`, ` parentClass: "${r.entity}",`, ` parentField: "uuid",`, ` childClass: "${model}",`, ` childField: "uuid_${r.key}",`, ` parentName: "${r.key}",`, ` childrenName: "${r.exp}"`, ` }` ].join('\n')) .join(',\n'); const children = this.keys_r[model] .filter(r => r.exp && r.exp.length > 0) .map(r => [ ` ${r.exp}: {`, ` parentClass: "${model}",`, ` parentField: "uuid",`, ` childClass: "${r.entity}",`, ` childField: "uuid_${r.key}",`, ` parentName: "${r.key}",`, ` childrenName: "${r.exp}"`, ` }` ].join('\n')) .join(',\n'); lines.push("", `export const $config${model} = {`, ` prefix: '${spec.entity.prefix}',`, ` table: '${spec.entity.table}',`, ` uuidsize: '${spec.entity.uuid}',`, ` fields: {`, ` '*': [ ${fields} ],`, ` 'uuid': [ 'uuid' ]`, ` },`, ...chain, ` booleans: [${booleans.map(([k, _]) => `'${k}'`).join(', ')}]${booleans.length ? '' : ' as string[]'},`, ...inherits, ` parents: {`, parents, ` },`, ` children: {`, children, ` }`, `} satisfies EntityConfig;`); } fs.writeFileSync(configFile, lines.join('\n')); } writeModelClasses() { if (!fs.existsSync(this.migration.files.models)) { Logger.warn(this.migration.files.models + " does not exist, creating."); fs.mkdirSync(this.migration.files.models, { recursive: true }); } Logger.info("Writing model classes..."); const modelsImport = path.join(this.migration.files.pathToDefinitions, "Models.js"); const configImport = path.join(this.migration.files.pathToDefinitions, "ModelConfigs.js"); for (const model in this.migration.models) { const className = this.migration.files.modelPrefix + model; const file = path.join(this.migration.files.models, className + ".ts"); if (fs.existsSync(file)) { Logger.warn(file + " already exists, skipping."); continue; } Logger.debug("Writing " + file); fs.writeFileSync(file, [ `import { Entity, UUID} from "@lordfokas/magic-orm";`, `import { K_${model}, T_${model} } from "${modelsImport}";`, `import { $config${model} } from "${configImport}";`, ``, `export interface ${className} extends T_${model} {}`, `export class ${className} extends Entity {`, ` static readonly $config = $config${model};`, ``, ` constructor(obj: Partial<T_${model}>){`, ` super(obj);`, ` }`, ``, ` declare uuid : UUID<K_${model}>;`, `}`, ].join('\n')); } } async applyDatabaseChanges() { Logger.info("Begin writing database tables"); for (const model in this.migration.models) { Logger.debug(model + " ..."); const spec = this.migration.models[model]; Logger.fine(spec.generatedSQL.T); await this.pool?.query(spec.generatedSQL.T); } Logger.info("Begin writing database constraints"); for (const model in this.migration.models) { const spec = this.migration.models[model]; if (!spec.generatedSQL.C) { Logger.debug("No constraints for " + model + ", skipping..."); continue; } Logger.debug(model + " ..."); Logger.fine(spec.generatedSQL.C); await this.pool?.query(spec.generatedSQL.C); } } }