@stacksjs/buddy 0.70.270 → 0.70.275

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.
@@ -1,4 +1,19 @@
1
1
  import { confirm } from '@stacksjs/cli';
2
2
  import type { CLI } from '@stacksjs/types';
3
+ /**
4
+ * Refuse to replay a migration corpus against a database it was not written
5
+ * for.
6
+ *
7
+ * Stacks ships ONE set of migration files under `database/migrations`, emitted
8
+ * for a single dialect, and `buddy new` copies them verbatim into every new
9
+ * project. Setting DB_CONNECTION=postgres therefore used to fail with a raw
10
+ * `syntax error at or near "AUTOINCREMENT"` on file 1 of 121, which says
11
+ * nothing about the actual cause.
12
+ *
13
+ * Checked in the parent, before the migrate:fresh drop confirmation and before
14
+ * any action subprocess starts, so a mismatch cannot drop the framework tables
15
+ * and only then discover it has nothing to rebuild them with.
16
+ */
17
+ export declare function validateMigrationDialect(cwd?: unknown): { valid: boolean, error?: string };
3
18
  export declare function migrate(buddy: CLI): void;
4
19
  declare type FreshGuard = 'allow' | 'confirm' | 'disabled';
@@ -1,4 +1,4 @@
1
- import{closeSync,existsSync,mkdirSync,openSync,readdirSync,readFileSync,rmSync}from"node:fs";import process from"node:process";import{confirm,intro,log,onUnknownSubcommand,outro,text}from"@stacksjs/cli";import{Action}from"@stacksjs/enums";import{hasTTY,isCI}from"@stacksjs/env";import{appPath,frameworkPath,frameworkRuntimePath,projectPath}from"@stacksjs/path";import{ExitCode}from"@stacksjs/types";import{preflightDatabase}from"../database-preflight";import{DDL_CONSTRAINT_OVERRIDE_ENV,DIALECT_OVERRIDE_ENV,auditDdlConstraints,auditMigrationCorpus,dialectCapabilities,formatDdlConstraintError,formatMigrationDialectError,stripSqlNoise}from"@stacksjs/database";import{resultFailed}from"../result";let _runAction;async function runAction(...args){if(!_runAction)_runAction=(await import("@stacksjs/actions")).runAction;return _runAction(...args)}function acquireMigrationLock(){const lockDir=frameworkRuntimePath(),lockFile=`${lockDir}/migrations.lock`;try{if(!existsSync(lockDir))mkdirSync(lockDir,{recursive:!0});const fd=openSync(lockFile,"wx");try{closeSync(fd)}catch{}return{acquired:!0,release:()=>{try{rmSync(lockFile,{force:!0})}catch{}}}}catch{return{acquired:!1,release:()=>{}}}}async function ensureDatabaseOrExit(){try{const{ensureDatabaseReady}=await import("@stacksjs/database");await ensureDatabaseReady()}catch(error){log.syncError(error instanceof Error?error.message:String(error));process.exit(ExitCode.FatalError)}}function readMigrateMarker(){const file=frameworkRuntimePath("last-migrate-result.json");if(!existsSync(file))return null;try{const raw=readFileSync(file,"utf8"),parsed=JSON.parse(raw),n=typeof parsed.appliedCount==="number"?parsed.appliedCount:null;if(n===null||!Number.isFinite(n))return null;return{appliedCount:Math.max(0,Math.floor(n))}}catch{return null}finally{try{rmSync(file,{force:!0})}catch{}}}function countModelFiles(dir){if(!existsSync(dir))return 0;let count=0;const entries=readdirSync(dir,{withFileTypes:!0});for(const entry of entries)if(entry.isDirectory())count+=countModelFiles(`${dir}/${entry.name}`);else if(entry.name.endsWith(".ts")&&!entry.name.startsWith(".")&&!entry.name.startsWith("index"))count++;return count}function validateModelsExist(){const userModelsPath=appPath("Models"),defaultModelsPath=frameworkPath("defaults/app/Models"),userModelCount=countModelFiles(userModelsPath),defaultModelCount=countModelFiles(defaultModelsPath);if(userModelCount===0&&defaultModelCount===0)return{valid:!1,error:"No models found. Please create models in app/Models or ensure framework defaults exist."};return{valid:!0}}function validateMigrationDialect(){const driver=String(process.env.DB_CONNECTION||"sqlite").toLowerCase(),dir=projectPath("database/migrations");if(process.env[DIALECT_OVERRIDE_ENV]!=="1"){const caps=dialectCapabilities(driver),target=caps.wire==="mysql"?"mysql":caps.wire==="postgres"?"postgres":"sqlite";if(driver==="sqlite"||driver==="postgres"||caps.wire==="mysql"){const audit=auditMigrationCorpus({dir,target});if(!audit.empty&&audit.incompatible.length>0)return{valid:!1,error:formatMigrationDialectError(audit,target,"database/migrations")}}}if(process.env[DDL_CONSTRAINT_OVERRIDE_ENV]!=="1"){const constraints=auditDdlConstraints({dir,dialect:driver});if(!constraints.empty&&constraints.violations.length>0)return{valid:!1,error:formatDdlConstraintError(constraints,driver,"database/migrations")}}return{valid:!0}}async function reportMissingForeignKeys(){try{const{auditForeignKeys}=await import("@stacksjs/database"),result=await auditForeignKeys();if(result.missing.length===0)return;const sample=result.missing.slice(0,5).map((fk)=>` \u2022 ${fk.fromTable}.${fk.fromColumn} \u2192 ${fk.toTable}.${fk.toColumn} (${fk.model})`).join(`
1
+ import{closeSync,existsSync,mkdirSync,openSync,readdirSync,readFileSync,rmSync}from"node:fs";import process from"node:process";import{confirm,intro,log,onUnknownSubcommand,outro,text}from"@stacksjs/cli";import{Action}from"@stacksjs/enums";import{hasTTY,isCI}from"@stacksjs/env";import{appPath,frameworkPath,frameworkRuntimePath,projectPath}from"@stacksjs/path";import{ExitCode}from"@stacksjs/types";import{preflightDatabase}from"../database-preflight";import{DDL_CONSTRAINT_OVERRIDE_ENV,DIALECT_OVERRIDE_ENV,auditDdlConstraints,auditMigrationCorpus,dialectCapabilities,formatDdlConstraintError,formatMigrationDialectError,relativeMigrationDirectory,resolveMigrationDirectory,stripSqlNoise}from"@stacksjs/database";import{resultFailed}from"../result";let _runAction;async function runAction(...args){if(!_runAction)_runAction=(await import("@stacksjs/actions")).runAction;return _runAction(...args)}function acquireMigrationLock(){const lockDir=frameworkRuntimePath(),lockFile=`${lockDir}/migrations.lock`;try{if(!existsSync(lockDir))mkdirSync(lockDir,{recursive:!0});const fd=openSync(lockFile,"wx");try{closeSync(fd)}catch{}return{acquired:!0,release:()=>{try{rmSync(lockFile,{force:!0})}catch{}}}}catch{return{acquired:!1,release:()=>{}}}}async function ensureDatabaseOrExit(){try{const{ensureDatabaseReady}=await import("@stacksjs/database");await ensureDatabaseReady()}catch(error){log.syncError(error instanceof Error?error.message:String(error));process.exit(ExitCode.FatalError)}}function readMigrateMarker(){const file=frameworkRuntimePath("last-migrate-result.json");if(!existsSync(file))return null;try{const raw=readFileSync(file,"utf8"),parsed=JSON.parse(raw),n=typeof parsed.appliedCount==="number"?parsed.appliedCount:null;if(n===null||!Number.isFinite(n))return null;return{appliedCount:Math.max(0,Math.floor(n))}}catch{return null}finally{try{rmSync(file,{force:!0})}catch{}}}function countModelFiles(dir){if(!existsSync(dir))return 0;let count=0;const entries=readdirSync(dir,{withFileTypes:!0});for(const entry of entries)if(entry.isDirectory())count+=countModelFiles(`${dir}/${entry.name}`);else if(entry.name.endsWith(".ts")&&!entry.name.startsWith(".")&&!entry.name.startsWith("index"))count++;return count}function validateModelsExist(){const userModelsPath=appPath("Models"),defaultModelsPath=frameworkPath("defaults/app/Models"),userModelCount=countModelFiles(userModelsPath),defaultModelCount=countModelFiles(defaultModelsPath);if(userModelCount===0&&defaultModelCount===0)return{valid:!1,error:"No models found. Please create models in app/Models or ensure framework defaults exist."};return{valid:!0}}export function validateMigrationDialect(cwd=process.cwd()){const driver=String(process.env.DB_CONNECTION||"sqlite").toLowerCase(),dir=resolveMigrationDirectory(driver,{cwd}),relativeDir=relativeMigrationDirectory(dir,cwd);if(process.env[DIALECT_OVERRIDE_ENV]!=="1"){const caps=dialectCapabilities(driver),target=caps.wire==="mysql"?"mysql":caps.wire==="postgres"?"postgres":"sqlite";if(driver==="sqlite"||driver==="postgres"||caps.wire==="mysql"){const audit=auditMigrationCorpus({dir,target});if(!audit.empty&&audit.incompatible.length>0)return{valid:!1,error:formatMigrationDialectError(audit,target,relativeDir)}}}if(process.env[DDL_CONSTRAINT_OVERRIDE_ENV]!=="1"){const constraints=auditDdlConstraints({dir,dialect:driver});if(!constraints.empty&&constraints.violations.length>0)return{valid:!1,error:formatDdlConstraintError(constraints,driver,relativeDir)}}return{valid:!0}}async function reportMissingForeignKeys(){try{const{auditForeignKeys}=await import("@stacksjs/database"),result=await auditForeignKeys();if(result.missing.length===0)return;const sample=result.missing.slice(0,5).map((fk)=>` \u2022 ${fk.fromTable}.${fk.fromColumn} \u2192 ${fk.toTable}.${fk.toColumn} (${fk.model})`).join(`
2
2
  `),more=result.missing.length>5?`
3
3
  + ${result.missing.length-5} more \u2014 run \`./buddy doctor\` for the full list.`:"";log.warn(`${result.missing.length} of ${result.declared.length} declared foreign keys are missing from the live schema:
4
4
  ${sample}${more}
@@ -21,7 +21,7 @@ ${sample}${more}
21
21
  Unknown target driver "${driver}". Allowed: sqlite, mysql, vitess, postgres.
22
22
  `);await outro("Aborted.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.FatalError)}const current=(process.env.DB_CONNECTION||"sqlite").toLowerCase();if(current===target){console.log(`
23
23
  DB_CONNECTION is already "${target}". Nothing to switch.
24
- `);await outro("No-op.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}const missingEnv=({sqlite:["DB_DATABASE"],mysql:["DB_HOST","DB_PORT","DB_USERNAME","DB_PASSWORD","DB_DATABASE"],vitess:["DB_HOST","DB_PORT","DB_USERNAME","DB_PASSWORD","DB_DATABASE"],postgres:["DB_HOST","DB_PORT","DB_USERNAME","DB_PASSWORD","DB_DATABASE"]}[target]??[]).filter((k)=>!process.env[k]);let alterCount=0,uniqueIdxCount=0;try{const migrationsDir=projectPath("database/migrations");if(existsSync(migrationsDir)){const files=readdirSync(migrationsDir).filter((f)=>f.endsWith(".sql"));for(const f of files){const content=stripSqlNoise(readFileSync(`${migrationsDir}/${f}`,"utf-8")).toLowerCase();if(/alter\s+table[\s\S]*add\s+constraint/.test(content))alterCount++;if(/^\s*create\s+unique\s+index/m.test(content))uniqueIdxCount++}}}catch{}const auditTarget=target==="vitess"?"mysql":target,switchAudit=auditMigrationCorpus({dir:projectPath("database/migrations"),target:auditTarget}),switchBlocked=switchAudit.incompatible.length>0,blockedFiles=new Set(switchAudit.incompatible.map((m)=>m.file)).size,dialectNote=switchBlocked?`
24
+ `);await outro("No-op.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}const missingEnv=({sqlite:["DB_DATABASE"],mysql:["DB_HOST","DB_PORT","DB_USERNAME","DB_PASSWORD","DB_DATABASE"],vitess:["DB_HOST","DB_PORT","DB_USERNAME","DB_PASSWORD","DB_DATABASE"],postgres:["DB_HOST","DB_PORT","DB_USERNAME","DB_PASSWORD","DB_DATABASE"]}[target]??[]).filter((k)=>!process.env[k]);let alterCount=0,uniqueIdxCount=0;try{const migrationsDir=resolveMigrationDirectory(target);if(existsSync(migrationsDir)){const files=readdirSync(migrationsDir).filter((f)=>f.endsWith(".sql"));for(const f of files){const content=stripSqlNoise(readFileSync(`${migrationsDir}/${f}`,"utf-8")).toLowerCase();if(/alter\s+table[\s\S]*add\s+constraint/.test(content))alterCount++;if(/^\s*create\s+unique\s+index/m.test(content))uniqueIdxCount++}}}catch{}const auditTarget=target==="vitess"?"mysql":target,switchAudit=auditMigrationCorpus({dir:resolveMigrationDirectory(target),target:auditTarget}),switchBlocked=switchAudit.incompatible.length>0,blockedFiles=new Set(switchAudit.incompatible.map((m)=>m.file)).size,dialectNote=switchBlocked?`
25
25
  \u26D4 ${blockedFiles} of ${switchAudit.total} migration files are ${switchAudit.inferred??"another dialect"}-flavoured and will NOT run on ${target}.
26
26
  Regenerate them against ${target} before migrating, or this switch will fail on the first file.`:"",sqliteFkNote=current==="sqlite"&&alterCount>0?`
27
27
  (These were skipped on SQLite per stacksjs/stacks#1916 and survive on disk for replay.)`:"",boolNote=current==="sqlite"&&(target==="postgres"||target==="mysql"||target==="vitess")?`
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/buddy",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.70.270",
5
+ "version": "0.70.275",
6
6
  "description": "Meet Buddy. The Stacks runtime.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -95,54 +95,54 @@
95
95
  "prepublishOnly": "bun run build"
96
96
  },
97
97
  "dependencies": {
98
- "@stacksjs/actions": "^0.70.270",
99
- "@stacksjs/ai": "^0.70.270",
100
- "@stacksjs/alias": "^0.70.270",
101
- "@stacksjs/arrays": "^0.70.270",
102
- "@stacksjs/auth": "^0.70.270",
103
- "@stacksjs/build": "^0.70.270",
104
- "@stacksjs/cache": "^0.70.270",
105
- "@stacksjs/cli": "^0.70.270",
98
+ "@stacksjs/actions": "^0.70.275",
99
+ "@stacksjs/ai": "^0.70.275",
100
+ "@stacksjs/alias": "^0.70.275",
101
+ "@stacksjs/arrays": "^0.70.275",
102
+ "@stacksjs/auth": "^0.70.275",
103
+ "@stacksjs/build": "^0.70.275",
104
+ "@stacksjs/cache": "^0.70.275",
105
+ "@stacksjs/cli": "^0.70.275",
106
106
  "@stacksjs/clapp": "^0.2.12",
107
- "@stacksjs/cloud": "^0.70.270",
108
- "@stacksjs/collections": "^0.70.270",
109
- "@stacksjs/config": "^0.70.270",
110
- "@stacksjs/database": "^0.70.270",
111
- "@stacksjs/desktop-build": "^0.70.270",
112
- "@stacksjs/dns": "^0.70.270",
113
- "@stacksjs/email": "^0.70.270",
114
- "@stacksjs/enums": "^0.70.270",
115
- "@stacksjs/error-handling": "^0.70.270",
116
- "@stacksjs/events": "^0.70.270",
117
- "@stacksjs/git": "^0.70.270",
107
+ "@stacksjs/cloud": "^0.70.275",
108
+ "@stacksjs/collections": "^0.70.275",
109
+ "@stacksjs/config": "^0.70.275",
110
+ "@stacksjs/database": "^0.70.275",
111
+ "@stacksjs/desktop-build": "^0.70.275",
112
+ "@stacksjs/dns": "^0.70.275",
113
+ "@stacksjs/email": "^0.70.275",
114
+ "@stacksjs/enums": "^0.70.275",
115
+ "@stacksjs/error-handling": "^0.70.275",
116
+ "@stacksjs/events": "^0.70.275",
117
+ "@stacksjs/git": "^0.70.275",
118
118
  "@stacksjs/gitit": "^0.2.5",
119
- "@stacksjs/health": "^0.70.270",
119
+ "@stacksjs/health": "^0.70.275",
120
120
  "@stacksjs/dnsx": "^0.2.3",
121
121
  "@stacksjs/httx": "^0.1.10",
122
- "@stacksjs/image": "^0.70.270",
123
- "@stacksjs/lint": "^0.70.270",
124
- "@stacksjs/logging": "^0.70.270",
125
- "@stacksjs/notifications": "^0.70.270",
126
- "@stacksjs/objects": "^0.70.270",
127
- "@stacksjs/orm": "^0.70.270",
128
- "@stacksjs/path": "^0.70.270",
129
- "@stacksjs/skills": "^0.70.270",
130
- "@stacksjs/payments": "^0.70.270",
131
- "@stacksjs/realtime": "^0.70.270",
132
- "@stacksjs/router": "^0.70.270",
122
+ "@stacksjs/image": "^0.70.275",
123
+ "@stacksjs/lint": "^0.70.275",
124
+ "@stacksjs/logging": "^0.70.275",
125
+ "@stacksjs/notifications": "^0.70.275",
126
+ "@stacksjs/objects": "^0.70.275",
127
+ "@stacksjs/orm": "^0.70.275",
128
+ "@stacksjs/path": "^0.70.275",
129
+ "@stacksjs/skills": "^0.70.275",
130
+ "@stacksjs/payments": "^0.70.275",
131
+ "@stacksjs/realtime": "^0.70.275",
132
+ "@stacksjs/router": "^0.70.275",
133
133
  "@stacksjs/rpx": "^0.11.42",
134
- "@stacksjs/search-engine": "^0.70.270",
135
- "@stacksjs/security": "^0.70.270",
136
- "@stacksjs/server": "^0.70.270",
137
- "@stacksjs/storage": "^0.70.270",
138
- "@stacksjs/strings": "^0.70.270",
139
- "@stacksjs/testing": "^0.70.270",
140
- "@stacksjs/tunnel": "^0.70.270",
141
- "@stacksjs/types": "^0.70.270",
142
- "@stacksjs/ui": "^0.70.270",
143
- "@stacksjs/utils": "^0.70.270",
144
- "@stacksjs/validation": "^0.70.270",
145
- "@stacksjs/ts-cloud": "^0.7.99",
134
+ "@stacksjs/search-engine": "^0.70.275",
135
+ "@stacksjs/security": "^0.70.275",
136
+ "@stacksjs/server": "^0.70.275",
137
+ "@stacksjs/storage": "^0.70.275",
138
+ "@stacksjs/strings": "^0.70.275",
139
+ "@stacksjs/testing": "^0.70.275",
140
+ "@stacksjs/tunnel": "^0.70.275",
141
+ "@stacksjs/types": "^0.70.275",
142
+ "@stacksjs/ui": "^0.70.275",
143
+ "@stacksjs/utils": "^0.70.275",
144
+ "@stacksjs/validation": "^0.70.275",
145
+ "@stacksjs/ts-cloud": "^0.7.103",
146
146
  "ajv": "^8.20.0",
147
147
  "ajv-formats": "^3.0.1",
148
148
  "ts-pantry": "^0.11.0"