spfn 0.2.0-beta.59 → 0.2.0-beta.60

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.
Files changed (3) hide show
  1. package/README.md +10 -0
  2. package/dist/index.js +166 -70
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -184,6 +184,16 @@ loaded `.env` chain.
184
184
  > `db push` is for development. For production, use `db generate` + `db migrate` to keep
185
185
  > migration history.
186
186
 
187
+ `db push` and `db migrate` also replay migrations shipped by installed SPFN function
188
+ packages (`@spfn/auth`, `@spfn/cms`, …) into per-package tracking tables
189
+ (`drizzle.__spfn_fn_<pkg>_migrations`). The CLI applies these with a built-in runner
190
+ that reads both migration layouts — drizzle-kit ≤0.31 (`NNNN_name.sql` +
191
+ `meta/_journal.json`) and drizzle-kit 1.0 (`<timestamp>_name/migration.sql`) — so a
192
+ package's layout never has to match the CLI's bundled drizzle version. `db push`
193
+ validates every package's migration folder before applying the project schema, and a
194
+ function-migration failure after a successful schema apply exits 1 with a message
195
+ making clear the project schema was already committed.
196
+
187
197
  Database TLS is controlled by `DATABASE_URL`. Loopback URLs (`localhost`, `127.0.0.1`,
188
198
  and `::1`) default to `ssl: false`; add an explicit `sslmode` when the local server uses
189
199
  TLS. For a TLS connection with a self-signed certificate, set
package/dist/index.js CHANGED
@@ -869,7 +869,7 @@ var init_deployment_config = __esm({
869
869
 
870
870
  // src/utils/version.ts
871
871
  function getCliVersion() {
872
- return "0.2.0-beta.59";
872
+ return "0.2.0-beta.60";
873
873
  }
874
874
  function getTagFromVersion(version) {
875
875
  const match = version.match(/-([a-z]+)\./i);
@@ -1336,10 +1336,14 @@ var init_init = __esm({
1336
1336
  // src/utils/function-migrations.ts
1337
1337
  var function_migrations_exports = {};
1338
1338
  __export(function_migrations_exports, {
1339
+ applyFunctionMigrationPlan: () => applyFunctionMigrationPlan,
1339
1340
  discoverFunctionMigrations: () => discoverFunctionMigrations,
1340
- executeFunctionMigrations: () => executeFunctionMigrations
1341
+ executeFunctionMigrations: () => executeFunctionMigrations,
1342
+ loadFunctionMigrationPlans: () => loadFunctionMigrationPlans,
1343
+ readMigrationEntries: () => readMigrationEntries
1341
1344
  });
1342
1345
  import chalk13 from "chalk";
1346
+ import { createHash } from "crypto";
1343
1347
  import { join as join19 } from "path";
1344
1348
  import { env as env2 } from "@spfn/core/config";
1345
1349
  import { loadEnv as loadEnv2 } from "@spfn/core/server";
@@ -1385,8 +1389,75 @@ function discoverFunctionMigrations(cwd = process.cwd()) {
1385
1389
  }
1386
1390
  return functions;
1387
1391
  }
1388
- async function migrateLegacyTable(db, functionMigrations) {
1389
- const legacyCheck = await db.execute(
1392
+ function readMigrationEntries(migrationsDir, packageName) {
1393
+ const journalPath = join19(migrationsDir, "meta", "_journal.json");
1394
+ return existsSync19(journalPath) ? readJournalEntries(migrationsDir, journalPath, packageName) : readFolderEntries(migrationsDir, packageName);
1395
+ }
1396
+ function loadFunctionMigrationPlans(functionMigrations) {
1397
+ return functionMigrations.map((func) => ({
1398
+ ...func,
1399
+ entries: readMigrationEntries(func.migrationsDir, func.packageName)
1400
+ }));
1401
+ }
1402
+ function readJournalEntries(migrationsDir, journalPath, packageName) {
1403
+ let journal;
1404
+ try {
1405
+ journal = JSON.parse(readFileSync9(journalPath, "utf-8"));
1406
+ } catch {
1407
+ journal = {};
1408
+ }
1409
+ if (!Array.isArray(journal.entries)) {
1410
+ throw new Error(`${packageName}: invalid migration journal at ${journalPath}`);
1411
+ }
1412
+ const entries = [...journal.entries];
1413
+ entries.sort((a, b) => a.idx - b.idx);
1414
+ return entries.map((entry) => {
1415
+ if (typeof entry?.tag !== "string" || typeof entry?.when !== "number") {
1416
+ throw new Error(`${packageName}: invalid journal entry in ${journalPath}`);
1417
+ }
1418
+ const sqlPath = join19(migrationsDir, `${entry.tag}.sql`);
1419
+ if (!existsSync19(sqlPath)) {
1420
+ throw new Error(`${packageName}: migration file not found: ${entry.tag}.sql`);
1421
+ }
1422
+ return toEntry(entry.tag, readFileSync9(sqlPath, "utf-8"), entry.when);
1423
+ });
1424
+ }
1425
+ function readFolderEntries(migrationsDir, packageName) {
1426
+ const folders = readdirSync2(migrationsDir, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name).filter((name) => existsSync19(join19(migrationsDir, name, "migration.sql"))).sort((a, b) => a.localeCompare(b));
1427
+ return folders.map((name) => toEntry(
1428
+ name,
1429
+ readFileSync9(join19(migrationsDir, name, "migration.sql"), "utf-8"),
1430
+ folderTimestampMillis(name, packageName)
1431
+ ));
1432
+ }
1433
+ function folderTimestampMillis(name, packageName) {
1434
+ const match = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/.exec(name);
1435
+ if (!match) {
1436
+ throw new Error(`${packageName}: migration folder name must start with a YYYYMMDDHHMMSS timestamp: ${name}`);
1437
+ }
1438
+ const [, year, month, day, hour, minute, second] = match;
1439
+ return Date.UTC(
1440
+ Number(year),
1441
+ Number(month) - 1,
1442
+ Number(day),
1443
+ Number(hour),
1444
+ Number(minute),
1445
+ Number(second)
1446
+ );
1447
+ }
1448
+ function toEntry(name, content, millis) {
1449
+ return {
1450
+ name,
1451
+ millis,
1452
+ hash: createHash("sha256").update(content).digest("hex"),
1453
+ statements: content.split("--> statement-breakpoint").map((statement) => statement.trim()).filter((statement) => statement.length > 0)
1454
+ };
1455
+ }
1456
+ function functionMigrationsTable(packageName) {
1457
+ return `__spfn_fn_${packageName.replace("@spfn/", "")}_migrations`;
1458
+ }
1459
+ async function migrateLegacyTable(db, plans) {
1460
+ const legacyCheck = await db.query(
1390
1461
  `SELECT EXISTS (
1391
1462
  SELECT 1 FROM information_schema.tables
1392
1463
  WHERE table_schema = 'drizzle' AND table_name = '__spfn_fn_migrations'
@@ -1396,89 +1467,103 @@ async function migrateLegacyTable(db, functionMigrations) {
1396
1467
  return;
1397
1468
  }
1398
1469
  console.log(chalk13.dim("\n Migrating legacy shared migration table to per-package tables..."));
1399
- const legacyRows = await db.execute(
1470
+ const legacyRows = await db.query(
1400
1471
  `SELECT hash, created_at FROM drizzle."__spfn_fn_migrations" ORDER BY id`
1401
1472
  );
1402
1473
  if (legacyRows.length === 0) {
1403
- await db.execute(`DROP TABLE drizzle."__spfn_fn_migrations"`);
1474
+ await db.query(`DROP TABLE drizzle."__spfn_fn_migrations"`);
1404
1475
  return;
1405
1476
  }
1406
- for (const func of functionMigrations) {
1407
- const journalPath = join19(func.migrationsDir, "meta", "_journal.json");
1408
- if (!existsSync19(journalPath)) {
1409
- continue;
1410
- }
1411
- const journal = JSON.parse(readFileSync9(journalPath, "utf-8"));
1412
- const entries = journal.entries || [];
1413
- const tableName = `__spfn_fn_${func.packageName.replace("@spfn/", "")}_migrations`;
1414
- await db.execute(
1415
- `CREATE SCHEMA IF NOT EXISTS drizzle`
1416
- );
1417
- await db.execute(
1418
- `CREATE TABLE IF NOT EXISTS drizzle."${tableName}" (
1419
- id serial PRIMARY KEY,
1420
- hash text NOT NULL,
1421
- created_at bigint
1422
- )`
1423
- );
1424
- const existing = await db.execute(
1477
+ const legacyHashes = new Set(legacyRows.map((row) => row.hash));
1478
+ for (const plan of plans) {
1479
+ const tableName = functionMigrationsTable(plan.packageName);
1480
+ await ensureMigrationsTable(db, tableName);
1481
+ const existing = await db.query(
1425
1482
  `SELECT COUNT(*) AS "count" FROM drizzle."${tableName}"`
1426
1483
  );
1427
1484
  if (Number(existing[0]?.count) > 0) {
1428
1485
  continue;
1429
1486
  }
1430
- const { createHash } = await import("crypto");
1431
1487
  let copied = 0;
1432
- for (const entry of entries) {
1433
- const sqlPath = join19(func.migrationsDir, `${entry.tag}.sql`);
1434
- if (!existsSync19(sqlPath)) {
1488
+ for (const entry of plan.entries) {
1489
+ if (!legacyHashes.has(entry.hash)) {
1435
1490
  continue;
1436
1491
  }
1437
- const sqlContent = readFileSync9(sqlPath, "utf-8");
1438
- const hash = createHash("sha256").update(sqlContent).digest("hex");
1439
- const found = legacyRows.find((r) => r.hash === hash);
1440
- if (found) {
1441
- await db.execute(
1442
- `INSERT INTO drizzle."${tableName}" (hash, created_at) VALUES ('${hash}', ${entry.when})`
1443
- );
1444
- copied++;
1445
- }
1492
+ await db.query(
1493
+ `INSERT INTO drizzle."${tableName}" (hash, created_at) VALUES ($1, $2)`,
1494
+ [entry.hash, entry.millis]
1495
+ );
1496
+ copied++;
1446
1497
  }
1447
1498
  if (copied > 0) {
1448
- console.log(chalk13.dim(` \u2713 ${func.packageName}: copied ${copied} migration record(s)`));
1499
+ console.log(chalk13.dim(` \u2713 ${plan.packageName}: copied ${copied} migration record(s)`));
1449
1500
  }
1450
1501
  }
1451
- await db.execute(`DROP TABLE drizzle."__spfn_fn_migrations"`);
1502
+ await db.query(`DROP TABLE drizzle."__spfn_fn_migrations"`);
1452
1503
  console.log(chalk13.dim(" \u2713 Legacy migration table removed\n"));
1453
1504
  }
1454
- async function executeFunctionMigrations(functionMigrations) {
1455
- let executedCount = 0;
1456
- const { drizzle } = await import("drizzle-orm/postgres-js");
1457
- const { migrate } = await import("drizzle-orm/postgres-js/migrator");
1505
+ async function ensureMigrationsTable(db, tableName) {
1506
+ await db.query(`CREATE SCHEMA IF NOT EXISTS drizzle`);
1507
+ await db.query(
1508
+ `CREATE TABLE IF NOT EXISTS drizzle."${tableName}" (
1509
+ id serial PRIMARY KEY,
1510
+ hash text NOT NULL,
1511
+ created_at bigint
1512
+ )`
1513
+ );
1514
+ }
1515
+ async function applyFunctionMigrationPlan(db, plan) {
1516
+ const tableName = functionMigrationsTable(plan.packageName);
1517
+ await ensureMigrationsTable(db, tableName);
1518
+ const rows = await db.query(
1519
+ `SELECT created_at FROM drizzle."${tableName}" ORDER BY created_at DESC LIMIT 1`
1520
+ );
1521
+ const lastMillis = rows.length > 0 ? Number(rows[0]?.created_at) : Number.NEGATIVE_INFINITY;
1522
+ const pending = plan.entries.filter((entry) => entry.millis > lastMillis);
1523
+ if (pending.length === 0) {
1524
+ return 0;
1525
+ }
1526
+ await db.transaction(async (tx) => {
1527
+ for (const entry of pending) {
1528
+ for (const statement of entry.statements) {
1529
+ await tx.query(statement);
1530
+ }
1531
+ await tx.query(
1532
+ `INSERT INTO drizzle."${tableName}" (hash, created_at) VALUES ($1, $2)`,
1533
+ [entry.hash, entry.millis]
1534
+ );
1535
+ }
1536
+ });
1537
+ return pending.length;
1538
+ }
1539
+ function createPostgresJsMigrationDb(client) {
1540
+ return {
1541
+ query: (text, params) => client.unsafe(text, params ?? []),
1542
+ transaction: (fn) => client.begin((tx) => fn(createPostgresJsMigrationDb(tx)))
1543
+ };
1544
+ }
1545
+ async function executeFunctionMigrations(plans) {
1458
1546
  const postgres = await import("postgres");
1459
1547
  loadEnv2();
1460
1548
  if (!env2.DATABASE_URL) {
1461
1549
  throw new Error("DATABASE_URL not found in environment");
1462
1550
  }
1463
1551
  const connection = postgres.default(env2.DATABASE_URL, { max: 1 });
1464
- const db = drizzle({ client: connection });
1552
+ const db = createPostgresJsMigrationDb(connection);
1553
+ let appliedCount = 0;
1465
1554
  try {
1466
- await migrateLegacyTable(db, functionMigrations);
1467
- for (const func of functionMigrations) {
1555
+ await migrateLegacyTable(db, plans);
1556
+ for (const plan of plans) {
1468
1557
  console.log(chalk13.blue(`
1469
- \u{1F4E6} Running ${func.packageName} migrations...`));
1470
- const tableName = `__spfn_fn_${func.packageName.replace("@spfn/", "")}_migrations`;
1471
- await migrate(db, {
1472
- migrationsFolder: func.migrationsDir,
1473
- migrationsTable: tableName
1474
- });
1475
- console.log(chalk13.green(` \u2713 ${func.packageName} migrations applied`));
1476
- executedCount++;
1558
+ \u{1F4E6} Running ${plan.packageName} migrations...`));
1559
+ const applied = await applyFunctionMigrationPlan(db, plan);
1560
+ console.log(applied > 0 ? chalk13.green(` \u2713 ${plan.packageName}: ${applied} migration(s) applied`) : chalk13.dim(` \u2013 ${plan.packageName}: up to date`));
1561
+ appliedCount += applied;
1477
1562
  }
1478
1563
  } finally {
1479
1564
  await connection.end();
1480
1565
  }
1481
- return executedCount;
1566
+ return appliedCount;
1482
1567
  }
1483
1568
  var init_function_migrations = __esm({
1484
1569
  "src/utils/function-migrations.ts"() {
@@ -3039,6 +3124,7 @@ function displayApplySummary(applied, skipped) {
3039
3124
  }
3040
3125
 
3041
3126
  // src/commands/db/push.ts
3127
+ init_function_migrations();
3042
3128
  async function resolvePushPlan(imports, db, schemaFilter) {
3043
3129
  const { pushSchema } = await import("drizzle-kit/api-postgres");
3044
3130
  const { sqlStatements, hints } = await pushSchema(
@@ -3094,6 +3180,7 @@ async function dbPush(options = {}) {
3094
3180
  }
3095
3181
  }
3096
3182
  const schemaFilter = Array.from(detectedSchemas);
3183
+ const functionPlans = loadFunctionPlansOrExit();
3097
3184
  const { db, close } = await createPushConnection();
3098
3185
  try {
3099
3186
  const { statements, hints } = await resolvePushPlan(imports, db, schemaFilter);
@@ -3102,7 +3189,7 @@ async function dbPush(options = {}) {
3102
3189
  }
3103
3190
  if (statements.length === 0) {
3104
3191
  console.log(chalk14.green("\u2705 No changes detected \u2014 database is up to date\n"));
3105
- await applyFunctionMigrations();
3192
+ await applyFunctionMigrations(functionPlans);
3106
3193
  return;
3107
3194
  }
3108
3195
  const result = classifyStatements(statements);
@@ -3144,27 +3231,36 @@ async function dbPush(options = {}) {
3144
3231
  console.log(chalk14.dim("Tip: Use --force to apply all changes without prompting.\n"));
3145
3232
  }
3146
3233
  }
3147
- await applyFunctionMigrations();
3234
+ await applyFunctionMigrations(functionPlans);
3148
3235
  } finally {
3149
3236
  await close();
3150
3237
  }
3151
3238
  }
3152
- async function applyFunctionMigrations() {
3153
- const { discoverFunctionMigrations: discoverFunctionMigrations2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
3154
- const functions = discoverFunctionMigrations2(process.cwd());
3155
- if (functions.length === 0) {
3239
+ function loadFunctionPlansOrExit() {
3240
+ const functions = discoverFunctionMigrations(process.cwd());
3241
+ try {
3242
+ return loadFunctionMigrationPlans(functions);
3243
+ } catch (error) {
3244
+ console.error(chalk14.red("\n\u274C Invalid function package migrations \u2014 nothing was applied"));
3245
+ console.error(chalk14.red(error instanceof Error ? error.message : "Unknown error"));
3246
+ process.exit(1);
3247
+ }
3248
+ }
3249
+ async function applyFunctionMigrations(plans) {
3250
+ if (plans.length === 0) {
3156
3251
  return;
3157
3252
  }
3158
3253
  console.log(chalk14.blue("\n\u{1F4E6} Applying function package migrations:"));
3159
- functions.forEach((func) => {
3160
- console.log(chalk14.dim(` - ${func.packageName}`));
3254
+ plans.forEach((plan) => {
3255
+ console.log(chalk14.dim(` - ${plan.packageName}`));
3161
3256
  });
3162
3257
  try {
3163
- await executeFunctionMigrations2(functions);
3258
+ await executeFunctionMigrations(plans);
3164
3259
  console.log(chalk14.green("\n\u2705 All function migrations applied\n"));
3165
3260
  } catch (error) {
3166
- console.error(chalk14.red("\n\u274C Failed to apply function migrations"));
3261
+ console.error(chalk14.red("\n\u274C Failed to apply function package migrations"));
3167
3262
  console.error(chalk14.red(error instanceof Error ? error.message : "Unknown error"));
3263
+ console.error(chalk14.yellow("Project schema changes (if any) were already applied \u2014 only function package migrations failed."));
3168
3264
  process.exit(1);
3169
3265
  }
3170
3266
  }
@@ -3571,14 +3667,14 @@ async function dbMigrate(options = {}) {
3571
3667
  console.error(chalk19.red("\u274C DATABASE_URL not found in environment"));
3572
3668
  process.exit(1);
3573
3669
  }
3574
- const { discoverFunctionMigrations: discoverFunctionMigrations2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
3670
+ const { discoverFunctionMigrations: discoverFunctionMigrations2, loadFunctionMigrationPlans: loadFunctionMigrationPlans2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
3575
3671
  const functions = discoverFunctionMigrations2(process.cwd());
3576
3672
  if (functions.length > 0) {
3577
3673
  console.log(chalk19.blue("\u{1F4E6} Applying function package migrations:"));
3578
3674
  functions.forEach((func) => {
3579
3675
  console.log(chalk19.dim(` - ${func.packageName}`));
3580
3676
  });
3581
- await executeFunctionMigrations2(functions);
3677
+ await executeFunctionMigrations2(loadFunctionMigrationPlans2(functions));
3582
3678
  console.log(chalk19.green("\u2705 Function migrations applied\n"));
3583
3679
  }
3584
3680
  const projectMigrationsDir = join20(process.cwd(), "src/server/drizzle");
@@ -4227,13 +4323,13 @@ async function addPackage(packageName) {
4227
4323
  console.log(chalk27.gray("Skipping database setup. Run migrations manually when ready:\n"));
4228
4324
  console.log(chalk27.gray(" pnpm spfn db push\n"));
4229
4325
  } else {
4230
- const { discoverFunctionMigrations: discoverFunctionMigrations2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
4326
+ const { discoverFunctionMigrations: discoverFunctionMigrations2, loadFunctionMigrationPlans: loadFunctionMigrationPlans2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
4231
4327
  const functions = discoverFunctionMigrations2(process.cwd());
4232
4328
  const targetFunction = functions.find((f) => f.packageName === packageName);
4233
4329
  if (targetFunction) {
4234
4330
  const spinner = ora11("Applying migrations...").start();
4235
4331
  try {
4236
- await executeFunctionMigrations2([targetFunction]);
4332
+ await executeFunctionMigrations2(loadFunctionMigrationPlans2([targetFunction]));
4237
4333
  spinner.succeed("Migrations applied");
4238
4334
  } catch (error) {
4239
4335
  spinner.fail("Failed to apply migrations");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spfn",
3
- "version": "0.2.0-beta.59",
3
+ "version": "0.2.0-beta.60",
4
4
  "description": "Superfunction CLI - Add SPFN to your Next.js project",
5
5
  "type": "module",
6
6
  "bin": {