arkormx 0.1.8 → 0.1.10

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/dist/cli.mjs CHANGED
@@ -1,33 +1,1597 @@
1
1
  #!/usr/bin/env node
2
- import{copyFileSync as e,existsSync as t,mkdirSync as n,readFileSync as r,readdirSync as i,rmSync as a,writeFileSync as o}from"fs";import s,{dirname as c,extname as l,join as u,relative as d}from"path";import{createRequire as f}from"module";import{existsSync as p,mkdirSync as m,readFileSync as h,readdirSync as g,writeFileSync as _}from"node:fs";import{join as v,resolve as y}from"node:path";import{spawnSync as ee}from"node:child_process";import{str as b}from"@h3ravel/support";import{fileURLToPath as te,pathToFileURL as ne}from"url";import{Logger as x}from"@h3ravel/shared";import{Command as S,Kernel as C}from"@h3ravel/musket";import{pathToFileURL as w}from"node:url";var T=class extends Error{constructor(e){super(e),this.name=`ArkormException`}},E=class{},D=class{columns=[];dropColumnNames=[];indexes=[];latestColumnName;primary(e,t){let n=typeof e==`string`?{columnName:e,...t??{}}:e??{},r=this.resolveColumn(n.columnName);return r.primary=!0,typeof n.autoIncrement==`boolean`&&(r.autoIncrement=n.autoIncrement),Object.prototype.hasOwnProperty.call(n,`default`)&&(r.default=n.default),this}id(e=`id`,t=`id`){return this.column(e,t,{primary:!0})}uuid(e,t={}){return this.column(e,`uuid`,t)}string(e,t={}){return this.column(e,`string`,t)}text(e,t={}){return this.column(e,`text`,t)}integer(e,t={}){return this.column(e,`integer`,t)}bigInteger(e,t={}){return this.column(e,`bigInteger`,t)}float(e,t={}){return this.column(e,`float`,t)}boolean(e,t={}){return this.column(e,`boolean`,t)}json(e,t={}){return this.column(e,`json`,t)}date(e,t={}){return this.column(e,`date`,t)}morphs(e,t=!1){return this.string(`${e}Type`,{nullable:t}),this.integer(`${e}Id`,{nullable:t}),this}nullableMorphs(e){return this.morphs(e,!0)}timestamp(e,t={}){return this.column(e,`timestamp`,t)}timestamps(){return this.timestamp(`createdAt`,{nullable:!1}),this.timestamp(`updatedAt`,{nullable:!1}),this}softDeletes(e=`deletedAt`){return this.timestamp(e,{nullable:!0}),this}dropColumn(e){return this.dropColumnNames.push(e),this}nullable(e){let t=this.resolveColumn(e);return t.nullable=!0,this}after(e,t){let n=this.resolveColumn(t);return n.after=e,this}map(e,t){let n=this.resolveColumn(t);return n.map=e,this}index(e,t){let n=Array.isArray(e)?e:typeof e==`string`?[e]:[this.resolveColumn().name];return this.indexes.push({columns:[...n],name:t}),this}getColumns(){return this.columns.map(e=>({...e}))}getDropColumns(){return[...this.dropColumnNames]}getIndexes(){return this.indexes.map(e=>({...e,columns:[...e.columns]}))}column(e,t,n){return this.columns.push({name:e,type:t,map:n.map,nullable:n.nullable,unique:n.unique,primary:n.primary,autoIncrement:n.autoIncrement,after:n.after,default:n.default}),this.latestColumnName=e,this}resolveColumn(e){let t=e??this.latestColumnName;if(!t)throw Error(`No column available for this operation.`);let n=this.columns.find(e=>e.name===t);if(!n)throw Error(`Column [${t}] was not found in the table definition.`);return n}},re=class{operations=[];createTable(e,t){let n=new D;return t(n),this.operations.push({type:`createTable`,table:e,columns:n.getColumns(),indexes:n.getIndexes()}),this}alterTable(e,t){let n=new D;return t(n),this.operations.push({type:`alterTable`,table:e,addColumns:n.getColumns(),dropColumns:n.getDropColumns(),addIndexes:n.getIndexes()}),this}dropTable(e){return this.operations.push({type:`dropTable`,table:e}),this}getOperations(){return this.operations.map(e=>e.type===`createTable`?{...e,columns:e.columns.map(e=>({...e})),indexes:e.indexes.map(e=>({...e,columns:[...e.columns]}))}:e.type===`alterTable`?{...e,addColumns:e.addColumns.map(e=>({...e})),dropColumns:[...e.dropColumns],addIndexes:e.addIndexes.map(e=>({...e,columns:[...e.columns]}))}:{...e})}};const ie=/model\s+(\w+)\s*\{[\s\S]*?\n\}/g,O=e=>{let t=e.replace(/[^a-zA-Z0-9]+/g,` `).trim(),n=(t.endsWith(`s`)&&t.length>1?t.slice(0,-1):t).split(/\s+/g).filter(Boolean);return n.length===0?`GeneratedModel`:n.map(e=>`${e.charAt(0).toUpperCase()}${e.slice(1)}`).join(``)},k=e=>e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`),ae=e=>e.type===`id`?`Int`:e.type===`uuid`||e.type===`string`||e.type===`text`?`String`:e.type===`integer`?`Int`:e.type===`bigInteger`?`BigInt`:e.type===`float`?`Float`:e.type===`boolean`?`Boolean`:e.type===`json`?`Json`:`DateTime`,A=e=>{if(e!=null){if(typeof e==`string`)return`@default("${e.replace(/"/g,`\\"`)}")`;if(typeof e==`number`||typeof e==`bigint`)return`@default(${e})`;if(typeof e==`boolean`)return`@default(${e?`true`:`false`})`}},j=e=>{if(e.type===`id`){let t=e.primary===!1?``:` @id`,n=typeof e.map==`string`&&e.map.trim().length>0?` @map("${e.map.replace(/"/g,`\\"`)}")`:``,r=A(e.default),i=e.autoIncrement??e.primary!==!1,a=r?` ${r}`:i&&t?` @default(autoincrement())`:``;return` ${e.name} Int${t}${a}${n}`}let t=ae(e),n=e.nullable?`?`:``,r=e.unique?` @unique`:``,i=e.primary?` @id`:``,a=typeof e.map==`string`&&e.map.trim().length>0?` @map("${e.map.replace(/"/g,`\\"`)}")`:``,o=A(e.default)??(e.type===`uuid`&&e.primary?`@default(uuid())`:void 0),s=o?` ${o}`:``;return` ${e.name} ${t}${n}${i}${r}${s}${a}`},M=e=>` @@index([${e.columns.join(`, `)}]${typeof e.name==`string`&&e.name.trim().length>0?`, name: "${e.name.replace(/"/g,`\\"`)}"`:``})`,N=e=>{let t=O(e.table),n=e.table!==t.toLowerCase(),r=e.columns.map(j),i=[...(e.indexes??[]).map(M),...n?[` @@map("${b(e.table).snake()}")`]:[]];return`model ${t} {\n${(i.length>0?[...r,``,...i]:r).join(`
3
- `)}\n}`},P=(e,t)=>{let n=[...e.matchAll(ie)],r=RegExp(`@@map\\("${k(t)}"\\)`);for(let e of n){let n=e[0],i=e[1],a=e.index??0,o=a+n.length;if(r.test(n)||i.toLowerCase()===t.toLowerCase()||i.toLowerCase()===O(t).toLowerCase())return{modelName:i,block:n,start:a,end:o}}return null},F=(e,t)=>{if(P(e,t.table))throw new T(`Prisma model for table [${t.table}] already exists.`);let n=N(t);return`${e.trimEnd()}\n\n${n}\n`},oe=(e,t)=>{let n=P(e,t.table);if(!n)throw new T(`Prisma model for table [${t.table}] was not found.`);let r=n.block,i=r.split(`
4
- `);return t.dropColumns.forEach(e=>{let t=RegExp(`^\\s*${k(e)}\\s+`);for(let e=0;e<i.length;e+=1)if(t.test(i[e])){i.splice(e,1);return}}),t.addColumns.forEach(e=>{let t=j(e),n=RegExp(`^\\s*${k(e.name)}\\s+`);if(i.some(e=>n.test(e)))return;let r=Math.max(1,i.length-1),a=typeof e.after==`string`&&e.after.length>0?i.findIndex(t=>RegExp(`^\\s*${k(e.after)}\\s+`).test(t)):-1,o=a>0?Math.min(a+1,r):r;i.splice(o,0,t)}),(t.addIndexes??[]).forEach(e=>{let t=M(e);if(i.some(e=>e.trim()===t.trim()))return;let n=Math.max(1,i.length-1);i.splice(n,0,t)}),r=i.join(`
5
- `),`${e.slice(0,n.start)}${r}${e.slice(n.end)}`},se=(e,t)=>{let n=P(e,t.table);if(!n)return e;let r=e.slice(0,n.start).trimEnd(),i=e.slice(n.end).trimStart();return`${r}${r&&i?`
6
-
7
- `:``}${i}`},ce=(e,t)=>t.reduce((e,t)=>t.type===`createTable`?F(e,t):t.type===`alterTable`?oe(e,t):se(e,t),e),I=(e,t)=>{let n=ee(`npx`,[`prisma`,...e],{cwd:t,encoding:`utf-8`});if(n.status===0)return;let r=[n.stdout,n.stderr].filter(Boolean).join(`
8
- `).trim();throw new T(r?`Prisma command failed: prisma ${e.join(` `)}\n${r}`:`Prisma command failed: prisma ${e.join(` `)}`)},L=e=>{let t=e.replace(/[^a-zA-Z0-9]+/g,` `).trim();return t?`${t.split(/\s+/g).map(e=>`${e.charAt(0).toUpperCase()}${e.slice(1)}`).join(``)}Migration`:`GeneratedMigration`},R=e=>String(e).padStart(2,`0`),z=(e=new Date)=>`${e.getFullYear()}${R(e.getMonth()+1)}${R(e.getDate())}${R(e.getHours())}${R(e.getMinutes())}${R(e.getSeconds())}`,B=e=>e.trim().toLowerCase().replace(/[^a-z0-9]+/g,`_`).replace(/^_+|_+$/g,``)||`migration`,V=(e,t=`ts`)=>t===`js`?[`import { Migration } from 'arkormx'`,``,`export default class ${e} extends Migration {`,` /**`,` * @param {import('arkormx').SchemaBuilder} schema`,` * @returns {Promise<void>}`,` */`,` async up (schema) {`,` }`,``,` /**`,` * @param {import('arkormx').SchemaBuilder} schema`,` * @returns {Promise<void>}`,` */`,` async down (schema) {`,` }`,`}`,``].join(`
9
- `):[`import { Migration, SchemaBuilder } from 'arkormx'`,``,`export default class ${e} extends Migration {`,` public async up (schema: SchemaBuilder): Promise<void> {`,` }`,``,` public async down (schema: SchemaBuilder): Promise<void> {`,` }`,`}`,``].join(`
10
- `),H=(e,t={})=>{let n=z(new Date),r=B(e),i=L(e),a=t.extension??`ts`,o=t.directory??v(process.cwd(),`database`,`migrations`),s=`${n}_${r}.${a}`,c=v(o,s),l=V(i,a);if(t.write??!0){if(p(o)||m(o,{recursive:!0}),p(c))throw new T(`Migration file already exists: ${c}`);_(c,l)}return{fileName:s,filePath:c,className:i,content:l}},U=async(e,t=`up`)=>{let n=e instanceof E?e:new e,r=new re;return t===`up`?await n.up(r):await n.down(r),r.getOperations()},W=async(e,t={})=>{let n=t.schemaPath??v(process.cwd(),`prisma`,`schema.prisma`);if(!p(n))throw new T(`Prisma schema file not found: ${n}`);let r=h(n,`utf-8`),i=await U(e,`up`),a=ce(r,i);return(t.write??!0)&&_(n,a),{schema:a,schemaPath:n,operations:i}},G=()=>{let e=s.dirname(te(import.meta.url));for(;;){let n=s.join(e,`package.json`),r=s.join(e,`stubs`);if(t(n)&&t(r))return r;let i=s.dirname(e);if(i===e)break;e=i}return s.join(process.cwd(),`stubs`)},K={paths:{stubs:G(),seeders:s.join(process.cwd(),`database`,`seeders`),models:s.join(process.cwd(),`src`,`models`),migrations:s.join(process.cwd(),`database`,`migrations`),factories:s.join(process.cwd(),`database`,`factories`),buildOutput:s.join(process.cwd(),`dist`)},outputExt:`ts`},q={...K,paths:{...K.paths??{}}};let J=!1,Y;const le=e=>{let t=K.paths??{},n=q.paths??{},r=Object.entries(e??{}).reduce((e,[t,n])=>(typeof n==`string`&&n.trim().length>0&&(e[t]=n),e),{});return{...t,...n,...r}},X=e=>e?q[e]:q,ue=(e,t={})=>{let n={...q,prisma:e,paths:le(t.paths)};t.pagination!==void 0&&(n.pagination=t.pagination),t.outputExt!==void 0&&(n.outputExt=t.outputExt),Object.assign(q,{...n}),n.pagination?.urlDriver},Z=e=>{let t=e?.default??e;!t||typeof t!=`object`||!t.prisma||(ue(t.prisma,{pagination:t.pagination,paths:t.paths,outputExt:t.outputExt}),J=!0)},de=e=>import(`${ne(e).href}?arkorm_runtime=${Date.now()}`),fe=()=>{let e=f(import.meta.url),n=[s.join(process.cwd(),`arkormx.config.cjs`)];for(let r of n)if(t(r))try{return Z(e(r)),!0}catch{continue}return!1},pe=async()=>{if(!J){if(Y)return await Y;fe()||(Y=(async()=>{let e=[s.join(process.cwd(),`arkormx.config.js`),s.join(process.cwd(),`arkormx.config.ts`)];for(let n of e)if(t(n))try{Z(await de(n));return}catch{continue}J=!0})(),await Y)}},Q=()=>G();pe();var me=class{command;config={};constructor(){this.config=X()}getConfig=X;ensureDirectory(e){let r=c(e);t(r)||n(r,{recursive:!0})}formatPathForLog(e){let t=d(process.cwd(),e);return t?t.startsWith(`..`)?e:t:`.`}splitLogger(e,t){return t=t.includes(process.cwd())?this.formatPathForLog(t):t,x.twoColumnDetail(e+` `,` `+t,!1).join(``)}hasTypeScriptInstalled(){try{return f(import.meta.url).resolve(`typescript`,{paths:[process.cwd()]}),!0}catch{return!1}}resolveOutputExt(){let e=this.getConfig(`outputExt`)===`js`?`js`:`ts`;return e===`ts`&&!this.hasTypeScriptInstalled()?`js`:e}stripKnownSourceExtension(e){return e.replace(/\.(ts|tsx|mts|cts|js|mjs|cjs)$/i,``)}resolveRuntimeDirectoryPath(e){if(t(e))return e;let{buildOutput:n}=this.getConfig(`paths`)||{};if(typeof n!=`string`||n.trim().length===0)return e;let r=d(process.cwd(),e);if(!r||r.startsWith(`..`))return e;let i=u(n,r);return t(i)?i:e}resolveRuntimeScriptPath(e){let n=l(e).toLowerCase(),r=n===`.ts`||n===`.mts`||n===`.cts`,i=[];if(r){let t=e.slice(0,-n.length);i.push(`${t}.js`,`${t}.cjs`,`${t}.mjs`)}let{buildOutput:a}=this.getConfig(`paths`)??{};if(typeof a==`string`&&a.trim().length>0){let t=d(process.cwd(),e);if(t&&!t.startsWith(`..`)){let e=u(a,t),n=l(e).toLowerCase();if(n===`.ts`||n===`.mts`||n===`.cts`){let t=e.slice(0,-n.length);i.push(`${t}.js`,`${t}.cjs`,`${t}.mjs`)}else i.push(e)}}return i.find(e=>t(e))||e}generateFile(e,n,i,s){t(n)&&!s?.force?(this.command.error(`Error: ${this.formatPathForLog(n)} already exists.`),process.exit(1)):t(n)&&s?.force&&a(n);let c=r(e,`utf-8`);for(let[e,t]of Object.entries(i))c=c.replace(RegExp(`{{${e}}}`,`g`),t);return this.ensureDirectory(n),o(n,c),n}resolveConfigPath(e,t){let{[e]:n}=this.getConfig(`paths`)??{};return typeof n==`string`&&n.trim().length>0?n:t}resolveStubPath(e){return u(this.resolveConfigPath(`stubs`,Q()),e)}makeFactory(e,t={}){let n=b(e.replace(/Factory$/,``)).pascal(),r=`${n}Factory`,i=t.modelName?b(t.modelName).pascal():n,a=this.resolveOutputExt(),o=u(this.resolveConfigPath(`factories`,u(process.cwd(),`database`,`factories`)),`${r}.${a}`),s=u(this.resolveConfigPath(`models`,u(process.cwd(),`src`,`models`)),`${i}.${a}`),l=t.modelImportPath??`./${this.stripKnownSourceExtension(d(c(o),s).replace(/\\/g,`/`))}${a===`js`?`.js`:``}`,f=this.resolveStubPath(a===`js`?`factory.js.stub`:`factory.stub`);return{name:r,path:this.generateFile(f,o,{FactoryName:r,ModelName:i.toString(),ModelImportPath:l.startsWith(`.`)?l:`./${l}`},t)}}makeSeeder(e,t={}){let n=`${b(e.replace(/Seeder$/,``)).pascal()}Seeder`,r=this.resolveOutputExt(),i=u(this.resolveConfigPath(`seeders`,u(process.cwd(),`database`,`seeders`)),`${n}.${r}`),a=this.resolveStubPath(r===`js`?`seeder.js.stub`:`seeder.stub`);return{name:n,path:this.generateFile(a,i,{SeederName:n},t)}}makeMigration(e){let t=H(e,{directory:this.resolveConfigPath(`migrations`,u(process.cwd(),`database`,`migrations`)),extension:this.resolveOutputExt()});return{name:t.className,path:t.filePath}}makeModel(e,t={}){let n=b(e.replace(/Model$/,``)).pascal().toString(),r=`${n}`,i=b(n).camel().plural().toString(),a=this.resolveOutputExt(),o=u(this.resolveConfigPath(`models`,u(process.cwd(),`src`,`models`)),`${r}.${a}`),s=t.all||t.factory,l=t.all||t.seeder,f=t.all||t.migration,p=`${n}Factory`,m=u(this.resolveConfigPath(`factories`,u(process.cwd(),`database`,`factories`)),`${p}.${a}`),h=`./${d(c(o),m).replace(/\\/g,`/`).replace(/\.(ts|tsx|mts|cts|js|mjs|cjs)$/i,``)}${a===`js`?`.js`:``}`,g=this.resolveStubPath(a===`js`?`model.js.stub`:`model.stub`),_=this.generateFile(g,o,{ModelName:r,DelegateName:i,FactoryImport:s?`import { ${p} } from '${h}'\n`:``,FactoryLink:s?a===`js`?`\n static factoryClass = ${p}`:`\n protected static override factoryClass = ${p}`:``},t),v=this.ensurePrismaModelEntry(r,i),y={model:{name:r,path:_},prisma:v,factory:void 0,seeder:void 0,migration:void 0};return s&&(y.factory=this.makeFactory(n,{force:t.force,modelName:r,modelImportPath:`./${d(c(m),o).replace(/\\/g,`/`).replace(/\.(ts|tsx|mts|cts|js|mjs|cjs)$/i,``)}${a===`js`?`.js`:``}`})),l&&(y.seeder=this.makeSeeder(n,{force:t.force})),f&&(y.migration=this.makeMigration(`create ${i} table`)),y}ensurePrismaModelEntry(e,n){let i=u(process.cwd(),`prisma`,`schema.prisma`);if(!t(i))return{path:i,updated:!1};let a=r(i,`utf-8`),s=P(a,n),c=RegExp(`model\\s+${e}\\s*\\{`,`m`).test(a);return s||c?{path:i,updated:!1}:(o(i,F(a,{type:`createTable`,table:n,columns:[{name:`id`,type:`id`,primary:!0}]})),{path:i,updated:!0})}prismaTypeToTs(e){return e===`Int`||e===`Float`||e===`Decimal`?`number`:e===`BigInt`?`bigint`:e===`String`?`string`:e===`Boolean`?`boolean`:e===`DateTime`?`Date`:e===`Json`?`Record<string, unknown>`:e===`Bytes`?`Buffer`:`unknown`}parsePrismaModels(e){let t=[],n=/model\s+(\w+)\s*\{([\s\S]*?)\n\}/g,r=new Set([`Int`,`Float`,`Decimal`,`BigInt`,`String`,`Boolean`,`DateTime`,`Json`,`Bytes`]);for(let i of e.matchAll(n)){let e=i[1],n=i[2],a=n.match(/@@map\("([^"]+)"\)/)?.[1]??`${e.charAt(0).toLowerCase()}${e.slice(1)}s`,o=[];n.split(`
11
- `).forEach(e=>{let t=e.trim();if(!t||t.startsWith(`@@`)||t.startsWith(`//`))return;let n=t.match(/^(\w+)\s+([A-Za-z]+)(\?)?\b/);if(!n)return;let i=n[2];r.has(i)&&o.push({name:n[1],type:this.prismaTypeToTs(i),optional:!!n[3]})}),t.push({name:e,table:a,fields:o})}return t}syncModelDeclarations(e,t){let n=e.split(`
12
- `),r=n.findIndex(e=>/export\s+class\s+\w+\s+extends\s+Model<.+>\s*\{/.test(e));if(r<0)return{content:e,updated:!1};let i=-1,a=0;for(let e=r;e<n.length;e+=1){let t=n[e];if(a+=(t.match(/\{/g)||[]).length,a-=(t.match(/\}/g)||[]).length,a===0){i=e;break}}if(i<0)return{content:e,updated:!1};let o=n.slice(r+1,i).filter(e=>!/^\s*declare\s+\w+\??:\s*[^\n]+$/.test(e)),s=[...t.map(e=>` ${e}`),...o],c=[...n.slice(0,r+1),...s,...n.slice(i)].join(`
13
- `);return{content:c,updated:c!==e}}syncModelsFromPrisma(e={}){let n=e.schemaPath??u(process.cwd(),`prisma`,`schema.prisma`),a=e.modelsDir??this.resolveConfigPath(`models`,u(process.cwd(),`src`,`models`));if(!t(n))throw Error(`Prisma schema file not found: ${n}`);if(!t(a))throw Error(`Models directory not found: ${a}`);let s=r(n,`utf-8`),c=this.parsePrismaModels(s),l=i(a).filter(e=>e.endsWith(`.ts`)),d=[],f=[];return l.forEach(e=>{let t=u(a,e),n=r(t,`utf-8`),i=n.match(/export\s+class\s+(\w+)\s+extends\s+Model<'([^']+)'>/);if(!i){f.push(t);return}let s=i[1],l=i[2],p=c.find(e=>e.table===l)??c.find(e=>e.name===s);if(!p||p.fields.length===0){f.push(t);return}let m=p.fields.map(e=>`declare ${e.name}${e.optional?`?`:``}: ${e.type}`),h=this.syncModelDeclarations(n,m);if(!h.updated){f.push(t);return}o(t,h.content),d.push(t)}),{schemaPath:n,modelsDir:a,total:l.length,updated:d,skipped:f}}},$=class extends S{signature=`init
2
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "fs";
3
+ import path, { dirname, extname, join, relative } from "path";
4
+ import { createRequire } from "module";
5
+ import { existsSync as existsSync$1, mkdirSync as mkdirSync$1, readFileSync as readFileSync$1, readdirSync as readdirSync$1, writeFileSync as writeFileSync$1 } from "node:fs";
6
+ import { join as join$1, resolve } from "node:path";
7
+ import { spawnSync } from "node:child_process";
8
+ import { str } from "@h3ravel/support";
9
+ import { fileURLToPath, pathToFileURL } from "url";
10
+ import { Logger } from "@h3ravel/shared";
11
+ import { Command, Kernel } from "@h3ravel/musket";
12
+ import { pathToFileURL as pathToFileURL$1 } from "node:url";
13
+
14
+ //#region src/Exceptions/ArkormException.ts
15
+ /**
16
+ * The ArkormException class is a custom error type for handling
17
+ * exceptions specific to the Arkormˣ.
18
+ *
19
+ * @author Legacy (3m1n3nc3)
20
+ * @since 0.1.0
21
+ */
22
+ var ArkormException = class extends Error {
23
+ constructor(message) {
24
+ super(message);
25
+ this.name = "ArkormException";
26
+ }
27
+ };
28
+
29
+ //#endregion
30
+ //#region src/database/TableBuilder.ts
31
+ /**
32
+ * The TableBuilder class provides a fluent interface for defining
33
+ * the structure of a database table in a migration, including columns to add or drop.
34
+ *
35
+ * @author Legacy (3m1n3nc3)
36
+ * @since 0.1.0
37
+ */
38
+ var TableBuilder = class {
39
+ columns = [];
40
+ dropColumnNames = [];
41
+ indexes = [];
42
+ latestColumnName;
43
+ /**
44
+ * Defines a primary key column in the table.
45
+ *
46
+ * @param columnNameOrOptions
47
+ * @param options
48
+ * @returns
49
+ */
50
+ primary(columnNameOrOptions, options) {
51
+ const config = typeof columnNameOrOptions === "string" ? {
52
+ columnName: columnNameOrOptions,
53
+ ...options ?? {}
54
+ } : columnNameOrOptions ?? {};
55
+ const column = this.resolveColumn(config.columnName);
56
+ column.primary = true;
57
+ if (typeof config.autoIncrement === "boolean") column.autoIncrement = config.autoIncrement;
58
+ if (Object.prototype.hasOwnProperty.call(config, "default")) column.default = config.default;
59
+ return this;
60
+ }
61
+ /**
62
+ * Defines an auto-incrementing primary key column.
63
+ *
64
+ * @param name The name of the primary key column.
65
+ * @default 'id'
66
+ * @returns The current TableBuilder instance for chaining.
67
+ */
68
+ id(name = "id", type = "id") {
69
+ return this.column(name, type, { primary: true });
70
+ }
71
+ /**
72
+ * Defines a UUID column in the table.
73
+ *
74
+ * @param name The name of the UUID column.
75
+ * @param options Additional options for the UUID column.
76
+ * @returns The current TableBuilder instance for chaining.
77
+ */
78
+ uuid(name, options = {}) {
79
+ return this.column(name, "uuid", options);
80
+ }
81
+ /**
82
+ * Defines a string column in the table.
83
+ *
84
+ * @param name The name of the string column.
85
+ * @param options Additional options for the string column.
86
+ * @returns The current TableBuilder instance for chaining.
87
+ */
88
+ string(name, options = {}) {
89
+ return this.column(name, "string", options);
90
+ }
91
+ /**
92
+ * Defines a text column in the table.
93
+ *
94
+ * @param name The name of the text column.
95
+ * @param options Additional options for the text column.
96
+ * @returns The current TableBuilder instance for chaining.
97
+ */
98
+ text(name, options = {}) {
99
+ return this.column(name, "text", options);
100
+ }
101
+ /**
102
+ * Defines an integer column in the table.
103
+ *
104
+ * @param name The name of the integer column.
105
+ * @param options Additional options for the integer column.
106
+ * @returns
107
+ */
108
+ integer(name, options = {}) {
109
+ return this.column(name, "integer", options);
110
+ }
111
+ /**
112
+ * Defines a big integer column in the table.
113
+ *
114
+ * @param name The name of the big integer column.
115
+ * @param options Additional options for the big integer column.
116
+ * @returns
117
+ */
118
+ bigInteger(name, options = {}) {
119
+ return this.column(name, "bigInteger", options);
120
+ }
121
+ /**
122
+ * Defines a float column in the table.
123
+ *
124
+ * @param name The name of the float column.
125
+ * @param options Additional options for the float column.
126
+ * @returns
127
+ */
128
+ float(name, options = {}) {
129
+ return this.column(name, "float", options);
130
+ }
131
+ /**
132
+ * Defines a boolean column in the table.
133
+ *
134
+ * @param name The name of the boolean column.
135
+ * @param options Additional options for the boolean column.
136
+ * @returns
137
+ */
138
+ boolean(name, options = {}) {
139
+ return this.column(name, "boolean", options);
140
+ }
141
+ /**
142
+ * Defines a JSON column in the table.
143
+ *
144
+ * @param name The name of the JSON column.
145
+ * @param options Additional options for the JSON column.
146
+ * @returns
147
+ */
148
+ json(name, options = {}) {
149
+ return this.column(name, "json", options);
150
+ }
151
+ /**
152
+ * Defines a date column in the table.
153
+ *
154
+ * @param name The name of the date column.
155
+ * @param options Additional options for the date column.
156
+ * @returns
157
+ */
158
+ date(name, options = {}) {
159
+ return this.column(name, "date", options);
160
+ }
161
+ /**
162
+ * Defines colonns for a polymorphic relationship in the table.
163
+ *
164
+ * @param name The base name for the polymorphic relationship columns.
165
+ * @returns
166
+ */
167
+ morphs(name, nullable = false) {
168
+ this.string(`${name}Type`, { nullable });
169
+ this.integer(`${name}Id`, { nullable });
170
+ return this;
171
+ }
172
+ /**
173
+ * Defines nullable columns for a polymorphic relationship in the table.
174
+ *
175
+ * @param name The base name for the polymorphic relationship columns.
176
+ * @returns
177
+ */
178
+ nullableMorphs(name) {
179
+ return this.morphs(name, true);
180
+ }
181
+ /**
182
+ * Defines a timestamp column in the table.
183
+ *
184
+ * @param name The name of the timestamp column.
185
+ * @param options Additional options for the timestamp column.
186
+ * @returns
187
+ */
188
+ timestamp(name, options = {}) {
189
+ return this.column(name, "timestamp", options);
190
+ }
191
+ /**
192
+ * Defines both createdAt and updatedAt timestamp columns in the table.
193
+ *
194
+ * @returns
195
+ */
196
+ timestamps() {
197
+ this.timestamp("createdAt", { nullable: false });
198
+ this.timestamp("updatedAt", { nullable: false });
199
+ return this;
200
+ }
201
+ /**
202
+ * Defines a soft delete timestamp column in the table.
203
+ *
204
+ * @param column The name of the soft delete column.
205
+ * @returns
206
+ */
207
+ softDeletes(column = "deletedAt") {
208
+ this.timestamp(column, { nullable: true });
209
+ return this;
210
+ }
211
+ /**
212
+ * Defines a column to be dropped from the table in an alterTable operation.
213
+ *
214
+ * @param name The name of the column to drop.
215
+ * @returns
216
+ */
217
+ dropColumn(name) {
218
+ this.dropColumnNames.push(name);
219
+ return this;
220
+ }
221
+ /**
222
+ * Marks a column as nullable.
223
+ *
224
+ * @param columnName Optional explicit column name. When omitted, applies to the latest defined column.
225
+ * @returns The current TableBuilder instance for chaining.
226
+ */
227
+ nullable(columnName) {
228
+ const column = this.resolveColumn(columnName);
229
+ column.nullable = true;
230
+ return this;
231
+ }
232
+ /**
233
+ * Sets the column position to appear after another column when possible.
234
+ *
235
+ * @param referenceColumn The column that the target column should be placed after.
236
+ * @param columnName Optional explicit target column name. When omitted, applies to the latest defined column.
237
+ * @returns The current TableBuilder instance for chaining.
238
+ */
239
+ after(referenceColumn, columnName) {
240
+ const column = this.resolveColumn(columnName);
241
+ column.after = referenceColumn;
242
+ return this;
243
+ }
244
+ /**
245
+ * Maps the column to a custom database column name.
246
+ *
247
+ * @param name The custom database column name.
248
+ * @param columnName Optional explicit target column name. When omitted, applies to the latest defined column.
249
+ * @returns The current TableBuilder instance for chaining.
250
+ */
251
+ map(name, columnName) {
252
+ const column = this.resolveColumn(columnName);
253
+ column.map = name;
254
+ return this;
255
+ }
256
+ /**
257
+ * Defines an index on one or more columns.
258
+ *
259
+ * @param columns Optional target columns. When omitted, applies to the latest defined column.
260
+ * @param name Optional index name.
261
+ * @returns The current TableBuilder instance for chaining.
262
+ */
263
+ index(columns, name) {
264
+ const columnList = Array.isArray(columns) ? columns : typeof columns === "string" ? [columns] : [this.resolveColumn().name];
265
+ this.indexes.push({
266
+ columns: [...columnList],
267
+ name
268
+ });
269
+ return this;
270
+ }
271
+ /**
272
+ * Returns a deep copy of the defined columns for the table.
273
+ *
274
+ * @returns
275
+ */
276
+ getColumns() {
277
+ return this.columns.map((column) => ({ ...column }));
278
+ }
279
+ /**
280
+ * Returns a copy of the defined column names to be dropped from the table.
281
+ *
282
+ * @returns
283
+ */
284
+ getDropColumns() {
285
+ return [...this.dropColumnNames];
286
+ }
287
+ /**
288
+ * Returns a deep copy of the defined indexes for the table.
289
+ *
290
+ * @returns
291
+ */
292
+ getIndexes() {
293
+ return this.indexes.map((index) => ({
294
+ ...index,
295
+ columns: [...index.columns]
296
+ }));
297
+ }
298
+ /**
299
+ * Defines a column in the table with the given name.
300
+ *
301
+ * @param name The name of the column.
302
+ * @param type The type of the column.
303
+ * @param options Additional options for the column.
304
+ * @returns
305
+ */
306
+ column(name, type, options) {
307
+ this.columns.push({
308
+ name,
309
+ type,
310
+ map: options.map,
311
+ nullable: options.nullable,
312
+ unique: options.unique,
313
+ primary: options.primary,
314
+ autoIncrement: options.autoIncrement,
315
+ after: options.after,
316
+ default: options.default
317
+ });
318
+ this.latestColumnName = name;
319
+ return this;
320
+ }
321
+ /**
322
+ * Resolve a target column by name or fallback to the latest defined column.
323
+ *
324
+ * @param columnName
325
+ * @returns
326
+ */
327
+ resolveColumn(columnName) {
328
+ const targetName = columnName ?? this.latestColumnName;
329
+ if (!targetName) throw new Error("No column available for this operation.");
330
+ const column = this.columns.find((item) => item.name === targetName);
331
+ if (!column) throw new Error(`Column [${targetName}] was not found in the table definition.`);
332
+ return column;
333
+ }
334
+ };
335
+
336
+ //#endregion
337
+ //#region src/database/SchemaBuilder.ts
338
+ /**
339
+ * The SchemaBuilder class provides methods for defining the operations to be
340
+ * performed in a migration, such as creating, altering, or dropping tables.
341
+ *
342
+ * @author Legacy (3m1n3nc3)
343
+ * @since 0.1.0
344
+ */
345
+ var SchemaBuilder = class {
346
+ operations = [];
347
+ /**
348
+ * Defines a new table to be created in the migration.
349
+ *
350
+ * @param table The name of the table to create.
351
+ * @param callback A callback function to define the table's columns and structure.
352
+ * @returns The current SchemaBuilder instance for chaining.
353
+ */
354
+ createTable(table, callback) {
355
+ const builder = new TableBuilder();
356
+ callback(builder);
357
+ this.operations.push({
358
+ type: "createTable",
359
+ table,
360
+ columns: builder.getColumns(),
361
+ indexes: builder.getIndexes()
362
+ });
363
+ return this;
364
+ }
365
+ /**
366
+ * Defines alterations to an existing table in the migration.
367
+ *
368
+ * @param table The name of the table to alter.
369
+ * @param callback A callback function to define the alterations to the table's columns and structure.
370
+ * @returns The current SchemaBuilder instance for chaining.
371
+ */
372
+ alterTable(table, callback) {
373
+ const builder = new TableBuilder();
374
+ callback(builder);
375
+ this.operations.push({
376
+ type: "alterTable",
377
+ table,
378
+ addColumns: builder.getColumns(),
379
+ dropColumns: builder.getDropColumns(),
380
+ addIndexes: builder.getIndexes()
381
+ });
382
+ return this;
383
+ }
384
+ /**
385
+ * Defines a table to be dropped in the migration.
386
+ *
387
+ * @param table The name of the table to drop.
388
+ * @returns The current SchemaBuilder instance for chaining.
389
+ */
390
+ dropTable(table) {
391
+ this.operations.push({
392
+ type: "dropTable",
393
+ table
394
+ });
395
+ return this;
396
+ }
397
+ /**
398
+ * Returns a deep copy of the defined schema operations for the migration/
399
+ *
400
+ * @returns An array of schema operations for the migration.
401
+ */
402
+ getOperations() {
403
+ return this.operations.map((operation) => {
404
+ if (operation.type === "createTable") return {
405
+ ...operation,
406
+ columns: operation.columns.map((column) => ({ ...column })),
407
+ indexes: operation.indexes.map((index) => ({
408
+ ...index,
409
+ columns: [...index.columns]
410
+ }))
411
+ };
412
+ if (operation.type === "alterTable") return {
413
+ ...operation,
414
+ addColumns: operation.addColumns.map((column) => ({ ...column })),
415
+ dropColumns: [...operation.dropColumns],
416
+ addIndexes: operation.addIndexes.map((index) => ({
417
+ ...index,
418
+ columns: [...index.columns]
419
+ }))
420
+ };
421
+ return { ...operation };
422
+ });
423
+ }
424
+ };
425
+
426
+ //#endregion
427
+ //#region src/helpers/migrations.ts
428
+ const PRISMA_MODEL_REGEX = /model\s+(\w+)\s*\{[\s\S]*?\n\}/g;
429
+ /**
430
+ * Convert a table name to a PascalCase model name, with basic singularization.
431
+ *
432
+ * @param tableName The name of the table to convert.
433
+ * @returns The corresponding PascalCase model name.
434
+ */
435
+ const toModelName = (tableName) => {
436
+ const normalized = tableName.replace(/[^a-zA-Z0-9]+/g, " ").trim();
437
+ const parts = (normalized.endsWith("s") && normalized.length > 1 ? normalized.slice(0, -1) : normalized).split(/\s+/g).filter(Boolean);
438
+ if (parts.length === 0) return "GeneratedModel";
439
+ return parts.map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`).join("");
440
+ };
441
+ /**
442
+ * Escape special characters in a string for use in a regular expression.
443
+ *
444
+ * @param value
445
+ * @returns
446
+ */
447
+ const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
448
+ /**
449
+ * Convert a SchemaColumn definition to a Prisma field type string, including modifiers.
450
+ *
451
+ * @param column
452
+ * @returns
453
+ */
454
+ const resolvePrismaType = (column) => {
455
+ if (column.type === "id") return "Int";
456
+ if (column.type === "uuid") return "String";
457
+ if (column.type === "string" || column.type === "text") return "String";
458
+ if (column.type === "integer") return "Int";
459
+ if (column.type === "bigInteger") return "BigInt";
460
+ if (column.type === "float") return "Float";
461
+ if (column.type === "boolean") return "Boolean";
462
+ if (column.type === "json") return "Json";
463
+ return "DateTime";
464
+ };
465
+ /**
466
+ * Format a default value for inclusion in a Prisma schema field definition, based on its type.
467
+ *
468
+ * @param value
469
+ * @returns
470
+ */
471
+ const formatDefaultValue = (value) => {
472
+ if (value == null) return void 0;
473
+ if (typeof value === "string") return `@default("${value.replace(/"/g, "\\\"")}")`;
474
+ if (typeof value === "number" || typeof value === "bigint") return `@default(${value})`;
475
+ if (typeof value === "boolean") return `@default(${value ? "true" : "false"})`;
476
+ };
477
+ /**
478
+ * Build a single line of a Prisma model field definition based on a SchemaColumn, including type and modifiers.
479
+ *
480
+ * @param column
481
+ * @returns
482
+ */
483
+ const buildFieldLine = (column) => {
484
+ if (column.type === "id") {
485
+ const primary = column.primary === false ? "" : " @id";
486
+ const mapped = typeof column.map === "string" && column.map.trim().length > 0 ? ` @map("${column.map.replace(/"/g, "\\\"")}")` : "";
487
+ const configuredDefault = formatDefaultValue(column.default);
488
+ const shouldAutoIncrement = column.autoIncrement ?? column.primary !== false;
489
+ const defaultSuffix = configuredDefault ? ` ${configuredDefault}` : shouldAutoIncrement && primary ? " @default(autoincrement())" : "";
490
+ return ` ${column.name} Int${primary}${defaultSuffix}${mapped}`;
491
+ }
492
+ const scalar = resolvePrismaType(column);
493
+ const nullable = column.nullable ? "?" : "";
494
+ const unique = column.unique ? " @unique" : "";
495
+ const primary = column.primary ? " @id" : "";
496
+ const mapped = typeof column.map === "string" && column.map.trim().length > 0 ? ` @map("${column.map.replace(/"/g, "\\\"")}")` : "";
497
+ const defaultValue = formatDefaultValue(column.default) ?? (column.type === "uuid" && column.primary ? "@default(uuid())" : void 0);
498
+ const defaultSuffix = defaultValue ? ` ${defaultValue}` : "";
499
+ return ` ${column.name} ${scalar}${nullable}${primary}${unique}${defaultSuffix}${mapped}`;
500
+ };
501
+ /**
502
+ * Build a Prisma model-level @@index definition line.
503
+ *
504
+ * @param index
505
+ * @returns
506
+ */
507
+ const buildIndexLine = (index) => {
508
+ return ` @@index([${index.columns.join(", ")}]${typeof index.name === "string" && index.name.trim().length > 0 ? `, name: "${index.name.replace(/"/g, "\\\"")}"` : ""})`;
509
+ };
510
+ /**
511
+ * Build a Prisma model block string based on a SchemaTableCreateOperation, including
512
+ * all fields and any necessary mapping.
513
+ *
514
+ * @param operation The schema table create operation to convert.
515
+ * @returns The corresponding Prisma model block string.
516
+ */
517
+ const buildModelBlock = (operation) => {
518
+ const modelName = toModelName(operation.table);
519
+ const mapped = operation.table !== modelName.toLowerCase();
520
+ const fields = operation.columns.map(buildFieldLine);
521
+ const metadata = [...(operation.indexes ?? []).map(buildIndexLine), ...mapped ? [` @@map("${str(operation.table).snake()}")`] : []];
522
+ return `model ${modelName} {\n${(metadata.length > 0 ? [
523
+ ...fields,
524
+ "",
525
+ ...metadata
526
+ ] : fields).join("\n")}\n}`;
527
+ };
528
+ /**
529
+ * Find the Prisma model block in a schema string that corresponds to a given
530
+ * table name, using both explicit mapping and naming conventions.
531
+ *
532
+ * @param schema
533
+ * @param table
534
+ * @returns
535
+ */
536
+ const findModelBlock = (schema, table) => {
537
+ const candidates = [...schema.matchAll(PRISMA_MODEL_REGEX)];
538
+ const explicitMapRegex = new RegExp(`@@map\\("${escapeRegex(table)}"\\)`);
539
+ for (const match of candidates) {
540
+ const block = match[0];
541
+ const modelName = match[1];
542
+ const start = match.index ?? 0;
543
+ const end = start + block.length;
544
+ if (explicitMapRegex.test(block)) return {
545
+ modelName,
546
+ block,
547
+ start,
548
+ end
549
+ };
550
+ if (modelName.toLowerCase() === table.toLowerCase()) return {
551
+ modelName,
552
+ block,
553
+ start,
554
+ end
555
+ };
556
+ if (modelName.toLowerCase() === toModelName(table).toLowerCase()) return {
557
+ modelName,
558
+ block,
559
+ start,
560
+ end
561
+ };
562
+ }
563
+ return null;
564
+ };
565
+ /**
566
+ * Apply a create table operation to a Prisma schema string, adding a new model
567
+ * block for the specified table and fields.
568
+ *
569
+ * @param schema The current Prisma schema string.
570
+ * @param operation The schema table create operation to apply.
571
+ * @returns The updated Prisma schema string with the new model block.
572
+ */
573
+ const applyCreateTableOperation = (schema, operation) => {
574
+ if (findModelBlock(schema, operation.table)) throw new ArkormException(`Prisma model for table [${operation.table}] already exists.`);
575
+ const block = buildModelBlock(operation);
576
+ return `${schema.trimEnd()}\n\n${block}\n`;
577
+ };
578
+ /**
579
+ * Apply an alter table operation to a Prisma schema string, modifying the model
580
+ * block for the specified table by adding and removing fields as needed.
581
+ *
582
+ * @param schema The current Prisma schema string.
583
+ * @param operation The schema table alter operation to apply.
584
+ * @returns The updated Prisma schema string with the modified model block.
585
+ */
586
+ const applyAlterTableOperation = (schema, operation) => {
587
+ const model = findModelBlock(schema, operation.table);
588
+ if (!model) throw new ArkormException(`Prisma model for table [${operation.table}] was not found.`);
589
+ let block = model.block;
590
+ const bodyLines = block.split("\n");
591
+ operation.dropColumns.forEach((column) => {
592
+ const columnRegex = new RegExp(`^\\s*${escapeRegex(column)}\\s+`);
593
+ for (let index = 0; index < bodyLines.length; index += 1) if (columnRegex.test(bodyLines[index])) {
594
+ bodyLines.splice(index, 1);
595
+ return;
596
+ }
597
+ });
598
+ operation.addColumns.forEach((column) => {
599
+ const fieldLine = buildFieldLine(column);
600
+ const columnRegex = new RegExp(`^\\s*${escapeRegex(column.name)}\\s+`);
601
+ if (bodyLines.some((line) => columnRegex.test(line))) return;
602
+ const defaultInsertIndex = Math.max(1, bodyLines.length - 1);
603
+ const afterInsertIndex = typeof column.after === "string" && column.after.length > 0 ? bodyLines.findIndex((line) => new RegExp(`^\\s*${escapeRegex(column.after)}\\s+`).test(line)) : -1;
604
+ const insertIndex = afterInsertIndex > 0 ? Math.min(afterInsertIndex + 1, defaultInsertIndex) : defaultInsertIndex;
605
+ bodyLines.splice(insertIndex, 0, fieldLine);
606
+ });
607
+ (operation.addIndexes ?? []).forEach((index) => {
608
+ const indexLine = buildIndexLine(index);
609
+ if (bodyLines.some((line) => line.trim() === indexLine.trim())) return;
610
+ const insertIndex = Math.max(1, bodyLines.length - 1);
611
+ bodyLines.splice(insertIndex, 0, indexLine);
612
+ });
613
+ block = bodyLines.join("\n");
614
+ return `${schema.slice(0, model.start)}${block}${schema.slice(model.end)}`;
615
+ };
616
+ /**
617
+ * Apply a drop table operation to a Prisma schema string, removing the model block
618
+ * for the specified table.
619
+ */
620
+ const applyDropTableOperation = (schema, operation) => {
621
+ const model = findModelBlock(schema, operation.table);
622
+ if (!model) return schema;
623
+ const before = schema.slice(0, model.start).trimEnd();
624
+ const after = schema.slice(model.end).trimStart();
625
+ return `${before}${before && after ? "\n\n" : ""}${after}`;
626
+ };
627
+ /**
628
+ * The SchemaBuilder class provides a fluent interface for defining
629
+ * database schema operations in a migration, such as creating, altering, and
630
+ * dropping tables.
631
+ *
632
+ * @param schema The current Prisma schema string.
633
+ * @param operations The list of schema operations to apply.
634
+ * @returns The updated Prisma schema string after applying all operations.
635
+ */
636
+ const applyOperationsToPrismaSchema = (schema, operations) => {
637
+ return operations.reduce((current, operation) => {
638
+ if (operation.type === "createTable") return applyCreateTableOperation(current, operation);
639
+ if (operation.type === "alterTable") return applyAlterTableOperation(current, operation);
640
+ return applyDropTableOperation(current, operation);
641
+ }, schema);
642
+ };
643
+ /**
644
+ * Run a Prisma CLI command using npx, capturing and throwing any errors that occur.
645
+ *
646
+ * @param args The arguments to pass to the Prisma CLI command.
647
+ * @param cwd The current working directory to run the command in.
648
+ * @returns void
649
+ */
650
+ const runPrismaCommand = (args, cwd) => {
651
+ const command = spawnSync("npx", ["prisma", ...args], {
652
+ cwd,
653
+ encoding: "utf-8"
654
+ });
655
+ if (command.status === 0) return;
656
+ const errorOutput = [command.stdout, command.stderr].filter(Boolean).join("\n").trim();
657
+ throw new ArkormException(errorOutput ? `Prisma command failed: prisma ${args.join(" ")}\n${errorOutput}` : `Prisma command failed: prisma ${args.join(" ")}`);
658
+ };
659
+ /**
660
+ * Generate a new migration file with a given name and options, including
661
+ * writing the file to disk if specified.
662
+ *
663
+ * @param name
664
+ * @returns
665
+ */
666
+ const resolveMigrationClassName = (name) => {
667
+ const cleaned = name.replace(/[^a-zA-Z0-9]+/g, " ").trim();
668
+ if (!cleaned) return "GeneratedMigration";
669
+ return `${cleaned.split(/\s+/g).map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`).join("")}Migration`;
670
+ };
671
+ /**
672
+ * Pad a number with leading zeros to ensure it is at least two digits, for
673
+ * use in migration timestamps.
674
+ *
675
+ * @param value
676
+ * @returns
677
+ */
678
+ const pad = (value) => String(value).padStart(2, "0");
679
+ /**
680
+ * Create a timestamp string in the format YYYYMMDDHHMMSS for use in migration
681
+ * file names, based on the current date and time or a provided date.
682
+ *
683
+ * @param date
684
+ * @returns
685
+ */
686
+ const createMigrationTimestamp = (date = /* @__PURE__ */ new Date()) => {
687
+ return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
688
+ };
689
+ /**
690
+ * Convert a migration name to a slug suitable for use in a file name, by
691
+ * lowercasing and replacing non-alphanumeric characters with underscores.
692
+ *
693
+ * @param name
694
+ * @returns
695
+ */
696
+ const toMigrationFileSlug = (name) => {
697
+ return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "migration";
698
+ };
699
+ /**
700
+ * Build the source code for a new migration file based on a given class
701
+ * name, using a template with empty up and down methods.
702
+ *
703
+ * @param className
704
+ * @returns
705
+ */
706
+ const buildMigrationSource = (className, extension = "ts") => {
707
+ if (extension === "js") return [
708
+ "import { Migration } from 'arkormx'",
709
+ "",
710
+ `export default class ${className} extends Migration {`,
711
+ " /**",
712
+ " * @param {import('arkormx').SchemaBuilder} schema",
713
+ " * @returns {Promise<void>}",
714
+ " */",
715
+ " async up (schema) {",
716
+ " }",
717
+ "",
718
+ " /**",
719
+ " * @param {import('arkormx').SchemaBuilder} schema",
720
+ " * @returns {Promise<void>}",
721
+ " */",
722
+ " async down (schema) {",
723
+ " }",
724
+ "}",
725
+ ""
726
+ ].join("\n");
727
+ return [
728
+ "import { Migration, SchemaBuilder } from 'arkormx'",
729
+ "",
730
+ `export default class ${className} extends Migration {`,
731
+ " public async up (schema: SchemaBuilder): Promise<void> {",
732
+ " }",
733
+ "",
734
+ " public async down (schema: SchemaBuilder): Promise<void> {",
735
+ " }",
736
+ "}",
737
+ ""
738
+ ].join("\n");
739
+ };
740
+ /**
741
+ * Generate a new migration file with a given name and options, including
742
+ * writing the file to disk if specified, and return the details of the generated file.
743
+ *
744
+ * @param name
745
+ * @param options
746
+ * @returns
747
+ */
748
+ const generateMigrationFile = (name, options = {}) => {
749
+ const timestamp = createMigrationTimestamp(/* @__PURE__ */ new Date());
750
+ const fileSlug = toMigrationFileSlug(name);
751
+ const className = resolveMigrationClassName(name);
752
+ const extension = options.extension ?? "ts";
753
+ const directory = options.directory ?? join$1(process.cwd(), "database", "migrations");
754
+ const fileName = `${timestamp}_${fileSlug}.${extension}`;
755
+ const filePath = join$1(directory, fileName);
756
+ const content = buildMigrationSource(className, extension);
757
+ if (options.write ?? true) {
758
+ if (!existsSync$1(directory)) mkdirSync$1(directory, { recursive: true });
759
+ if (existsSync$1(filePath)) throw new ArkormException(`Migration file already exists: ${filePath}`);
760
+ writeFileSync$1(filePath, content);
761
+ }
762
+ return {
763
+ fileName,
764
+ filePath,
765
+ className,
766
+ content
767
+ };
768
+ };
769
+ /**
770
+ * Get the list of schema operations that would be performed by a given migration class when run in a specified direction (up or down), without actually applying them.
771
+ *
772
+ * @param migration The migration class or instance to analyze.
773
+ * @param direction The direction of the migration to plan for ('up' or 'down').
774
+ * @returns A promise that resolves to an array of schema operations that would be performed.
775
+ */
776
+ const getMigrationPlan = async (migration, direction = "up") => {
777
+ const instance = typeof migration === "function" ? new migration() : migration;
778
+ const schema = new SchemaBuilder();
779
+ if (direction === "up") await instance.up(schema);
780
+ else await instance.down(schema);
781
+ return schema.getOperations();
782
+ };
783
+ /**
784
+ * Apply the schema operations defined in a migration to a Prisma schema
785
+ * file, updating the file on disk if specified, and return the updated
786
+ * schema and list of operations applied.
787
+ *
788
+ * @param migration The migration class or instance to apply.
789
+ * @param options Options for applying the migration, including schema path and write flag.
790
+ * @returns A promise that resolves to an object containing the updated schema, schema path, and list of operations applied.
791
+ */
792
+ const applyMigrationToPrismaSchema = async (migration, options = {}) => {
793
+ const schemaPath = options.schemaPath ?? join$1(process.cwd(), "prisma", "schema.prisma");
794
+ if (!existsSync$1(schemaPath)) throw new ArkormException(`Prisma schema file not found: ${schemaPath}`);
795
+ const source = readFileSync$1(schemaPath, "utf-8");
796
+ const operations = await getMigrationPlan(migration, "up");
797
+ const schema = applyOperationsToPrismaSchema(source, operations);
798
+ if (options.write ?? true) writeFileSync$1(schemaPath, schema);
799
+ return {
800
+ schema,
801
+ schemaPath,
802
+ operations
803
+ };
804
+ };
805
+
806
+ //#endregion
807
+ //#region src/helpers/runtime-config.ts
808
+ const resolveDefaultStubsPath = () => {
809
+ let current = path.dirname(fileURLToPath(import.meta.url));
810
+ while (true) {
811
+ const packageJsonPath = path.join(current, "package.json");
812
+ const stubsPath = path.join(current, "stubs");
813
+ if (existsSync(packageJsonPath) && existsSync(stubsPath)) return stubsPath;
814
+ const parent = path.dirname(current);
815
+ if (parent === current) break;
816
+ current = parent;
817
+ }
818
+ return path.join(process.cwd(), "stubs");
819
+ };
820
+ const baseConfig = {
821
+ paths: {
822
+ stubs: resolveDefaultStubsPath(),
823
+ seeders: path.join(process.cwd(), "database", "seeders"),
824
+ models: path.join(process.cwd(), "src", "models"),
825
+ migrations: path.join(process.cwd(), "database", "migrations"),
826
+ factories: path.join(process.cwd(), "database", "factories"),
827
+ buildOutput: path.join(process.cwd(), "dist")
828
+ },
829
+ outputExt: "ts"
830
+ };
831
+ const userConfig = {
832
+ ...baseConfig,
833
+ paths: { ...baseConfig.paths ?? {} }
834
+ };
835
+ let runtimeConfigLoaded = false;
836
+ let runtimeConfigLoadingPromise;
837
+ let runtimeClientResolver;
838
+ let runtimePaginationURLDriverFactory;
839
+ const mergePathConfig = (paths) => {
840
+ const defaults = baseConfig.paths ?? {};
841
+ const current = userConfig.paths ?? {};
842
+ const incoming = Object.entries(paths ?? {}).reduce((all, [key, value]) => {
843
+ if (typeof value === "string" && value.trim().length > 0) all[key] = path.isAbsolute(value) ? value : path.resolve(process.cwd(), value);
844
+ return all;
845
+ }, {});
846
+ return {
847
+ ...defaults,
848
+ ...current,
849
+ ...incoming
850
+ };
851
+ };
852
+ /**
853
+ * Get the user-provided ArkORM configuration.
854
+ *
855
+ * @returns The user-provided ArkORM configuration object.
856
+ */
857
+ const getUserConfig = (key) => {
858
+ if (key) return userConfig[key];
859
+ return userConfig;
860
+ };
861
+ /**
862
+ * Configure the ArkORM runtime with the provided Prisma client resolver and
863
+ * delegate mapping resolver.
864
+ *
865
+ * @param prisma
866
+ * @param mapping
867
+ */
868
+ const configureArkormRuntime = (prisma, options = {}) => {
869
+ const nextConfig = {
870
+ ...userConfig,
871
+ prisma,
872
+ paths: mergePathConfig(options.paths)
873
+ };
874
+ if (options.pagination !== void 0) nextConfig.pagination = options.pagination;
875
+ if (options.outputExt !== void 0) nextConfig.outputExt = options.outputExt;
876
+ Object.assign(userConfig, { ...nextConfig });
877
+ runtimeClientResolver = prisma;
878
+ runtimePaginationURLDriverFactory = nextConfig.pagination?.urlDriver;
879
+ };
880
+ /**
881
+ * Resolve and apply the ArkORM configuration from an imported module.
882
+ * This function checks for a default export and falls back to the module itself, then validates
883
+ * the configuration object and applies it to the runtime if valid.
884
+ *
885
+ * @param imported
886
+ * @returns
887
+ */
888
+ const resolveAndApplyConfig = (imported) => {
889
+ const config = imported?.default ?? imported;
890
+ if (!config || typeof config !== "object" || !config.prisma) return;
891
+ configureArkormRuntime(config.prisma, {
892
+ pagination: config.pagination,
893
+ paths: config.paths,
894
+ outputExt: config.outputExt
895
+ });
896
+ runtimeConfigLoaded = true;
897
+ };
898
+ /**
899
+ * Dynamically import a configuration file.
900
+ * A cache-busting query parameter is appended to ensure the latest version is loaded.
901
+ *
902
+ * @param configPath
903
+ * @returns A promise that resolves to the imported configuration module.
904
+ */
905
+ const importConfigFile = (configPath) => {
906
+ return import(`${pathToFileURL(configPath).href}?arkorm_runtime=${Date.now()}`);
907
+ };
908
+ const loadRuntimeConfigSync = () => {
909
+ const require = createRequire(import.meta.url);
910
+ const syncConfigPaths = [path.join(process.cwd(), "arkormx.config.cjs")];
911
+ for (const configPath of syncConfigPaths) {
912
+ if (!existsSync(configPath)) continue;
913
+ try {
914
+ resolveAndApplyConfig(require(configPath));
915
+ return true;
916
+ } catch {
917
+ continue;
918
+ }
919
+ }
920
+ return false;
921
+ };
922
+ /**
923
+ * Load the ArkORM configuration by searching for configuration files in the
924
+ * current working directory.
925
+ * @returns
926
+ */
927
+ const loadArkormConfig = async () => {
928
+ if (runtimeConfigLoaded) return;
929
+ if (runtimeConfigLoadingPromise) return await runtimeConfigLoadingPromise;
930
+ if (loadRuntimeConfigSync()) return;
931
+ runtimeConfigLoadingPromise = (async () => {
932
+ const configPaths = [path.join(process.cwd(), "arkormx.config.js"), path.join(process.cwd(), "arkormx.config.ts")];
933
+ for (const configPath of configPaths) {
934
+ if (!existsSync(configPath)) continue;
935
+ try {
936
+ resolveAndApplyConfig(await importConfigFile(configPath));
937
+ return;
938
+ } catch {
939
+ continue;
940
+ }
941
+ }
942
+ runtimeConfigLoaded = true;
943
+ })();
944
+ await runtimeConfigLoadingPromise;
945
+ };
946
+ const getDefaultStubsPath = () => {
947
+ return resolveDefaultStubsPath();
948
+ };
949
+ loadArkormConfig();
950
+
951
+ //#endregion
952
+ //#region src/cli/CliApp.ts
953
+ /**
954
+ * Main application class for the Arkormˣ CLI.
955
+ *
956
+ * @author Legacy (3m1n3nc3)
957
+ * @since 0.1.0
958
+ */
959
+ var CliApp = class {
960
+ command;
961
+ config = {};
962
+ constructor() {
963
+ this.config = getUserConfig();
964
+ }
965
+ /**
966
+ * Get the current configuration object or a specific configuration value.
967
+ *
968
+ * @param key Optional specific configuration key to retrieve
969
+ * @returns The entire configuration object or the value of the specified key
970
+ */
971
+ getConfig = getUserConfig;
972
+ /**
973
+ * Utility to ensure directory exists
974
+ *
975
+ * @param filePath
976
+ */
977
+ ensureDirectory(filePath) {
978
+ const dir = dirname(filePath);
979
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
980
+ }
981
+ /**
982
+ * Convert absolute paths under current working directory into relative display paths.
983
+ *
984
+ * @param filePath
985
+ * @returns
986
+ */
987
+ formatPathForLog(filePath) {
988
+ const relPath = relative(process.cwd(), filePath);
989
+ if (!relPath) return ".";
990
+ if (relPath.startsWith("..")) return filePath;
991
+ return relPath;
992
+ }
993
+ /**
994
+ * Utility to format a value for logging, converting absolute paths under current
995
+ * working directory into relative display paths.
996
+ *
997
+ * @param name
998
+ * @param value
999
+ * @returns
1000
+ */
1001
+ splitLogger(name, value) {
1002
+ value = value.includes(process.cwd()) ? this.formatPathForLog(value) : value;
1003
+ return Logger.twoColumnDetail(name + " ", " " + value, false).join("");
1004
+ }
1005
+ hasTypeScriptInstalled() {
1006
+ try {
1007
+ createRequire(import.meta.url).resolve("typescript", { paths: [process.cwd()] });
1008
+ return true;
1009
+ } catch {
1010
+ return false;
1011
+ }
1012
+ }
1013
+ resolveOutputExt() {
1014
+ const preferred = this.getConfig("outputExt") === "js" ? "js" : "ts";
1015
+ if (preferred === "ts" && !this.hasTypeScriptInstalled()) return "js";
1016
+ return preferred;
1017
+ }
1018
+ stripKnownSourceExtension(value) {
1019
+ return value.replace(/\.(ts|tsx|mts|cts|js|mjs|cjs)$/i, "");
1020
+ }
1021
+ /**
1022
+ * Resolve a directory path to runtime output when the source path is unavailable.
1023
+ *
1024
+ * @param directoryPath
1025
+ * @returns
1026
+ */
1027
+ resolveRuntimeDirectoryPath(directoryPath) {
1028
+ if (existsSync(directoryPath)) return directoryPath;
1029
+ const { buildOutput } = this.getConfig("paths") || {};
1030
+ if (typeof buildOutput !== "string" || buildOutput.trim().length === 0) return directoryPath;
1031
+ const relativeSource = relative(process.cwd(), directoryPath);
1032
+ if (!relativeSource || relativeSource.startsWith("..")) return directoryPath;
1033
+ const mappedDirectory = join(buildOutput, relativeSource);
1034
+ return existsSync(mappedDirectory) ? mappedDirectory : directoryPath;
1035
+ }
1036
+ /**
1037
+ * Resolve a script file path for runtime execution.
1038
+ * If a .ts file is provided, tries equivalent .js/.cjs/.mjs files first.
1039
+ * Also attempts mapped paths inside paths.buildOutput preserving structure.
1040
+ *
1041
+ * @param filePath
1042
+ * @returns
1043
+ */
1044
+ resolveRuntimeScriptPath(filePath) {
1045
+ const extension = extname(filePath).toLowerCase();
1046
+ const isTsFile = extension === ".ts" || extension === ".mts" || extension === ".cts";
1047
+ const candidates = [];
1048
+ if (isTsFile) {
1049
+ const base = filePath.slice(0, -extension.length);
1050
+ candidates.push(`${base}.js`, `${base}.cjs`, `${base}.mjs`);
1051
+ }
1052
+ const { buildOutput } = this.getConfig("paths") ?? {};
1053
+ if (typeof buildOutput === "string" && buildOutput.trim().length > 0) {
1054
+ const relativeSource = relative(process.cwd(), filePath);
1055
+ if (relativeSource && !relativeSource.startsWith("..")) {
1056
+ const mappedFile = join(buildOutput, relativeSource);
1057
+ const mappedExtension = extname(mappedFile).toLowerCase();
1058
+ if (mappedExtension === ".ts" || mappedExtension === ".mts" || mappedExtension === ".cts") {
1059
+ const mappedBase = mappedFile.slice(0, -mappedExtension.length);
1060
+ candidates.push(`${mappedBase}.js`, `${mappedBase}.cjs`, `${mappedBase}.mjs`);
1061
+ } else candidates.push(mappedFile);
1062
+ }
1063
+ }
1064
+ const runtimeMatch = candidates.find((path) => existsSync(path));
1065
+ if (runtimeMatch) return runtimeMatch;
1066
+ return filePath;
1067
+ }
1068
+ /**
1069
+ * Utility to generate file from stub
1070
+ *
1071
+ * @param stubPath
1072
+ * @param outputPath
1073
+ * @param replacements
1074
+ */
1075
+ generateFile(stubPath, outputPath, replacements, options) {
1076
+ if (existsSync(outputPath) && !options?.force) {
1077
+ this.command.error(`Error: ${this.formatPathForLog(outputPath)} already exists.`);
1078
+ process.exit(1);
1079
+ } else if (existsSync(outputPath) && options?.force) rmSync(outputPath);
1080
+ let content = readFileSync(stubPath, "utf-8");
1081
+ for (const [key, value] of Object.entries(replacements)) content = content.replace(new RegExp(`{{${key}}}`, "g"), value);
1082
+ this.ensureDirectory(outputPath);
1083
+ writeFileSync(outputPath, content);
1084
+ return outputPath;
1085
+ }
1086
+ /**
1087
+ * Resolve a configuration path with a fallback default
1088
+ *
1089
+ * @param key The configuration key to resolve
1090
+ * @param fallback The fallback value if the configuration key is not set
1091
+ * @returns The resolved configuration path
1092
+ */
1093
+ resolveConfigPath(key, fallback) {
1094
+ const { [key]: configured } = this.getConfig("paths") ?? {};
1095
+ if (typeof configured === "string" && configured.trim().length > 0) return configured;
1096
+ return fallback;
1097
+ }
1098
+ /**
1099
+ * Resolve the path to a stub file based on configuration
1100
+ *
1101
+ * @param stubName
1102
+ * @returns
1103
+ */
1104
+ resolveStubPath(stubName) {
1105
+ return join(this.resolveConfigPath("stubs", getDefaultStubsPath()), stubName);
1106
+ }
1107
+ /**
1108
+ * Generate a factory file for a given model name.
1109
+ *
1110
+ * @param name
1111
+ * @param options
1112
+ * @returns
1113
+ */
1114
+ makeFactory(name, options = {}) {
1115
+ const baseName = str(name.replace(/Factory$/, "")).pascal();
1116
+ const factoryName = `${baseName}Factory`;
1117
+ const modelName = options.modelName ? str(options.modelName).pascal() : baseName;
1118
+ const outputExt = this.resolveOutputExt();
1119
+ const outputPath = join(this.resolveConfigPath("factories", join(process.cwd(), "database", "factories")), `${factoryName}.${outputExt}`);
1120
+ const modelPath = join(this.resolveConfigPath("models", join(process.cwd(), "src", "models")), `${modelName}.${outputExt}`);
1121
+ const relativeImport = options.modelImportPath ?? `./${this.stripKnownSourceExtension(relative(dirname(outputPath), modelPath).replace(/\\/g, "/"))}${outputExt === "js" ? ".js" : ""}`;
1122
+ const stubPath = this.resolveStubPath(outputExt === "js" ? "factory.js.stub" : "factory.stub");
1123
+ return {
1124
+ name: factoryName,
1125
+ path: this.generateFile(stubPath, outputPath, {
1126
+ FactoryName: factoryName,
1127
+ ModelName: modelName.toString(),
1128
+ ModelImportPath: relativeImport.startsWith(".") ? relativeImport : `./${relativeImport}`
1129
+ }, options)
1130
+ };
1131
+ }
1132
+ /**
1133
+ * Generate a seeder file for a given name.
1134
+ *
1135
+ * @param name
1136
+ * @param options
1137
+ * @returns
1138
+ */
1139
+ makeSeeder(name, options = {}) {
1140
+ const seederName = `${str(name.replace(/Seeder$/, "")).pascal()}Seeder`;
1141
+ const outputExt = this.resolveOutputExt();
1142
+ const outputPath = join(this.resolveConfigPath("seeders", join(process.cwd(), "database", "seeders")), `${seederName}.${outputExt}`);
1143
+ const stubPath = this.resolveStubPath(outputExt === "js" ? "seeder.js.stub" : "seeder.stub");
1144
+ return {
1145
+ name: seederName,
1146
+ path: this.generateFile(stubPath, outputPath, { SeederName: seederName }, options)
1147
+ };
1148
+ }
1149
+ /**
1150
+ * Generate a migration file for a given name.
1151
+ *
1152
+ * @param name The name of the migration.
1153
+ * @returns An object containing the name and path of the generated migration file.
1154
+ */
1155
+ makeMigration(name) {
1156
+ const generated = generateMigrationFile(name, {
1157
+ directory: this.resolveConfigPath("migrations", join(process.cwd(), "database", "migrations")),
1158
+ extension: this.resolveOutputExt()
1159
+ });
1160
+ return {
1161
+ name: generated.className,
1162
+ path: generated.filePath
1163
+ };
1164
+ }
1165
+ /**
1166
+ * Generate a model file along with optional factory, seeder, and migration files.
1167
+ *
1168
+ * @param name
1169
+ * @param options
1170
+ * @returns
1171
+ */
1172
+ makeModel(name, options = {}) {
1173
+ const baseName = str(name.replace(/Model$/, "")).pascal().toString();
1174
+ const modelName = `${baseName}`;
1175
+ const delegateName = str(baseName).camel().plural().toString();
1176
+ const outputExt = this.resolveOutputExt();
1177
+ const outputPath = join(this.resolveConfigPath("models", join(process.cwd(), "src", "models")), `${modelName}.${outputExt}`);
1178
+ const shouldBuildFactory = options.all || options.factory;
1179
+ const shouldBuildSeeder = options.all || options.seeder;
1180
+ const shouldBuildMigration = options.all || options.migration;
1181
+ const factoryName = `${baseName}Factory`;
1182
+ const factoryPath = join(this.resolveConfigPath("factories", join(process.cwd(), "database", "factories")), `${factoryName}.${outputExt}`);
1183
+ const factoryImportPath = `./${relative(dirname(outputPath), factoryPath).replace(/\\/g, "/").replace(/\.(ts|tsx|mts|cts|js|mjs|cjs)$/i, "")}${outputExt === "js" ? ".js" : ""}`;
1184
+ const stubPath = this.resolveStubPath(outputExt === "js" ? "model.js.stub" : "model.stub");
1185
+ const modelPath = this.generateFile(stubPath, outputPath, {
1186
+ ModelName: modelName,
1187
+ DelegateName: delegateName,
1188
+ FactoryImport: shouldBuildFactory ? `import { ${factoryName} } from '${factoryImportPath}'\n` : "",
1189
+ FactoryLink: shouldBuildFactory ? outputExt === "js" ? `\n static factoryClass = ${factoryName}` : `\n protected static override factoryClass = ${factoryName}` : ""
1190
+ }, options);
1191
+ const prisma = this.ensurePrismaModelEntry(modelName, delegateName);
1192
+ const created = {
1193
+ model: {
1194
+ name: modelName,
1195
+ path: modelPath
1196
+ },
1197
+ prisma,
1198
+ factory: void 0,
1199
+ seeder: void 0,
1200
+ migration: void 0
1201
+ };
1202
+ if (shouldBuildFactory) created.factory = this.makeFactory(baseName, {
1203
+ force: options.force,
1204
+ modelName,
1205
+ modelImportPath: `./${relative(dirname(factoryPath), outputPath).replace(/\\/g, "/").replace(/\.(ts|tsx|mts|cts|js|mjs|cjs)$/i, "")}${outputExt === "js" ? ".js" : ""}`
1206
+ });
1207
+ if (shouldBuildSeeder) created.seeder = this.makeSeeder(baseName, { force: options.force });
1208
+ if (shouldBuildMigration) created.migration = this.makeMigration(`create ${delegateName} table`);
1209
+ return created;
1210
+ }
1211
+ /**
1212
+ * Ensure that the Prisma schema has a model entry for the given model
1213
+ * and delegate names.
1214
+ * If the entry does not exist, it will be created with a default `id` field.
1215
+ *
1216
+ * @param modelName The name of the model to ensure in the Prisma schema.
1217
+ * @param delegateName The name of the delegate (table) to ensure in the Prisma schema.
1218
+ */
1219
+ ensurePrismaModelEntry(modelName, delegateName) {
1220
+ const schemaPath = join(process.cwd(), "prisma", "schema.prisma");
1221
+ if (!existsSync(schemaPath)) return {
1222
+ path: schemaPath,
1223
+ updated: false
1224
+ };
1225
+ const source = readFileSync(schemaPath, "utf-8");
1226
+ const existingByTable = findModelBlock(source, delegateName);
1227
+ const existingByName = new RegExp(`model\\s+${modelName}\\s*\\{`, "m").test(source);
1228
+ if (existingByTable || existingByName) return {
1229
+ path: schemaPath,
1230
+ updated: false
1231
+ };
1232
+ writeFileSync(schemaPath, applyCreateTableOperation(source, {
1233
+ type: "createTable",
1234
+ table: delegateName,
1235
+ columns: [{
1236
+ name: "id",
1237
+ type: "id",
1238
+ primary: true
1239
+ }]
1240
+ }));
1241
+ return {
1242
+ path: schemaPath,
1243
+ updated: true
1244
+ };
1245
+ }
1246
+ /**
1247
+ * Convert a Prisma scalar type to its corresponding TypeScript type.
1248
+ *
1249
+ * @param value The Prisma scalar type.
1250
+ * @returns The corresponding TypeScript type.
1251
+ */
1252
+ prismaTypeToTs(value) {
1253
+ if (value === "Int" || value === "Float" || value === "Decimal") return "number";
1254
+ if (value === "BigInt") return "bigint";
1255
+ if (value === "String") return "string";
1256
+ if (value === "Boolean") return "boolean";
1257
+ if (value === "DateTime") return "Date";
1258
+ if (value === "Json") return "Record<string, unknown>";
1259
+ if (value === "Bytes") return "Buffer";
1260
+ return "unknown";
1261
+ }
1262
+ /**
1263
+ * Parse the Prisma schema to extract model definitions and their fields, focusing
1264
+ * on scalar types.
1265
+ *
1266
+ * @param schema The Prisma schema as a string.
1267
+ * @returns An array of model definitions with their fields.
1268
+ */
1269
+ parsePrismaModels(schema) {
1270
+ const models = [];
1271
+ const modelRegex = /model\s+(\w+)\s*\{([\s\S]*?)\n\}/g;
1272
+ const scalarTypes = new Set([
1273
+ "Int",
1274
+ "Float",
1275
+ "Decimal",
1276
+ "BigInt",
1277
+ "String",
1278
+ "Boolean",
1279
+ "DateTime",
1280
+ "Json",
1281
+ "Bytes"
1282
+ ]);
1283
+ for (const match of schema.matchAll(modelRegex)) {
1284
+ const name = match[1];
1285
+ const body = match[2];
1286
+ const table = body.match(/@@map\("([^"]+)"\)/)?.[1] ?? `${name.charAt(0).toLowerCase()}${name.slice(1)}s`;
1287
+ const fields = [];
1288
+ body.split("\n").forEach((rawLine) => {
1289
+ const line = rawLine.trim();
1290
+ if (!line || line.startsWith("@@") || line.startsWith("//")) return;
1291
+ const fieldMatch = line.match(/^(\w+)\s+([A-Za-z]+)(\?)?\b/);
1292
+ if (!fieldMatch) return;
1293
+ const fieldType = fieldMatch[2];
1294
+ if (!scalarTypes.has(fieldType)) return;
1295
+ fields.push({
1296
+ name: fieldMatch[1],
1297
+ type: this.prismaTypeToTs(fieldType),
1298
+ optional: Boolean(fieldMatch[3])
1299
+ });
1300
+ });
1301
+ models.push({
1302
+ name,
1303
+ table,
1304
+ fields
1305
+ });
1306
+ }
1307
+ return models;
1308
+ }
1309
+ /**
1310
+ * Sync model attribute declarations in a model file based on the
1311
+ * provided declarations.
1312
+ * This method takes the source code of a model file and a list of
1313
+ * attribute declarations,
1314
+ *
1315
+ * @param modelSource The source code of the model file.
1316
+ * @param declarations A list of attribute declarations to sync.
1317
+ * @returns An object containing the updated content and a flag indicating if it was updated.
1318
+ */
1319
+ syncModelDeclarations(modelSource, declarations) {
1320
+ const lines = modelSource.split("\n");
1321
+ const classIndex = lines.findIndex((line) => /export\s+class\s+\w+\s+extends\s+Model<.+>\s*\{/.test(line));
1322
+ if (classIndex < 0) return {
1323
+ content: modelSource,
1324
+ updated: false
1325
+ };
1326
+ let classEndIndex = -1;
1327
+ let depth = 0;
1328
+ for (let index = classIndex; index < lines.length; index += 1) {
1329
+ const line = lines[index];
1330
+ depth += (line.match(/\{/g) || []).length;
1331
+ depth -= (line.match(/\}/g) || []).length;
1332
+ if (depth === 0) {
1333
+ classEndIndex = index;
1334
+ break;
1335
+ }
1336
+ }
1337
+ if (classEndIndex < 0) return {
1338
+ content: modelSource,
1339
+ updated: false
1340
+ };
1341
+ const withoutDeclares = lines.slice(classIndex + 1, classEndIndex).filter((line) => !/^\s*declare\s+\w+\??:\s*[^\n]+$/.test(line));
1342
+ const rebuiltClass = [...declarations.map((declaration) => ` ${declaration}`), ...withoutDeclares];
1343
+ const content = [
1344
+ ...lines.slice(0, classIndex + 1),
1345
+ ...rebuiltClass,
1346
+ ...lines.slice(classEndIndex)
1347
+ ].join("\n");
1348
+ return {
1349
+ content,
1350
+ updated: content !== modelSource
1351
+ };
1352
+ }
1353
+ /**
1354
+ * Sync model attribute declarations in model files based on the Prisma schema.
1355
+ * This method reads the Prisma schema to extract model definitions and their
1356
+ * scalar fields, then updates the corresponding model files to include `declare`
1357
+ * statements for these fields. It returns an object containing the paths of the
1358
+ * schema and models, the total number of model files processed, and lists of
1359
+ * updated and skipped files.
1360
+ *
1361
+ * @param options Optional parameters to specify custom paths for the Prisma schema and models directory.
1362
+ * @returns An object with details about the synchronization process, including updated and skipped files.
1363
+ */
1364
+ syncModelsFromPrisma(options = {}) {
1365
+ const schemaPath = options.schemaPath ?? join(process.cwd(), "prisma", "schema.prisma");
1366
+ const modelsDir = options.modelsDir ?? this.resolveConfigPath("models", join(process.cwd(), "src", "models"));
1367
+ if (!existsSync(schemaPath)) throw new Error(`Prisma schema file not found: ${schemaPath}`);
1368
+ if (!existsSync(modelsDir)) throw new Error(`Models directory not found: ${modelsDir}`);
1369
+ const schema = readFileSync(schemaPath, "utf-8");
1370
+ const prismaModels = this.parsePrismaModels(schema);
1371
+ const modelFiles = readdirSync(modelsDir).filter((file) => file.endsWith(".ts"));
1372
+ const updated = [];
1373
+ const skipped = [];
1374
+ modelFiles.forEach((file) => {
1375
+ const filePath = join(modelsDir, file);
1376
+ const source = readFileSync(filePath, "utf-8");
1377
+ const classMatch = source.match(/export\s+class\s+(\w+)\s+extends\s+Model<'([^']+)'>/);
1378
+ if (!classMatch) {
1379
+ skipped.push(filePath);
1380
+ return;
1381
+ }
1382
+ const className = classMatch[1];
1383
+ const delegate = classMatch[2];
1384
+ const prismaModel = prismaModels.find((model) => model.table === delegate) ?? prismaModels.find((model) => model.name === className);
1385
+ if (!prismaModel || prismaModel.fields.length === 0) {
1386
+ skipped.push(filePath);
1387
+ return;
1388
+ }
1389
+ const declarations = prismaModel.fields.map((field) => `declare ${field.name}${field.optional ? "?" : ""}: ${field.type}`);
1390
+ const synced = this.syncModelDeclarations(source, declarations);
1391
+ if (!synced.updated) {
1392
+ skipped.push(filePath);
1393
+ return;
1394
+ }
1395
+ writeFileSync(filePath, synced.content);
1396
+ updated.push(filePath);
1397
+ });
1398
+ return {
1399
+ schemaPath,
1400
+ modelsDir,
1401
+ total: modelFiles.length,
1402
+ updated,
1403
+ skipped
1404
+ };
1405
+ }
1406
+ };
1407
+
1408
+ //#endregion
1409
+ //#region src/cli/commands/InitCommand.ts
1410
+ /**
1411
+ * The InitCommand class implements the CLI command for initializing Arkormˣ by creating
1412
+ * a default config file in the current directory.
1413
+ *
1414
+ * @author Legacy (3m1n3nc3)
1415
+ * @since 0.1.0
1416
+ */
1417
+ var InitCommand = class extends Command {
1418
+ signature = `init
14
1419
  {--force : Force overwrite if config file already exists (existing file will be backed up) }
15
- `;description=`Initialize Arkormˣ by creating a default config file in the current directory`;async handle(){this.app.command=this;let n=v(process.cwd(),`arkormx.config.js`),{stubs:i}=X(`paths`)??{},a=typeof i==`string`&&i.trim().length>0?i:Q(),s=v(a,`arkormx.config.stub`),c=v(a,`arkorm.config.stub`),l=t(s)?s:c;t(n)&&!this.option(`force`)&&(this.error(`Error: Arkormˣ has already been initialized. Use --force to reinitialize.`),process.exit(1)),this.app.ensureDirectory(n),t(n)&&this.option(`force`)&&e(n,n.replace(/\.js$/,`.backup.${Date.now()}.js`)),t(l)||(this.error(`Error: Missing config stub at ${s} (or ${c})`),process.exit(1)),o(n,r(l,`utf-8`)),this.success(`Arkormˣ initialized successfully!`)}},he=class extends S{signature=`make:factory
1420
+ `;
1421
+ description = "Initialize Arkormˣ by creating a default config file in the current directory";
1422
+ /**
1423
+ * Command handler for the init command.
1424
+ */
1425
+ async handle() {
1426
+ this.app.command = this;
1427
+ const outputDir = join$1(process.cwd(), "arkormx.config.js");
1428
+ const { stubs } = getUserConfig("paths") ?? {};
1429
+ const stubsDir = typeof stubs === "string" && stubs.trim().length > 0 ? stubs : getDefaultStubsPath();
1430
+ const preferredStubPath = join$1(stubsDir, "arkormx.config.stub");
1431
+ const legacyStubPath = join$1(stubsDir, "arkorm.config.stub");
1432
+ const stubPath = existsSync(preferredStubPath) ? preferredStubPath : legacyStubPath;
1433
+ if (existsSync(outputDir) && !this.option("force")) {
1434
+ this.error("Error: Arkormˣ has already been initialized. Use --force to reinitialize.");
1435
+ process.exit(1);
1436
+ }
1437
+ this.app.ensureDirectory(outputDir);
1438
+ if (existsSync(outputDir) && this.option("force")) copyFileSync(outputDir, outputDir.replace(/\.js$/, `.backup.${Date.now()}.js`));
1439
+ if (!existsSync(stubPath)) {
1440
+ this.error(`Error: Missing config stub at ${preferredStubPath} (or ${legacyStubPath})`);
1441
+ process.exit(1);
1442
+ }
1443
+ writeFileSync(outputDir, readFileSync(stubPath, "utf-8"));
1444
+ this.success("Arkormˣ initialized successfully!");
1445
+ }
1446
+ };
1447
+
1448
+ //#endregion
1449
+ //#region src/cli/commands/MakeFactoryCommand.ts
1450
+ /**
1451
+ * The MakeFactoryCommand class implements the CLI command for creating new factory classes.
1452
+ *
1453
+ * @author Legacy (3m1n3nc3)
1454
+ * @since 0.1.0
1455
+ */
1456
+ var MakeFactoryCommand = class extends Command {
1457
+ signature = `make:factory
16
1458
  {name : Name of the factory to create}
17
1459
  {--f|force : Overwrite existing file}
18
- `;description=`Create a new model factory class`;async handle(){this.app.command=this;let e=this.argument(`name`);if(!e)return void this.error(`Error: Name argument is required.`);let t=this.app.makeFactory(e,{force:this.option(`force`)});this.success(`Created factory: ${this.app.formatPathForLog(t.path)}`)}},ge=class extends S{signature=`make:migration
1460
+ `;
1461
+ description = "Create a new model factory class";
1462
+ /**
1463
+ * Command handler for the make:factory command.
1464
+ *
1465
+ * @returns
1466
+ */
1467
+ async handle() {
1468
+ this.app.command = this;
1469
+ const name = this.argument("name");
1470
+ if (!name) return void this.error("Error: Name argument is required.");
1471
+ const created = this.app.makeFactory(name, { force: this.option("force") });
1472
+ this.success(`Created factory: ${this.app.formatPathForLog(created.path)}`);
1473
+ }
1474
+ };
1475
+
1476
+ //#endregion
1477
+ //#region src/cli/commands/MakeMigrationCommand.ts
1478
+ /**
1479
+ * The MakeMigrationCommand class implements the CLI command for creating new migration classes.
1480
+ *
1481
+ * @author Legacy (3m1n3nc3)
1482
+ * @since 0.1.0
1483
+ */
1484
+ var MakeMigrationCommand = class extends Command {
1485
+ signature = `make:migration
19
1486
  {name : Name of the migration to create}
20
- `;description=`Create a new migration class file`;async handle(){this.app.command=this;let e=this.argument(`name`);if(!e)return void this.error(`Error: Name argument is required.`);let t=this.app.makeMigration(e);this.success(`Created migration: ${this.app.formatPathForLog(t.path)}`)}},_e=class extends S{signature=`make:model
1487
+ `;
1488
+ description = "Create a new migration class file";
1489
+ /**
1490
+ * Command handler for the make:migration command.
1491
+ *
1492
+ * @returns
1493
+ */
1494
+ async handle() {
1495
+ this.app.command = this;
1496
+ const name = this.argument("name");
1497
+ if (!name) return void this.error("Error: Name argument is required.");
1498
+ const created = this.app.makeMigration(name);
1499
+ this.success(`Created migration: ${this.app.formatPathForLog(created.path)}`);
1500
+ }
1501
+ };
1502
+
1503
+ //#endregion
1504
+ //#region src/cli/commands/MakeModelCommand.ts
1505
+ /**
1506
+ * The MakeModelCommand class implements the CLI command for creating new model
1507
+ * classes along with optional linked resources such as factories, seeders, and migrations.
1508
+ *
1509
+ * @author Legacy (3m1n3nc3)
1510
+ * @since 0.1.0
1511
+ */
1512
+ var MakeModelCommand = class extends Command {
1513
+ signature = `make:model
21
1514
  {name : Name of the model to create}
22
1515
  {--f|force : Overwrite existing files}
23
1516
  {--factory : Create and link a factory}
24
1517
  {--seeder : Create a seeder}
25
1518
  {--migration : Create a migration}
26
1519
  {--all : Create and link factory, seeder, and migration}
27
- `;description=`Create a new model and optional linked resources`;async handle(){this.app.command=this;let e=this.argument(`name`);if(!e)return void this.error(`Error: Name argument is required.`);let t=this.app.makeModel(e,this.options());this.success(`Created files:`),[[`Model`,t.model.path],[`Prisma schema ${t.prisma.updated?`(updated)`:`(already up to date)`}`,t.prisma.path],t.factory?[`Factory`,t.factory.path]:``,t.seeder?[`Seeder`,t.seeder.path]:``,t.migration?[`Migration`,t.migration.path]:``].filter(Boolean).map(([e,t])=>this.success(this.app.splitLogger(e,t)))}},ve=class extends S{signature=`make:seeder
1520
+ `;
1521
+ description = "Create a new model and optional linked resources";
1522
+ /**
1523
+ * Command handler for the make:model command.
1524
+ *
1525
+ * @returns
1526
+ */
1527
+ async handle() {
1528
+ this.app.command = this;
1529
+ const name = this.argument("name");
1530
+ if (!name) return void this.error("Error: Name argument is required.");
1531
+ const created = this.app.makeModel(name, this.options());
1532
+ this.success("Created files:");
1533
+ [
1534
+ ["Model", created.model.path],
1535
+ [`Prisma schema ${created.prisma.updated ? "(updated)" : "(already up to date)"}`, created.prisma.path],
1536
+ created.factory ? ["Factory", created.factory.path] : "",
1537
+ created.seeder ? ["Seeder", created.seeder.path] : "",
1538
+ created.migration ? ["Migration", created.migration.path] : ""
1539
+ ].filter(Boolean).map(([name, path]) => this.success(this.app.splitLogger(name, path)));
1540
+ }
1541
+ };
1542
+
1543
+ //#endregion
1544
+ //#region src/cli/commands/MakeSeederCommand.ts
1545
+ /**
1546
+ * The MakeSeederCommand class implements the CLI command for creating new seeder classes.
1547
+ *
1548
+ * @author Legacy (3m1n3nc3)
1549
+ * @since 0.1.0
1550
+ */
1551
+ var MakeSeederCommand = class extends Command {
1552
+ signature = `make:seeder
28
1553
  {name : Name of the seeder to create}
29
1554
  {--f|force : Overwrite existing file}
30
- `;description=`Create a new seeder class`;async handle(){this.app.command=this;let e=this.argument(`name`);if(!e)return void this.error(`Error: Name argument is required.`);let t=this.app.makeSeeder(e,this.options());this.success(`Created seeder: ${this.app.formatPathForLog(t.path)}`)}},ye=class extends S{signature=`migrate
1555
+ `;
1556
+ description = "Create a new seeder class";
1557
+ /**
1558
+ * Command handler for the make:seeder command.
1559
+ */
1560
+ async handle() {
1561
+ this.app.command = this;
1562
+ const name = this.argument("name");
1563
+ if (!name) return void this.error("Error: Name argument is required.");
1564
+ const created = this.app.makeSeeder(name, this.options());
1565
+ this.success(`Created seeder: ${this.app.formatPathForLog(created.path)}`);
1566
+ }
1567
+ };
1568
+
1569
+ //#endregion
1570
+ //#region src/database/Migration.ts
1571
+ const MIGRATION_BRAND = Symbol.for("arkormx.migration");
1572
+ /**
1573
+ * The Migration class serves as a base for defining database migrations, requiring
1574
+ * the implementation of `up` and `down` methods to specify the changes to be
1575
+ * applied or reverted in the database schema.
1576
+ *
1577
+ * @author Legacy (3m1n3nc3)
1578
+ * @since 0.1.0
1579
+ */
1580
+ var Migration = class {
1581
+ static [MIGRATION_BRAND] = true;
1582
+ };
1583
+
1584
+ //#endregion
1585
+ //#region src/cli/commands/MigrateCommand.ts
1586
+ /**
1587
+ * The MigrateCommand class implements the CLI command for applying migration
1588
+ * classes to the Prisma schema and running the Prisma workflow.
1589
+ *
1590
+ * @author Legacy (3m1n3nc3)
1591
+ * @since 0.1.0
1592
+ */
1593
+ var MigrateCommand = class extends Command {
1594
+ signature = `migrate
31
1595
  {name? : Migration class or file name}
32
1596
  {--all : Run all migrations from the configured migrations directory}
33
1597
  {--deploy : Use prisma migrate deploy instead of migrate dev}
@@ -35,17 +1599,275 @@ import{copyFileSync as e,existsSync as t,mkdirSync as n,readFileSync as r,readdi
35
1599
  {--skip-migrate : Skip prisma migrate command}
36
1600
  {--schema= : Explicit prisma schema path}
37
1601
  {--migration-name= : Name for prisma migrate dev}
38
- `;description=`Apply migration classes to schema.prisma and run Prisma workflow`;async handle(){this.app.command=this;let e=this.app.getConfig(`paths`)?.migrations??v(process.cwd(),`database`,`migrations`),t=this.app.resolveRuntimeDirectoryPath(e);if(!p(t))return void this.error(`Error: Migrations directory not found: ${this.app.formatPathForLog(e)}`);let n=this.option(`schema`)?y(String(this.option(`schema`))):v(process.cwd(),`prisma`,`schema.prisma`),r=this.option(`all`)?await this.loadAllMigrations(t):(await this.loadNamedMigration(t,this.argument(`name`))).filter(([e])=>e!==void 0);if(r.length===0)return void this.error(`Error: No migration classes found to run.`);for(let[e]of r)await W(e,{schemaPath:n,write:!0});this.option(`skip-generate`)||I([`generate`],process.cwd()),this.option(`skip-migrate`)||(this.option(`deploy`)?I([`migrate`,`deploy`],process.cwd()):I([`migrate`,`dev`,`--name`,this.option(`migration-name`)?String(this.option(`migration-name`)):`arkorm_cli_${Date.now()}`],process.cwd())),this.success(`Applied ${r.length} migration(s).`),r.forEach(([e,t])=>this.success(this.app.splitLogger(`Migrated`,t)))}async loadAllMigrations(e){let t=g(e).filter(e=>/\.(ts|js|mjs|cjs)$/i.test(e)).sort((e,t)=>e.localeCompare(t)).map(t=>this.app.resolveRuntimeScriptPath(v(e,t)));return(await Promise.all(t.map(async e=>(await this.loadMigrationClassesFromFile(e)).map(t=>[t,e])))).flat()}async loadNamedMigration(e,t){if(!t)return[[void 0,``]];let n=t.replace(/Migration$/,``),r=[`${t}.ts`,`${t}.js`,`${t}.mjs`,`${t}.cjs`,`${n}Migration.ts`,`${n}Migration.js`,`${n}Migration.mjs`,`${n}Migration.cjs`].map(t=>v(e,t)).find(e=>p(e));if(!r)return[[void 0,t]];let i=this.app.resolveRuntimeScriptPath(r);return(await this.loadMigrationClassesFromFile(i)).map(e=>[e,i])}async loadMigrationClassesFromFile(e){let t=await import(`${w(y(e)).href}?arkorm_migrate=${Date.now()}`);return Object.values(t).filter(e=>typeof e==`function`?e.prototype instanceof E:!1)}},be=class extends S{signature=`models:sync
1602
+ `;
1603
+ description = "Apply migration classes to schema.prisma and run Prisma workflow";
1604
+ /**
1605
+ * Command handler for the migrate command.
1606
+ * This method is responsible for orchestrating the migration
1607
+ * process, including loading migration classes, applying them to
1608
+ * the Prisma schema, and running the appropriate Prisma commands
1609
+ * based on the provided options.
1610
+ *
1611
+ * @returns
1612
+ */
1613
+ async handle() {
1614
+ this.app.command = this;
1615
+ const configuredMigrationsDir = this.app.getConfig("paths")?.migrations ?? join$1(process.cwd(), "database", "migrations");
1616
+ const migrationsDir = this.app.resolveRuntimeDirectoryPath(configuredMigrationsDir);
1617
+ if (!existsSync$1(migrationsDir)) return void this.error(`Error: Migrations directory not found: ${this.app.formatPathForLog(configuredMigrationsDir)}`);
1618
+ const schemaPath = this.option("schema") ? resolve(String(this.option("schema"))) : join$1(process.cwd(), "prisma", "schema.prisma");
1619
+ const classes = this.option("all") ? await this.loadAllMigrations(migrationsDir) : (await this.loadNamedMigration(migrationsDir, this.argument("name"))).filter(([cls]) => cls !== void 0);
1620
+ if (classes.length === 0) return void this.error("Error: No migration classes found to run.");
1621
+ for (const [MigrationClassItem] of classes) await applyMigrationToPrismaSchema(MigrationClassItem, {
1622
+ schemaPath,
1623
+ write: true
1624
+ });
1625
+ if (!this.option("skip-generate")) runPrismaCommand(["generate"], process.cwd());
1626
+ if (!this.option("skip-migrate")) if (this.option("deploy")) runPrismaCommand(["migrate", "deploy"], process.cwd());
1627
+ else runPrismaCommand([
1628
+ "migrate",
1629
+ "dev",
1630
+ "--name",
1631
+ this.option("migration-name") ? String(this.option("migration-name")) : `arkorm_cli_${Date.now()}`
1632
+ ], process.cwd());
1633
+ this.success(`Applied ${classes.length} migration(s).`);
1634
+ classes.forEach(([_, file]) => this.success(this.app.splitLogger("Migrated", file)));
1635
+ }
1636
+ /**
1637
+ * Load all migration classes from the specified directory.
1638
+ *
1639
+ * @param migrationsDir The directory to load migration classes from.
1640
+ */
1641
+ async loadAllMigrations(migrationsDir) {
1642
+ const files = readdirSync$1(migrationsDir).filter((file) => /\.(ts|js|mjs|cjs)$/i.test(file)).sort((left, right) => left.localeCompare(right)).map((file) => this.app.resolveRuntimeScriptPath(join$1(migrationsDir, file)));
1643
+ return (await Promise.all(files.map(async (file) => (await this.loadMigrationClassesFromFile(file)).map((cls) => [cls, file])))).flat();
1644
+ }
1645
+ /**
1646
+ * Load migration classes from a specific file or by class name.
1647
+ *
1648
+ * @param migrationsDir
1649
+ * @param name
1650
+ * @returns
1651
+ */
1652
+ async loadNamedMigration(migrationsDir, name) {
1653
+ if (!name) return [[void 0, ""]];
1654
+ const base = name.replace(/Migration$/, "");
1655
+ const target = [
1656
+ `${name}.ts`,
1657
+ `${name}.js`,
1658
+ `${name}.mjs`,
1659
+ `${name}.cjs`,
1660
+ `${base}Migration.ts`,
1661
+ `${base}Migration.js`,
1662
+ `${base}Migration.mjs`,
1663
+ `${base}Migration.cjs`
1664
+ ].map((file) => join$1(migrationsDir, file)).find((file) => existsSync$1(file));
1665
+ if (!target) return [[void 0, name]];
1666
+ const runtimeTarget = this.app.resolveRuntimeScriptPath(target);
1667
+ return (await this.loadMigrationClassesFromFile(runtimeTarget)).map((cls) => [cls, runtimeTarget]);
1668
+ }
1669
+ /**
1670
+ * Load migration classes from a given file path.
1671
+ *
1672
+ * @param filePath
1673
+ * @returns
1674
+ */
1675
+ async loadMigrationClassesFromFile(filePath) {
1676
+ const imported = await import(`${pathToFileURL$1(resolve(filePath)).href}?arkorm_migrate=${Date.now()}`);
1677
+ return Object.values(imported).filter((value) => {
1678
+ if (typeof value !== "function") return false;
1679
+ const candidate = value;
1680
+ const prototype = candidate.prototype;
1681
+ return candidate[MIGRATION_BRAND] === true || typeof prototype?.up === "function" && typeof prototype?.down === "function";
1682
+ });
1683
+ }
1684
+ };
1685
+
1686
+ //#endregion
1687
+ //#region src/cli/commands/ModelsSyncCommand.ts
1688
+ var ModelsSyncCommand = class extends Command {
1689
+ signature = `models:sync
39
1690
  {--schema= : Path to prisma schema file}
40
1691
  {--models= : Path to models directory}
41
- `;description=`Sync model declare attributes from prisma schema for all model files`;async handle(){this.app.command=this;let e=this.app.syncModelsFromPrisma({schemaPath:this.option(`schema`)?y(String(this.option(`schema`))):void 0,modelsDir:this.option(`models`)?y(String(this.option(`models`))):void 0}),t=e.updated.length===0?[this.app.splitLogger(`Updated`,`none`)]:e.updated.map(e=>this.app.splitLogger(`Updated`,e));this.success(`SUCCESS: Model sync completed with the following results:`),[this.app.splitLogger(`Schema`,e.schemaPath),this.app.splitLogger(`Models`,e.modelsDir),this.app.splitLogger(`Processed`,String(e.total)),...t,this.app.splitLogger(`Skipped`,String(e.skipped.length))].map(e=>this.success(e))}},xe=class e{async call(...t){await e.runSeeders(...t)}static toSeederInstance(t){return t instanceof e?t:new t}static async runSeeders(...e){let t=e.reduce((e,t)=>Array.isArray(t)?[...e,...t]:(e.push(t),e),[]);for(let e of t)await this.toSeederInstance(e).run()}},Se=class extends S{signature=`seed
1692
+ `;
1693
+ description = "Sync model declare attributes from prisma schema for all model files";
1694
+ async handle() {
1695
+ this.app.command = this;
1696
+ const result = this.app.syncModelsFromPrisma({
1697
+ schemaPath: this.option("schema") ? resolve(String(this.option("schema"))) : void 0,
1698
+ modelsDir: this.option("models") ? resolve(String(this.option("models"))) : void 0
1699
+ });
1700
+ const updatedLines = result.updated.length === 0 ? [this.app.splitLogger("Updated", "none")] : result.updated.map((path) => this.app.splitLogger("Updated", path));
1701
+ this.success("SUCCESS: Model sync completed with the following results:");
1702
+ [
1703
+ this.app.splitLogger("Schema", result.schemaPath),
1704
+ this.app.splitLogger("Models", result.modelsDir),
1705
+ this.app.splitLogger("Processed", String(result.total)),
1706
+ ...updatedLines,
1707
+ this.app.splitLogger("Skipped", String(result.skipped.length))
1708
+ ].map((line) => this.success(line));
1709
+ }
1710
+ };
1711
+
1712
+ //#endregion
1713
+ //#region src/database/Seeder.ts
1714
+ const SEEDER_BRAND = Symbol.for("arkormx.seeder");
1715
+ /**
1716
+ * The Seeder class serves as a base for defining database seeders, which are
1717
+ * used to populate the database with initial or test data.
1718
+ *
1719
+ * @author Legacy (3m1n3nc3)
1720
+ * @since 0.1.0
1721
+ */
1722
+ var Seeder = class Seeder {
1723
+ static [SEEDER_BRAND] = true;
1724
+ /**
1725
+ * Runs one or more seeders.
1726
+ *
1727
+ * @param seeders The seeders to be run.
1728
+ */
1729
+ async call(...seeders) {
1730
+ await Seeder.runSeeders(...seeders);
1731
+ }
1732
+ /**
1733
+ * Converts a SeederInput into a Seeder instance.
1734
+ *
1735
+ * @param input The SeederInput to convert.
1736
+ * @returns A Seeder instance.
1737
+ */
1738
+ static toSeederInstance(input) {
1739
+ if (typeof input === "function") return new input();
1740
+ if (typeof input === "object" && input !== null) {
1741
+ if (typeof input.run === "function") return input;
1742
+ }
1743
+ return input;
1744
+ }
1745
+ /**
1746
+ * Runs the given seeders in sequence.
1747
+ *
1748
+ * @param seeders The seeders to be run.
1749
+ */
1750
+ static async runSeeders(...seeders) {
1751
+ const queue = seeders.reduce((all, current) => {
1752
+ if (Array.isArray(current)) return [...all, ...current];
1753
+ all.push(current);
1754
+ return all;
1755
+ }, []);
1756
+ for (const seeder of queue) await this.toSeederInstance(seeder).run();
1757
+ }
1758
+ };
1759
+
1760
+ //#endregion
1761
+ //#region src/cli/commands/SeedCommand.ts
1762
+ /**
1763
+ * The SeedCommand class implements the CLI command for running seeder classes.
1764
+ *
1765
+ * @author Legacy (3m1n3nc3)
1766
+ * @since 0.1.0
1767
+ */
1768
+ var SeedCommand = class extends Command {
1769
+ signature = `seed
42
1770
  {name? : Seeder class or file name}
43
1771
  {--all : Run all seeders in the configured seeders directory}
44
- `;description=`Run one or more seeders`;async handle(){this.app.command=this;let e=this.app.getConfig(`paths`)?.seeders??v(process.cwd(),`database`,`seeders`),t=this.app.resolveRuntimeDirectoryPath(e);if(!p(t))return void this.error(`ERROR: Seeders directory not found: ${this.app.formatPathForLog(e)}`);let n=this.option(`all`)?await this.loadAllSeeders(t):await this.loadNamedSeeder(t,this.argument(`name`)??`DatabaseSeeder`);if(n.length===0)return void this.error(`ERROR: No seeder classes found to run.`);for(let e of n)await new e().run();this.success(`Database seeding completed`),n.forEach(e=>this.success(this.app.splitLogger(`Seeded`,e.name)))}async loadAllSeeders(e){let t=g(e).filter(e=>/\.(ts|js|mjs|cjs)$/i.test(e)).map(t=>this.app.resolveRuntimeScriptPath(v(e,t)));return(await Promise.all(t.map(async e=>await this.loadSeederClassesFromFile(e)))).flat()}async loadNamedSeeder(e,t){let n=t.replace(/Seeder$/,``),r=[`${t}.ts`,`${t}.js`,`${t}.mjs`,`${t}.cjs`,`${n}Seeder.ts`,`${n}Seeder.js`,`${n}Seeder.mjs`,`${n}Seeder.cjs`].map(t=>v(e,t)).find(e=>p(e));if(!r)return[];let i=this.app.resolveRuntimeScriptPath(r);return await this.loadSeederClassesFromFile(i)}async loadSeederClassesFromFile(e){let t=await import(`${w(y(e)).href}?arkorm_seed=${Date.now()}`);return Object.values(t).filter(e=>typeof e==`function`?e.prototype instanceof xe:!1)}},Ce=String.raw`
1772
+ `;
1773
+ description = "Run one or more seeders";
1774
+ /**
1775
+ * Command handler for the seed command.
1776
+ *
1777
+ * @returns
1778
+ */
1779
+ async handle() {
1780
+ this.app.command = this;
1781
+ const configuredSeedersDir = this.app.getConfig("paths")?.seeders ?? join$1(process.cwd(), "database", "seeders");
1782
+ const seedersDir = this.app.resolveRuntimeDirectoryPath(configuredSeedersDir);
1783
+ if (!existsSync$1(seedersDir)) return void this.error(`ERROR: Seeders directory not found: ${this.app.formatPathForLog(configuredSeedersDir)}`);
1784
+ const classes = this.option("all") ? await this.loadAllSeeders(seedersDir) : await this.loadNamedSeeder(seedersDir, this.argument("name") ?? "DatabaseSeeder");
1785
+ if (classes.length === 0) return void this.error("ERROR: No seeder classes found to run.");
1786
+ for (const SeederClassItem of classes) await new SeederClassItem().run();
1787
+ this.success("Database seeding completed");
1788
+ classes.forEach((cls) => this.success(this.app.splitLogger("Seeded", cls.name)));
1789
+ }
1790
+ /**
1791
+ * Load all seeder classes from the specified directory.
1792
+ *
1793
+ * @param seedersDir
1794
+ * @returns
1795
+ */
1796
+ async loadAllSeeders(seedersDir) {
1797
+ const files = readdirSync$1(seedersDir).filter((file) => /\.(ts|js|mjs|cjs)$/i.test(file)).map((file) => this.app.resolveRuntimeScriptPath(join$1(seedersDir, file)));
1798
+ return (await Promise.all(files.map(async (file) => await this.loadSeederClassesFromFile(file)))).flat();
1799
+ }
1800
+ /**
1801
+ * Load seeder classes from a specific file or by class name.
1802
+ *
1803
+ * @param seedersDir
1804
+ * @param name
1805
+ * @returns
1806
+ */
1807
+ async loadNamedSeeder(seedersDir, name) {
1808
+ const base = name.replace(/Seeder$/, "");
1809
+ const target = [
1810
+ `${name}.ts`,
1811
+ `${name}.js`,
1812
+ `${name}.mjs`,
1813
+ `${name}.cjs`,
1814
+ `${base}Seeder.ts`,
1815
+ `${base}Seeder.js`,
1816
+ `${base}Seeder.mjs`,
1817
+ `${base}Seeder.cjs`
1818
+ ].map((file) => join$1(seedersDir, file)).find((file) => existsSync$1(file));
1819
+ if (!target) return [];
1820
+ const runtimeTarget = this.app.resolveRuntimeScriptPath(target);
1821
+ return await this.loadSeederClassesFromFile(runtimeTarget);
1822
+ }
1823
+ /**
1824
+ * Load seeder classes from a given file path.
1825
+ *
1826
+ * @param filePath The path to the file containing seeder classes.
1827
+ * @returns An array of seeder classes.
1828
+ */
1829
+ async loadSeederClassesFromFile(filePath) {
1830
+ const imported = await import(`${pathToFileURL$1(resolve(filePath)).href}?arkorm_seed=${Date.now()}`);
1831
+ return Object.values(imported).filter((value) => {
1832
+ if (typeof value !== "function") return false;
1833
+ const candidate = value;
1834
+ const prototype = candidate.prototype;
1835
+ return candidate[SEEDER_BRAND] === true || typeof prototype?.run === "function";
1836
+ });
1837
+ }
1838
+ };
1839
+
1840
+ //#endregion
1841
+ //#region src/cli/logo.ts
1842
+ var logo_default = String.raw`
45
1843
  __/^^^^^^^^^^^^^^^^\__
46
1844
  ▄▄▄/ \▄▄
47
1845
  ▄██▀▀██▄ ▄▄
48
1846
  ███ ███ ████▄ ██ ▄█▀ ▄███▄ ████▄ ███▄███▄
49
1847
  ███▀▀███ ██ ▀▀ ████ ██ ██ ██ ▀▀ ██ ██ ██
50
1848
  ███ ███ ██ ██ ▀█▄ ▀███▀ ██ ██ ██ ██
51
- `;const we=new me;await C.init(we,{logo:Ce,name:`Arkormˣ CLI`,baseCommands:[$,_e,he,ve,ge,be,Se,ye],exceptionHandler(e){throw e}});export{};
1849
+ `;
1850
+
1851
+ //#endregion
1852
+ //#region src/cli/index.ts
1853
+ const app = new CliApp();
1854
+ await Kernel.init(app, {
1855
+ logo: logo_default,
1856
+ name: "Arkormˣ CLI",
1857
+ baseCommands: [
1858
+ InitCommand,
1859
+ MakeModelCommand,
1860
+ MakeFactoryCommand,
1861
+ MakeSeederCommand,
1862
+ MakeMigrationCommand,
1863
+ ModelsSyncCommand,
1864
+ SeedCommand,
1865
+ MigrateCommand
1866
+ ],
1867
+ exceptionHandler(exception) {
1868
+ throw exception;
1869
+ }
1870
+ });
1871
+
1872
+ //#endregion
1873
+ export { };