@stacksjs/database 0.70.161 → 0.70.162

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.
@@ -47,7 +47,9 @@ export declare function usersStripeIdSql(): string;
47
47
  * password_changed_at, two_factor_secret, two_factor_enabled,
48
48
  * stripe_id), each independently try/catch-swallowed so one
49
49
  * already-existing column (or a not-yet-existing `users` table) never
50
- * skips the others. Exported so `buddy migrate`/`migrate:fresh` can
50
+ * skips the others. The schema-diff guard mirrors this set in
51
+ * `managed-columns.ts` (`USERS_GUARANTEED_COLUMNS`) so the differ never
52
+ * proposes dropping them (stacksjs/stacks#2075) — keep the two in sync. Exported so `buddy migrate`/`migrate:fresh` can
51
53
  * call it a second time after the numbered model migrations run — see
52
54
  * the call site in {@link migrateAuthTables} for why a single call
53
55
  * isn't enough.
package/dist/index.d.ts CHANGED
@@ -107,6 +107,8 @@ export * from './custom/index';
107
107
  export * from './auth-tables';
108
108
  // uuid column guarantee for `useUuid` models (stacksjs/status#1 Phase 9)
109
109
  export * from './uuid-columns';
110
+ // Schema-diff guards so trait-managed columns aren't proposed for dropping (stacksjs/stacks#2075)
111
+ export * from './managed-columns';
110
112
  // Notification tables migration (stacksjs/stacks#1937)
111
113
  export { migrateNotificationTables } from './notification-tables';
112
114
  // RBAC tables migration (stacksjs/stacks#1941 Phase A)
package/dist/index.js CHANGED
@@ -26,6 +26,7 @@ export * from "./drivers";
26
26
  export * from "./custom";
27
27
  export * from "./auth-tables";
28
28
  export * from "./uuid-columns";
29
+ export * from "./managed-columns";
29
30
  export { migrateNotificationTables } from "./notification-tables";
30
31
  export { migrateRbacTables } from "./rbac-tables";
31
32
  export * from "./sql-helpers";
@@ -0,0 +1,31 @@
1
+ import type { MigrationOperation } from '@stacksjs/query-builder';
2
+ /**
3
+ * Resolve `table -> protected column names`: the `users` auth/billing columns
4
+ * plus `uuid` on every table backing a `useUuid` model. `findUuidTables` is
5
+ * lazy-imported so this module stays free of the model-walking ORM graph until
6
+ * actually resolving. Best-effort on the uuid side — if that walk fails we still
7
+ * guard the hardcoded `users` columns rather than losing the protection.
8
+ */
9
+ export declare function frameworkManagedColumns(): Promise<Map<string, Set<string>>>;
10
+ /** True when `op` would drop a column the framework guarantees at runtime. */
11
+ export declare function isManagedColumnDrop(op: ColumnOp, managed: Map<string, Set<string>>): boolean;
12
+ /** Return `operations` without any drop of a framework-managed column. */
13
+ export declare function withoutManagedColumnDrops<T extends ColumnOp>(operations: T[], managed: Map<string, Set<string>>): T[];
14
+ /**
15
+ * Remove generated SQL statements that drop a framework-managed column, so the
16
+ * drop is never written to a migration file. Catches both the direct
17
+ * `ALTER TABLE ... DROP COLUMN` form and, by matching the structured
18
+ * operations' own `sql`, the SQLite table-rebuild form (which drops a column by
19
+ * recreating the table without it). Pass `operations` when available for the
20
+ * rebuild case; the regex alone still covers the common direct form.
21
+ */
22
+ export declare function withoutManagedColumnDropSql(statements: string[], managed: Map<string, Set<string>>, operations?: MigrationOperation[]): { statements: string[], removed: string[] };
23
+ /**
24
+ * The `users` columns the framework guarantees via `ensureUsersAuthColumns`'s
25
+ * defensive ALTERs (auth-tables.ts) — created OUTSIDE any model's `attributes`.
26
+ * Kept here (a dependency-light module) so the differ guard doesn't drag in the
27
+ * ORM/query-builder graph. Keep in sync with `ensureUsersAuthColumns`.
28
+ */
29
+ export declare const USERS_GUARANTEED_COLUMNS: readonly string[];
30
+ /** A minimal view of a migration operation — all these helpers need. */
31
+ declare type ColumnOp = Pick<MigrationOperation, 'kind' | 'table' | 'column'>;
@@ -0,0 +1,46 @@
1
+ export const USERS_GUARANTEED_COLUMNS = [
2
+ "email_verified_at",
3
+ "password_changed_at",
4
+ "two_factor_secret",
5
+ "two_factor_enabled",
6
+ "two_factor_last_used_step",
7
+ "stripe_id"
8
+ ];
9
+ export async function frameworkManagedColumns() {
10
+ const managed = new Map;
11
+ managed.set("users", new Set(USERS_GUARANTEED_COLUMNS));
12
+ try {
13
+ const { findUuidTables } = await import("./uuid-columns");
14
+ for (const table of await findUuidTables()) {
15
+ const columns = managed.get(table) ?? new Set;
16
+ columns.add("uuid");
17
+ managed.set(table, columns);
18
+ }
19
+ } catch {}
20
+ return managed;
21
+ }
22
+ export function isManagedColumnDrop(op, managed) {
23
+ return op.kind === "drop_column" && op.column != null && (managed.get(op.table)?.has(op.column) ?? !1);
24
+ }
25
+ export function withoutManagedColumnDrops(operations, managed) {
26
+ return operations.filter((op) => !isManagedColumnDrop(op, managed));
27
+ }
28
+ function normalizeSql(sql) {
29
+ return sql.trim().replace(/\s+/g, " ").replace(/;+\s*$/, "");
30
+ }
31
+ const DROP_COLUMN_RE = /ALTER\s+TABLE\s+["'`]?(\w+)["'`]?\s+DROP\s+COLUMN\s+(?:IF\s+EXISTS\s+)?["'`]?(\w+)["'`]?/i;
32
+ export function withoutManagedColumnDropSql(statements, managed, operations = []) {
33
+ const protectedSql = new Set(operations.filter((op) => isManagedColumnDrop(op, managed)).map((op) => normalizeSql(op.sql))), removed = [];
34
+ return { statements: statements.filter((statement) => {
35
+ if (protectedSql.has(normalizeSql(statement))) {
36
+ removed.push(statement);
37
+ return !1;
38
+ }
39
+ const match = statement.match(DROP_COLUMN_RE);
40
+ if (match?.[1] && match[2] && (managed.get(match[1])?.has(match[2]) ?? !1)) {
41
+ removed.push(statement);
42
+ return !1;
43
+ }
44
+ return !0;
45
+ }), removed };
46
+ }
@@ -20,6 +20,7 @@ import {
20
20
  setConfig
21
21
  } from "@stacksjs/query-builder";
22
22
  import { db } from "./utils";
23
+ import { frameworkManagedColumns, withoutManagedColumnDrops, withoutManagedColumnDropSql } from "./managed-columns";
23
24
  import { acquireMigrationLock } from "./migration-lock";
24
25
  import { env as envVars } from "@stacksjs/env";
25
26
  import { getConnectionDefaults } from "./defaults";
@@ -454,8 +455,10 @@ export async function previewPendingMigrations(options = {}) {
454
455
  const dialect = getDialect(), { modelsDir, skip } = prepareMigrationModelsDir();
455
456
  if (skip)
456
457
  return [];
457
- const { applyRenames, fromDb } = resolveGenerateOptions(options);
458
- return (await qbGenerateMigration(modelsDir, { dialect: getQbDialect(), dryRun: !0, applyRenames, fromDb })).operations ?? [];
458
+ const { applyRenames, fromDb } = resolveGenerateOptions(options), operations = (await qbGenerateMigration(modelsDir, { dialect: getQbDialect(), dryRun: !0, applyRenames, fromDb })).operations ?? [];
459
+ if (!operations.some((op) => op.kind === "drop_column"))
460
+ return operations;
461
+ return withoutManagedColumnDrops(operations, await frameworkManagedColumns());
459
462
  } catch (error) {
460
463
  log.debug(`[migration] preview failed: ${error instanceof Error ? error.message : String(error)}`);
461
464
  return [];
@@ -494,8 +497,15 @@ export async function generateMigrations(options = {}) {
494
497
  const { applyRenames, fromDb } = resolveGenerateOptions(options);
495
498
  log.debug(`[migration] Generating migrations for dialect: ${dialect}, models: ${modelsDir}`);
496
499
  const result = await qbGenerateMigration(modelsDir, { dialect: getQbDialect(), dryRun: !0, applyRenames, fromDb });
500
+ let sqlStatements = result.sqlStatements ?? [];
501
+ if (result.hasChanges && sqlStatements.length > 0) {
502
+ const filtered = withoutManagedColumnDropSql(sqlStatements, await frameworkManagedColumns(), result.operations ?? []);
503
+ if (filtered.removed.length > 0)
504
+ log.debug(`[migration] Skipped ${filtered.removed.length} generated drop(s) of framework-managed column(s) (stacksjs/stacks#2075)`);
505
+ sqlStatements = filtered.statements;
506
+ }
497
507
  if (result.hasChanges) {
498
- const written = persistGeneratedMigrations(result.sqlStatements ?? []);
508
+ const written = persistGeneratedMigrations(sqlStatements);
499
509
  if (written > 0)
500
510
  log.success(`Generated ${written} migration file${written === 1 ? "" : "s"}`);
501
511
  else
package/dist/types.js CHANGED
@@ -54,7 +54,7 @@ const SAFE_FILTER_OPERATORS = new Set([
54
54
  "is",
55
55
  "is not"
56
56
  ]);
57
- function inlineSqlLiteral(value) {
57
+ function inlineSqlLiteral(_value) {
58
58
  if (value === null || value === void 0)
59
59
  return "NULL";
60
60
  if (typeof value === "number") {
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/database",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.70.161",
5
+ "version": "0.70.162",
6
6
  "description": "The Stacks database integration.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -60,15 +60,15 @@
60
60
  "dynamodb-tooling": "^0.3.2"
61
61
  },
62
62
  "devDependencies": {
63
- "@stacksjs/cli": "0.70.161",
64
- "@stacksjs/config": "0.70.161",
65
- "@stacksjs/logging": "0.70.161",
66
- "@stacksjs/router": "0.70.161",
63
+ "@stacksjs/cli": "0.70.162",
64
+ "@stacksjs/config": "0.70.162",
65
+ "@stacksjs/logging": "0.70.162",
66
+ "@stacksjs/router": "0.70.162",
67
67
  "better-dx": "^0.2.17",
68
- "@stacksjs/path": "0.70.161",
69
- "@stacksjs/query-builder": "0.70.161",
70
- "@stacksjs/storage": "0.70.161",
71
- "@stacksjs/strings": "0.70.161",
72
- "@stacksjs/utils": "0.70.161"
68
+ "@stacksjs/path": "0.70.162",
69
+ "@stacksjs/query-builder": "0.70.162",
70
+ "@stacksjs/storage": "0.70.162",
71
+ "@stacksjs/strings": "0.70.162",
72
+ "@stacksjs/utils": "0.70.162"
73
73
  }
74
74
  }