@objectstack/metadata 6.8.1 → 7.0.0

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.
@@ -36,7 +36,7 @@ async function migrateEnvIdToProjectId(driver) {
36
36
  const driverAny = driver;
37
37
  if (typeof driverAny.raw !== "function") {
38
38
  throw new Error(
39
- "migrateEnvIdToProjectId: driver must expose a .raw(sql, bindings?) method. SqlDriver (better-sqlite3/knex) and TursoDriver both support this."
39
+ "migrateEnvIdToProjectId: driver must expose a .raw(sql, bindings?) method. SqlDriver (better-sqlite3/knex) supports this; cloud-side TursoDriver also conforms."
40
40
  );
41
41
  }
42
42
  const results = [];
@@ -87,7 +87,7 @@ async function migrateProjectIdToEnvironmentId(driver) {
87
87
  const driverAny = driver;
88
88
  if (typeof driverAny.raw !== "function") {
89
89
  throw new Error(
90
- "migrateProjectIdToEnvironmentId: driver must expose a .raw(sql, bindings?) method. SqlDriver (better-sqlite3/knex) and TursoDriver both support this."
90
+ "migrateProjectIdToEnvironmentId: driver must expose a .raw(sql, bindings?) method. migrateProjectIdToEnvironmentId: driver must expose a .raw(sql, bindings?) method. SqlDriver (better-sqlite3/knex) supports this; cloud-side TursoDriver also conforms."
91
91
  );
92
92
  }
93
93
  const results = [];
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/migrations/index.ts","../../src/migrations/migrate-env-id-to-project-id.ts","../../src/migrations/migrate-project-id-to-environment-id.ts","../../src/migrations/drop-projection-tables.ts","../../src/migrations/add-sys-metadata-overlay-index.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * @objectstack/metadata/migrations\n *\n * One-off database migrations for the metadata storage layer.\n */\n\nexport { migrateEnvIdToProjectId, type MigrationResult } from './migrate-env-id-to-project-id.js';\nexport {\n migrateProjectIdToEnvironmentId,\n type ProjectIdToEnvironmentIdResult,\n} from './migrate-project-id-to-environment-id.js';\nexport { dropProjectionTables, type DropProjectionResult } from './drop-projection-tables.js';\nexport {\n addSysMetadataOverlayIndex,\n type AddSysMetadataOverlayIndexResult,\n} from './add-sys-metadata-overlay-index.js';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Migration: env_id → project_id\n *\n * Renames the `env_id` column to `project_id` on the metadata storage tables:\n * - sys_metadata\n * - sys_metadata_history\n *\n * (The per-type projection tables `sys_object` / `sys_view` / `sys_flow` /\n * `sys_agent` / `sys_tool` were removed in 2026-05 along with the projection\n * pipeline — see ADR 0005 addendum. They are intentionally not included.)\n *\n * Safe to run multiple times (idempotent): checks for column existence before\n * attempting to rename. If `project_id` already exists, the step is skipped.\n *\n * Usage:\n * import { migrateEnvIdToProjectId } from '@objectstack/metadata/migrations';\n * await migrateEnvIdToProjectId(driver);\n */\n\nimport type { IDataDriver } from '@objectstack/spec/contracts';\n\nconst AFFECTED_TABLES = [\n 'sys_metadata',\n 'sys_metadata_history',\n] as const;\n\nexport interface MigrationResult {\n table: string;\n status: 'renamed' | 'already_done' | 'table_missing' | 'error';\n error?: string;\n}\n\n/**\n * Rename `env_id` → `project_id` on all metadata tables.\n *\n * @param driver An IDataDriver with access to the target database.\n * Must expose a raw query method: `driver.raw(sql, bindings?)`.\n * @returns Per-table migration results.\n */\nexport async function migrateEnvIdToProjectId(driver: IDataDriver): Promise<MigrationResult[]> {\n const driverAny = driver as any;\n\n if (typeof driverAny.raw !== 'function') {\n throw new Error(\n 'migrateEnvIdToProjectId: driver must expose a .raw(sql, bindings?) method. ' +\n 'SqlDriver (better-sqlite3/knex) and TursoDriver both support this.'\n );\n }\n\n const results: MigrationResult[] = [];\n\n for (const table of AFFECTED_TABLES) {\n try {\n // Detect dialect: SQLite uses PRAGMA, others use information_schema.\n const hasColumn = await _columnExists(driverAny, table, 'env_id');\n const alreadyMigrated = await _columnExists(driverAny, table, 'project_id');\n\n if (alreadyMigrated && !hasColumn) {\n results.push({ table, status: 'already_done' });\n continue;\n }\n\n if (!hasColumn) {\n // Neither column exists — table might not exist yet.\n results.push({ table, status: 'table_missing' });\n continue;\n }\n\n // Perform the rename. SQLite ≥ 3.25.0 supports ALTER TABLE RENAME COLUMN.\n await driverAny.raw(`ALTER TABLE \"${table}\" RENAME COLUMN env_id TO project_id`);\n\n results.push({ table, status: 'renamed' });\n } catch (err: any) {\n results.push({ table, status: 'error', error: err?.message ?? String(err) });\n }\n }\n\n return results;\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nasync function _columnExists(driver: any, table: string, column: string): Promise<boolean> {\n try {\n // SQLite: PRAGMA table_info returns rows with `name` column.\n const rows: any[] = await driver.raw(`PRAGMA table_info(\"${table}\")`);\n if (Array.isArray(rows) && rows.length > 0) {\n // knex wraps PRAGMA result; handle both `rows` and `rows[0]` shapes.\n const list: any[] = Array.isArray(rows[0]) ? rows[0] : rows;\n return list.some((r: any) => r?.name === column);\n }\n\n // Fallback for non-SQLite: query information_schema.\n const result: any[] = await driver.raw(\n `SELECT column_name FROM information_schema.columns WHERE table_name = ? AND column_name = ?`,\n [table, column]\n );\n const list: any[] = Array.isArray(result[0]) ? result[0] : result;\n return list.length > 0;\n } catch {\n return false;\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Migration: project_id → environment_id\n *\n * Renames the `project_id` column to `environment_id` on the metadata\n * storage tables:\n * - sys_metadata\n * - sys_metadata_history\n *\n * Forward counterpart of {@link migrateEnvIdToProjectId} (which performed the\n * earlier `env_id → project_id` rename). Together they let an operator walk an\n * old schema all the way forward in two steps:\n *\n * migrateEnvIdToProjectId(driver); // env_id → project_id (legacy)\n * migrateProjectIdToEnvironmentId(driver); // project_id → environment_id (v5)\n *\n * (The per-type projection tables `sys_object` / `sys_view` / `sys_flow` /\n * `sys_agent` / `sys_tool` were removed in 2026-05 along with the projection\n * pipeline — see ADR 0005 addendum. They are intentionally not included.)\n *\n * Safe to run multiple times (idempotent): checks for column existence before\n * attempting to rename. If `environment_id` already exists, the step is\n * skipped.\n *\n * Usage:\n * import { migrateProjectIdToEnvironmentId } from '@objectstack/metadata/migrations';\n * await migrateProjectIdToEnvironmentId(driver);\n */\n\nimport type { IDataDriver } from '@objectstack/spec/contracts';\n\nconst AFFECTED_TABLES = [\n 'sys_metadata',\n 'sys_metadata_history',\n] as const;\n\nexport interface ProjectIdToEnvironmentIdResult {\n table: string;\n status: 'renamed' | 'already_done' | 'table_missing' | 'error';\n error?: string;\n}\n\n/**\n * Rename `project_id` → `environment_id` on all metadata tables.\n *\n * @param driver An IDataDriver with access to the target database.\n * Must expose a raw query method: `driver.raw(sql, bindings?)`.\n * @returns Per-table migration results.\n */\nexport async function migrateProjectIdToEnvironmentId(\n driver: IDataDriver,\n): Promise<ProjectIdToEnvironmentIdResult[]> {\n const driverAny = driver as any;\n\n if (typeof driverAny.raw !== 'function') {\n throw new Error(\n 'migrateProjectIdToEnvironmentId: driver must expose a .raw(sql, bindings?) method. ' +\n 'SqlDriver (better-sqlite3/knex) and TursoDriver both support this.'\n );\n }\n\n const results: ProjectIdToEnvironmentIdResult[] = [];\n\n for (const table of AFFECTED_TABLES) {\n try {\n const hasColumn = await _columnExists(driverAny, table, 'project_id');\n const alreadyMigrated = await _columnExists(driverAny, table, 'environment_id');\n\n if (alreadyMigrated && !hasColumn) {\n results.push({ table, status: 'already_done' });\n continue;\n }\n\n if (!hasColumn) {\n results.push({ table, status: 'table_missing' });\n continue;\n }\n\n await driverAny.raw(\n `ALTER TABLE \"${table}\" RENAME COLUMN project_id TO environment_id`,\n );\n\n results.push({ table, status: 'renamed' });\n } catch (err: any) {\n results.push({ table, status: 'error', error: err?.message ?? String(err) });\n }\n }\n\n return results;\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nasync function _columnExists(driver: any, table: string, column: string): Promise<boolean> {\n try {\n const rows: any[] = await driver.raw(`PRAGMA table_info(\"${table}\")`);\n if (Array.isArray(rows) && rows.length > 0) {\n const list: any[] = Array.isArray(rows[0]) ? rows[0] : rows;\n return list.some((r: any) => r?.name === column);\n }\n\n const result: any[] = await driver.raw(\n `SELECT column_name FROM information_schema.columns WHERE table_name = ? AND column_name = ?`,\n [table, column],\n );\n const list: any[] = Array.isArray(result[0]) ? result[0] : result;\n return list.length > 0;\n } catch {\n return false;\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Migration: drop deprecated metadata projection tables.\n *\n * In 2026-05 the per-type projection tables (`sys_object` / `sys_view` /\n * `sys_flow` / `sys_agent` / `sys_tool`) and the corresponding\n * `MetadataProjector` were removed (see ADR 0005 addendum). All metadata\n * now lives as JSON inside `sys_metadata` — these projection tables are\n * dead weight on any existing database.\n *\n * This migration drops them if present. It is idempotent and safe to run\n * on databases that never had them (the `DROP TABLE IF EXISTS` is a no-op).\n *\n * Usage:\n * import { dropProjectionTables } from '@objectstack/metadata/migrations';\n * await dropProjectionTables(driver);\n */\n\nimport type { IDataDriver } from '@objectstack/spec/contracts';\n\nconst DEPRECATED_TABLES = [\n 'sys_object',\n 'sys_view',\n 'sys_flow',\n 'sys_agent',\n 'sys_tool',\n] as const;\n\nexport interface DropProjectionResult {\n table: string;\n status: 'dropped' | 'not_present' | 'error';\n error?: string;\n}\n\n/**\n * Drop the deprecated per-type metadata projection tables.\n *\n * @param driver An `IDataDriver` with `driver.raw(sql, bindings?)` access.\n * @returns Per-table results.\n */\nexport async function dropProjectionTables(driver: IDataDriver): Promise<DropProjectionResult[]> {\n const driverAny = driver as any;\n if (typeof driverAny.raw !== 'function') {\n throw new Error('dropProjectionTables: driver must expose a raw(sql) method');\n }\n\n const results: DropProjectionResult[] = [];\n for (const table of DEPRECATED_TABLES) {\n try {\n await driverAny.raw(`DROP TABLE IF EXISTS ${table}`);\n results.push({ table, status: 'dropped' });\n } catch (error) {\n results.push({\n table,\n status: 'error',\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n return results;\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Migration: ensure overlay-uniqueness index exists on `sys_metadata`.\n *\n * ADR-0005 Phase 1 — Overlay rows must be uniquely keyed by\n * `(type, name, organization_id, environment_id, scope)` for active rows only.\n * The previous `(type, name, environment_id)` unique constraint pre-dated\n * multi-tenant overlays and would incorrectly reject per-org customizations.\n *\n * Behaviour:\n * - SQLite / Postgres: creates a partial UNIQUE INDEX with `WHERE state = 'active'`.\n * - MySQL (no partial-index support): falls back to a non-unique composite index\n * plus an application-level guard (handled in `protocol.ts saveMetaItem`).\n * - Idempotent — uses `CREATE INDEX IF NOT EXISTS`. Safe to run on every boot.\n * - Best-effort: failures are recorded but never throw, so tenant boot is\n * not blocked on a database that doesn't support partial indexes.\n *\n * Usage:\n * import { addSysMetadataOverlayIndex } from '@objectstack/metadata/migrations';\n * await addSysMetadataOverlayIndex(driver);\n *\n * The `DatabaseLoader.ensureSchema()` invokes this automatically after the\n * `sys_metadata` table is created/synced, so most callers do not need to\n * invoke it directly.\n */\n\nimport type { IDataDriver } from '@objectstack/spec/contracts';\n\nconst INDEX_NAME = 'idx_sys_metadata_overlay_active';\nconst TABLE = 'sys_metadata';\nconst COLUMNS = '(type, name, organization_id, environment_id, scope)';\nconst WHERE = \"state = 'active'\";\n\nexport interface AddSysMetadataOverlayIndexResult {\n index: string;\n status: 'created' | 'already_exists' | 'fallback_non_unique' | 'unsupported' | 'error';\n error?: string;\n}\n\n/**\n * Ensure the overlay-uniqueness index exists on `sys_metadata`.\n *\n * @param driver An `IDataDriver` exposing a `raw(sql, bindings?)` method.\n */\nexport async function addSysMetadataOverlayIndex(\n driver: IDataDriver,\n): Promise<AddSysMetadataOverlayIndexResult> {\n const driverAny = driver as any;\n const exec = async (sql: string): Promise<void> => {\n if (typeof driverAny.raw === 'function') {\n await driverAny.raw(sql);\n } else if (typeof driverAny.execute === 'function') {\n await driverAny.execute(sql);\n } else {\n throw new Error('driver has neither raw nor execute');\n }\n };\n\n const partialSql = `CREATE UNIQUE INDEX IF NOT EXISTS ${INDEX_NAME} ON ${TABLE} ${COLUMNS} WHERE ${WHERE}`;\n const fallbackSql = `CREATE INDEX IF NOT EXISTS ${INDEX_NAME} ON ${TABLE} ${COLUMNS}`;\n\n try {\n await exec(partialSql);\n return { index: INDEX_NAME, status: 'created' };\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n\n // Partial-index unsupported (typically MySQL): fall back to a plain composite index.\n if (/partial|where clause|syntax/i.test(msg)) {\n try {\n await exec(fallbackSql);\n return { index: INDEX_NAME, status: 'fallback_non_unique' };\n } catch (fallbackErr) {\n return {\n index: INDEX_NAME,\n status: 'error',\n error:\n fallbackErr instanceof Error\n ? fallbackErr.message\n : String(fallbackErr),\n };\n }\n }\n\n if (/already exists/i.test(msg)) {\n return { index: INDEX_NAME, status: 'already_exists' };\n }\n\n return { index: INDEX_NAME, status: 'error', error: msg };\n }\n}\n"],"mappings":"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","names":["list","AFFECTED_TABLES","_columnExists","list"]}
1
+ {"version":3,"sources":["../../src/migrations/index.ts","../../src/migrations/migrate-env-id-to-project-id.ts","../../src/migrations/migrate-project-id-to-environment-id.ts","../../src/migrations/drop-projection-tables.ts","../../src/migrations/add-sys-metadata-overlay-index.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * @objectstack/metadata/migrations\n *\n * One-off database migrations for the metadata storage layer.\n */\n\nexport { migrateEnvIdToProjectId, type MigrationResult } from './migrate-env-id-to-project-id.js';\nexport {\n migrateProjectIdToEnvironmentId,\n type ProjectIdToEnvironmentIdResult,\n} from './migrate-project-id-to-environment-id.js';\nexport { dropProjectionTables, type DropProjectionResult } from './drop-projection-tables.js';\nexport {\n addSysMetadataOverlayIndex,\n type AddSysMetadataOverlayIndexResult,\n} from './add-sys-metadata-overlay-index.js';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Migration: env_id → project_id\n *\n * Renames the `env_id` column to `project_id` on the metadata storage tables:\n * - sys_metadata\n * - sys_metadata_history\n *\n * (The per-type projection tables `sys_object` / `sys_view` / `sys_flow` /\n * `sys_agent` / `sys_tool` were removed in 2026-05 along with the projection\n * pipeline — see ADR 0005 addendum. They are intentionally not included.)\n *\n * Safe to run multiple times (idempotent): checks for column existence before\n * attempting to rename. If `project_id` already exists, the step is skipped.\n *\n * Usage:\n * import { migrateEnvIdToProjectId } from '@objectstack/metadata/migrations';\n * await migrateEnvIdToProjectId(driver);\n */\n\nimport type { IDataDriver } from '@objectstack/spec/contracts';\n\nconst AFFECTED_TABLES = [\n 'sys_metadata',\n 'sys_metadata_history',\n] as const;\n\nexport interface MigrationResult {\n table: string;\n status: 'renamed' | 'already_done' | 'table_missing' | 'error';\n error?: string;\n}\n\n/**\n * Rename `env_id` → `project_id` on all metadata tables.\n *\n * @param driver An IDataDriver with access to the target database.\n * Must expose a raw query method: `driver.raw(sql, bindings?)`.\n * @returns Per-table migration results.\n */\nexport async function migrateEnvIdToProjectId(driver: IDataDriver): Promise<MigrationResult[]> {\n const driverAny = driver as any;\n\n if (typeof driverAny.raw !== 'function') {\n throw new Error(\n 'migrateEnvIdToProjectId: driver must expose a .raw(sql, bindings?) method. ' +\n 'SqlDriver (better-sqlite3/knex) supports this; cloud-side TursoDriver also conforms.'\n );\n }\n\n const results: MigrationResult[] = [];\n\n for (const table of AFFECTED_TABLES) {\n try {\n // Detect dialect: SQLite uses PRAGMA, others use information_schema.\n const hasColumn = await _columnExists(driverAny, table, 'env_id');\n const alreadyMigrated = await _columnExists(driverAny, table, 'project_id');\n\n if (alreadyMigrated && !hasColumn) {\n results.push({ table, status: 'already_done' });\n continue;\n }\n\n if (!hasColumn) {\n // Neither column exists — table might not exist yet.\n results.push({ table, status: 'table_missing' });\n continue;\n }\n\n // Perform the rename. SQLite ≥ 3.25.0 supports ALTER TABLE RENAME COLUMN.\n await driverAny.raw(`ALTER TABLE \"${table}\" RENAME COLUMN env_id TO project_id`);\n\n results.push({ table, status: 'renamed' });\n } catch (err: any) {\n results.push({ table, status: 'error', error: err?.message ?? String(err) });\n }\n }\n\n return results;\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nasync function _columnExists(driver: any, table: string, column: string): Promise<boolean> {\n try {\n // SQLite: PRAGMA table_info returns rows with `name` column.\n const rows: any[] = await driver.raw(`PRAGMA table_info(\"${table}\")`);\n if (Array.isArray(rows) && rows.length > 0) {\n // knex wraps PRAGMA result; handle both `rows` and `rows[0]` shapes.\n const list: any[] = Array.isArray(rows[0]) ? rows[0] : rows;\n return list.some((r: any) => r?.name === column);\n }\n\n // Fallback for non-SQLite: query information_schema.\n const result: any[] = await driver.raw(\n `SELECT column_name FROM information_schema.columns WHERE table_name = ? AND column_name = ?`,\n [table, column]\n );\n const list: any[] = Array.isArray(result[0]) ? result[0] : result;\n return list.length > 0;\n } catch {\n return false;\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Migration: project_id → environment_id\n *\n * Renames the `project_id` column to `environment_id` on the metadata\n * storage tables:\n * - sys_metadata\n * - sys_metadata_history\n *\n * Forward counterpart of {@link migrateEnvIdToProjectId} (which performed the\n * earlier `env_id → project_id` rename). Together they let an operator walk an\n * old schema all the way forward in two steps:\n *\n * migrateEnvIdToProjectId(driver); // env_id → project_id (legacy)\n * migrateProjectIdToEnvironmentId(driver); // project_id → environment_id (v5)\n *\n * (The per-type projection tables `sys_object` / `sys_view` / `sys_flow` /\n * `sys_agent` / `sys_tool` were removed in 2026-05 along with the projection\n * pipeline — see ADR 0005 addendum. They are intentionally not included.)\n *\n * Safe to run multiple times (idempotent): checks for column existence before\n * attempting to rename. If `environment_id` already exists, the step is\n * skipped.\n *\n * Usage:\n * import { migrateProjectIdToEnvironmentId } from '@objectstack/metadata/migrations';\n * await migrateProjectIdToEnvironmentId(driver);\n */\n\nimport type { IDataDriver } from '@objectstack/spec/contracts';\n\nconst AFFECTED_TABLES = [\n 'sys_metadata',\n 'sys_metadata_history',\n] as const;\n\nexport interface ProjectIdToEnvironmentIdResult {\n table: string;\n status: 'renamed' | 'already_done' | 'table_missing' | 'error';\n error?: string;\n}\n\n/**\n * Rename `project_id` → `environment_id` on all metadata tables.\n *\n * @param driver An IDataDriver with access to the target database.\n * Must expose a raw query method: `driver.raw(sql, bindings?)`.\n * @returns Per-table migration results.\n */\nexport async function migrateProjectIdToEnvironmentId(\n driver: IDataDriver,\n): Promise<ProjectIdToEnvironmentIdResult[]> {\n const driverAny = driver as any;\n\n if (typeof driverAny.raw !== 'function') {\n throw new Error(\n 'migrateProjectIdToEnvironmentId: driver must expose a .raw(sql, bindings?) method. ' +\n 'migrateProjectIdToEnvironmentId: driver must expose a .raw(sql, bindings?) method. ' +\n 'SqlDriver (better-sqlite3/knex) supports this; cloud-side TursoDriver also conforms.'\n );\n }\n\n const results: ProjectIdToEnvironmentIdResult[] = [];\n\n for (const table of AFFECTED_TABLES) {\n try {\n const hasColumn = await _columnExists(driverAny, table, 'project_id');\n const alreadyMigrated = await _columnExists(driverAny, table, 'environment_id');\n\n if (alreadyMigrated && !hasColumn) {\n results.push({ table, status: 'already_done' });\n continue;\n }\n\n if (!hasColumn) {\n results.push({ table, status: 'table_missing' });\n continue;\n }\n\n await driverAny.raw(\n `ALTER TABLE \"${table}\" RENAME COLUMN project_id TO environment_id`,\n );\n\n results.push({ table, status: 'renamed' });\n } catch (err: any) {\n results.push({ table, status: 'error', error: err?.message ?? String(err) });\n }\n }\n\n return results;\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nasync function _columnExists(driver: any, table: string, column: string): Promise<boolean> {\n try {\n const rows: any[] = await driver.raw(`PRAGMA table_info(\"${table}\")`);\n if (Array.isArray(rows) && rows.length > 0) {\n const list: any[] = Array.isArray(rows[0]) ? rows[0] : rows;\n return list.some((r: any) => r?.name === column);\n }\n\n const result: any[] = await driver.raw(\n `SELECT column_name FROM information_schema.columns WHERE table_name = ? AND column_name = ?`,\n [table, column],\n );\n const list: any[] = Array.isArray(result[0]) ? result[0] : result;\n return list.length > 0;\n } catch {\n return false;\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Migration: drop deprecated metadata projection tables.\n *\n * In 2026-05 the per-type projection tables (`sys_object` / `sys_view` /\n * `sys_flow` / `sys_agent` / `sys_tool`) and the corresponding\n * `MetadataProjector` were removed (see ADR 0005 addendum). All metadata\n * now lives as JSON inside `sys_metadata` — these projection tables are\n * dead weight on any existing database.\n *\n * This migration drops them if present. It is idempotent and safe to run\n * on databases that never had them (the `DROP TABLE IF EXISTS` is a no-op).\n *\n * Usage:\n * import { dropProjectionTables } from '@objectstack/metadata/migrations';\n * await dropProjectionTables(driver);\n */\n\nimport type { IDataDriver } from '@objectstack/spec/contracts';\n\nconst DEPRECATED_TABLES = [\n 'sys_object',\n 'sys_view',\n 'sys_flow',\n 'sys_agent',\n 'sys_tool',\n] as const;\n\nexport interface DropProjectionResult {\n table: string;\n status: 'dropped' | 'not_present' | 'error';\n error?: string;\n}\n\n/**\n * Drop the deprecated per-type metadata projection tables.\n *\n * @param driver An `IDataDriver` with `driver.raw(sql, bindings?)` access.\n * @returns Per-table results.\n */\nexport async function dropProjectionTables(driver: IDataDriver): Promise<DropProjectionResult[]> {\n const driverAny = driver as any;\n if (typeof driverAny.raw !== 'function') {\n throw new Error('dropProjectionTables: driver must expose a raw(sql) method');\n }\n\n const results: DropProjectionResult[] = [];\n for (const table of DEPRECATED_TABLES) {\n try {\n await driverAny.raw(`DROP TABLE IF EXISTS ${table}`);\n results.push({ table, status: 'dropped' });\n } catch (error) {\n results.push({\n table,\n status: 'error',\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n return results;\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Migration: ensure overlay-uniqueness index exists on `sys_metadata`.\n *\n * ADR-0005 Phase 1 — Overlay rows must be uniquely keyed by\n * `(type, name, organization_id, environment_id, scope)` for active rows only.\n * The previous `(type, name, environment_id)` unique constraint pre-dated\n * multi-tenant overlays and would incorrectly reject per-org customizations.\n *\n * Behaviour:\n * - SQLite / Postgres: creates a partial UNIQUE INDEX with `WHERE state = 'active'`.\n * - MySQL (no partial-index support): falls back to a non-unique composite index\n * plus an application-level guard (handled in `protocol.ts saveMetaItem`).\n * - Idempotent — uses `CREATE INDEX IF NOT EXISTS`. Safe to run on every boot.\n * - Best-effort: failures are recorded but never throw, so tenant boot is\n * not blocked on a database that doesn't support partial indexes.\n *\n * Usage:\n * import { addSysMetadataOverlayIndex } from '@objectstack/metadata/migrations';\n * await addSysMetadataOverlayIndex(driver);\n *\n * The `DatabaseLoader.ensureSchema()` invokes this automatically after the\n * `sys_metadata` table is created/synced, so most callers do not need to\n * invoke it directly.\n */\n\nimport type { IDataDriver } from '@objectstack/spec/contracts';\n\nconst INDEX_NAME = 'idx_sys_metadata_overlay_active';\nconst TABLE = 'sys_metadata';\nconst COLUMNS = '(type, name, organization_id, environment_id, scope)';\nconst WHERE = \"state = 'active'\";\n\nexport interface AddSysMetadataOverlayIndexResult {\n index: string;\n status: 'created' | 'already_exists' | 'fallback_non_unique' | 'unsupported' | 'error';\n error?: string;\n}\n\n/**\n * Ensure the overlay-uniqueness index exists on `sys_metadata`.\n *\n * @param driver An `IDataDriver` exposing a `raw(sql, bindings?)` method.\n */\nexport async function addSysMetadataOverlayIndex(\n driver: IDataDriver,\n): Promise<AddSysMetadataOverlayIndexResult> {\n const driverAny = driver as any;\n const exec = async (sql: string): Promise<void> => {\n if (typeof driverAny.raw === 'function') {\n await driverAny.raw(sql);\n } else if (typeof driverAny.execute === 'function') {\n await driverAny.execute(sql);\n } else {\n throw new Error('driver has neither raw nor execute');\n }\n };\n\n const partialSql = `CREATE UNIQUE INDEX IF NOT EXISTS ${INDEX_NAME} ON ${TABLE} ${COLUMNS} WHERE ${WHERE}`;\n const fallbackSql = `CREATE INDEX IF NOT EXISTS ${INDEX_NAME} ON ${TABLE} ${COLUMNS}`;\n\n try {\n await exec(partialSql);\n return { index: INDEX_NAME, status: 'created' };\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n\n // Partial-index unsupported (typically MySQL): fall back to a plain composite index.\n if (/partial|where clause|syntax/i.test(msg)) {\n try {\n await exec(fallbackSql);\n return { index: INDEX_NAME, status: 'fallback_non_unique' };\n } catch (fallbackErr) {\n return {\n index: INDEX_NAME,\n status: 'error',\n error:\n fallbackErr instanceof Error\n ? fallbackErr.message\n : String(fallbackErr),\n };\n }\n }\n\n if (/already exists/i.test(msg)) {\n return { index: INDEX_NAME, status: 'already_exists' };\n }\n\n return { index: INDEX_NAME, status: 'error', error: msg };\n }\n}\n"],"mappings":"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","names":["list","AFFECTED_TABLES","_columnExists","list"]}
@@ -7,7 +7,7 @@ async function migrateEnvIdToProjectId(driver) {
7
7
  const driverAny = driver;
8
8
  if (typeof driverAny.raw !== "function") {
9
9
  throw new Error(
10
- "migrateEnvIdToProjectId: driver must expose a .raw(sql, bindings?) method. SqlDriver (better-sqlite3/knex) and TursoDriver both support this."
10
+ "migrateEnvIdToProjectId: driver must expose a .raw(sql, bindings?) method. SqlDriver (better-sqlite3/knex) supports this; cloud-side TursoDriver also conforms."
11
11
  );
12
12
  }
13
13
  const results = [];
@@ -58,7 +58,7 @@ async function migrateProjectIdToEnvironmentId(driver) {
58
58
  const driverAny = driver;
59
59
  if (typeof driverAny.raw !== "function") {
60
60
  throw new Error(
61
- "migrateProjectIdToEnvironmentId: driver must expose a .raw(sql, bindings?) method. SqlDriver (better-sqlite3/knex) and TursoDriver both support this."
61
+ "migrateProjectIdToEnvironmentId: driver must expose a .raw(sql, bindings?) method. migrateProjectIdToEnvironmentId: driver must expose a .raw(sql, bindings?) method. SqlDriver (better-sqlite3/knex) supports this; cloud-side TursoDriver also conforms."
62
62
  );
63
63
  }
64
64
  const results = [];
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/migrations/migrate-env-id-to-project-id.ts","../../src/migrations/migrate-project-id-to-environment-id.ts","../../src/migrations/drop-projection-tables.ts","../../src/migrations/add-sys-metadata-overlay-index.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Migration: env_id → project_id\n *\n * Renames the `env_id` column to `project_id` on the metadata storage tables:\n * - sys_metadata\n * - sys_metadata_history\n *\n * (The per-type projection tables `sys_object` / `sys_view` / `sys_flow` /\n * `sys_agent` / `sys_tool` were removed in 2026-05 along with the projection\n * pipeline — see ADR 0005 addendum. They are intentionally not included.)\n *\n * Safe to run multiple times (idempotent): checks for column existence before\n * attempting to rename. If `project_id` already exists, the step is skipped.\n *\n * Usage:\n * import { migrateEnvIdToProjectId } from '@objectstack/metadata/migrations';\n * await migrateEnvIdToProjectId(driver);\n */\n\nimport type { IDataDriver } from '@objectstack/spec/contracts';\n\nconst AFFECTED_TABLES = [\n 'sys_metadata',\n 'sys_metadata_history',\n] as const;\n\nexport interface MigrationResult {\n table: string;\n status: 'renamed' | 'already_done' | 'table_missing' | 'error';\n error?: string;\n}\n\n/**\n * Rename `env_id` → `project_id` on all metadata tables.\n *\n * @param driver An IDataDriver with access to the target database.\n * Must expose a raw query method: `driver.raw(sql, bindings?)`.\n * @returns Per-table migration results.\n */\nexport async function migrateEnvIdToProjectId(driver: IDataDriver): Promise<MigrationResult[]> {\n const driverAny = driver as any;\n\n if (typeof driverAny.raw !== 'function') {\n throw new Error(\n 'migrateEnvIdToProjectId: driver must expose a .raw(sql, bindings?) method. ' +\n 'SqlDriver (better-sqlite3/knex) and TursoDriver both support this.'\n );\n }\n\n const results: MigrationResult[] = [];\n\n for (const table of AFFECTED_TABLES) {\n try {\n // Detect dialect: SQLite uses PRAGMA, others use information_schema.\n const hasColumn = await _columnExists(driverAny, table, 'env_id');\n const alreadyMigrated = await _columnExists(driverAny, table, 'project_id');\n\n if (alreadyMigrated && !hasColumn) {\n results.push({ table, status: 'already_done' });\n continue;\n }\n\n if (!hasColumn) {\n // Neither column exists — table might not exist yet.\n results.push({ table, status: 'table_missing' });\n continue;\n }\n\n // Perform the rename. SQLite ≥ 3.25.0 supports ALTER TABLE RENAME COLUMN.\n await driverAny.raw(`ALTER TABLE \"${table}\" RENAME COLUMN env_id TO project_id`);\n\n results.push({ table, status: 'renamed' });\n } catch (err: any) {\n results.push({ table, status: 'error', error: err?.message ?? String(err) });\n }\n }\n\n return results;\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nasync function _columnExists(driver: any, table: string, column: string): Promise<boolean> {\n try {\n // SQLite: PRAGMA table_info returns rows with `name` column.\n const rows: any[] = await driver.raw(`PRAGMA table_info(\"${table}\")`);\n if (Array.isArray(rows) && rows.length > 0) {\n // knex wraps PRAGMA result; handle both `rows` and `rows[0]` shapes.\n const list: any[] = Array.isArray(rows[0]) ? rows[0] : rows;\n return list.some((r: any) => r?.name === column);\n }\n\n // Fallback for non-SQLite: query information_schema.\n const result: any[] = await driver.raw(\n `SELECT column_name FROM information_schema.columns WHERE table_name = ? AND column_name = ?`,\n [table, column]\n );\n const list: any[] = Array.isArray(result[0]) ? result[0] : result;\n return list.length > 0;\n } catch {\n return false;\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Migration: project_id → environment_id\n *\n * Renames the `project_id` column to `environment_id` on the metadata\n * storage tables:\n * - sys_metadata\n * - sys_metadata_history\n *\n * Forward counterpart of {@link migrateEnvIdToProjectId} (which performed the\n * earlier `env_id → project_id` rename). Together they let an operator walk an\n * old schema all the way forward in two steps:\n *\n * migrateEnvIdToProjectId(driver); // env_id → project_id (legacy)\n * migrateProjectIdToEnvironmentId(driver); // project_id → environment_id (v5)\n *\n * (The per-type projection tables `sys_object` / `sys_view` / `sys_flow` /\n * `sys_agent` / `sys_tool` were removed in 2026-05 along with the projection\n * pipeline — see ADR 0005 addendum. They are intentionally not included.)\n *\n * Safe to run multiple times (idempotent): checks for column existence before\n * attempting to rename. If `environment_id` already exists, the step is\n * skipped.\n *\n * Usage:\n * import { migrateProjectIdToEnvironmentId } from '@objectstack/metadata/migrations';\n * await migrateProjectIdToEnvironmentId(driver);\n */\n\nimport type { IDataDriver } from '@objectstack/spec/contracts';\n\nconst AFFECTED_TABLES = [\n 'sys_metadata',\n 'sys_metadata_history',\n] as const;\n\nexport interface ProjectIdToEnvironmentIdResult {\n table: string;\n status: 'renamed' | 'already_done' | 'table_missing' | 'error';\n error?: string;\n}\n\n/**\n * Rename `project_id` → `environment_id` on all metadata tables.\n *\n * @param driver An IDataDriver with access to the target database.\n * Must expose a raw query method: `driver.raw(sql, bindings?)`.\n * @returns Per-table migration results.\n */\nexport async function migrateProjectIdToEnvironmentId(\n driver: IDataDriver,\n): Promise<ProjectIdToEnvironmentIdResult[]> {\n const driverAny = driver as any;\n\n if (typeof driverAny.raw !== 'function') {\n throw new Error(\n 'migrateProjectIdToEnvironmentId: driver must expose a .raw(sql, bindings?) method. ' +\n 'SqlDriver (better-sqlite3/knex) and TursoDriver both support this.'\n );\n }\n\n const results: ProjectIdToEnvironmentIdResult[] = [];\n\n for (const table of AFFECTED_TABLES) {\n try {\n const hasColumn = await _columnExists(driverAny, table, 'project_id');\n const alreadyMigrated = await _columnExists(driverAny, table, 'environment_id');\n\n if (alreadyMigrated && !hasColumn) {\n results.push({ table, status: 'already_done' });\n continue;\n }\n\n if (!hasColumn) {\n results.push({ table, status: 'table_missing' });\n continue;\n }\n\n await driverAny.raw(\n `ALTER TABLE \"${table}\" RENAME COLUMN project_id TO environment_id`,\n );\n\n results.push({ table, status: 'renamed' });\n } catch (err: any) {\n results.push({ table, status: 'error', error: err?.message ?? String(err) });\n }\n }\n\n return results;\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nasync function _columnExists(driver: any, table: string, column: string): Promise<boolean> {\n try {\n const rows: any[] = await driver.raw(`PRAGMA table_info(\"${table}\")`);\n if (Array.isArray(rows) && rows.length > 0) {\n const list: any[] = Array.isArray(rows[0]) ? rows[0] : rows;\n return list.some((r: any) => r?.name === column);\n }\n\n const result: any[] = await driver.raw(\n `SELECT column_name FROM information_schema.columns WHERE table_name = ? AND column_name = ?`,\n [table, column],\n );\n const list: any[] = Array.isArray(result[0]) ? result[0] : result;\n return list.length > 0;\n } catch {\n return false;\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Migration: drop deprecated metadata projection tables.\n *\n * In 2026-05 the per-type projection tables (`sys_object` / `sys_view` /\n * `sys_flow` / `sys_agent` / `sys_tool`) and the corresponding\n * `MetadataProjector` were removed (see ADR 0005 addendum). All metadata\n * now lives as JSON inside `sys_metadata` — these projection tables are\n * dead weight on any existing database.\n *\n * This migration drops them if present. It is idempotent and safe to run\n * on databases that never had them (the `DROP TABLE IF EXISTS` is a no-op).\n *\n * Usage:\n * import { dropProjectionTables } from '@objectstack/metadata/migrations';\n * await dropProjectionTables(driver);\n */\n\nimport type { IDataDriver } from '@objectstack/spec/contracts';\n\nconst DEPRECATED_TABLES = [\n 'sys_object',\n 'sys_view',\n 'sys_flow',\n 'sys_agent',\n 'sys_tool',\n] as const;\n\nexport interface DropProjectionResult {\n table: string;\n status: 'dropped' | 'not_present' | 'error';\n error?: string;\n}\n\n/**\n * Drop the deprecated per-type metadata projection tables.\n *\n * @param driver An `IDataDriver` with `driver.raw(sql, bindings?)` access.\n * @returns Per-table results.\n */\nexport async function dropProjectionTables(driver: IDataDriver): Promise<DropProjectionResult[]> {\n const driverAny = driver as any;\n if (typeof driverAny.raw !== 'function') {\n throw new Error('dropProjectionTables: driver must expose a raw(sql) method');\n }\n\n const results: DropProjectionResult[] = [];\n for (const table of DEPRECATED_TABLES) {\n try {\n await driverAny.raw(`DROP TABLE IF EXISTS ${table}`);\n results.push({ table, status: 'dropped' });\n } catch (error) {\n results.push({\n table,\n status: 'error',\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n return results;\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Migration: ensure overlay-uniqueness index exists on `sys_metadata`.\n *\n * ADR-0005 Phase 1 — Overlay rows must be uniquely keyed by\n * `(type, name, organization_id, environment_id, scope)` for active rows only.\n * The previous `(type, name, environment_id)` unique constraint pre-dated\n * multi-tenant overlays and would incorrectly reject per-org customizations.\n *\n * Behaviour:\n * - SQLite / Postgres: creates a partial UNIQUE INDEX with `WHERE state = 'active'`.\n * - MySQL (no partial-index support): falls back to a non-unique composite index\n * plus an application-level guard (handled in `protocol.ts saveMetaItem`).\n * - Idempotent — uses `CREATE INDEX IF NOT EXISTS`. Safe to run on every boot.\n * - Best-effort: failures are recorded but never throw, so tenant boot is\n * not blocked on a database that doesn't support partial indexes.\n *\n * Usage:\n * import { addSysMetadataOverlayIndex } from '@objectstack/metadata/migrations';\n * await addSysMetadataOverlayIndex(driver);\n *\n * The `DatabaseLoader.ensureSchema()` invokes this automatically after the\n * `sys_metadata` table is created/synced, so most callers do not need to\n * invoke it directly.\n */\n\nimport type { IDataDriver } from '@objectstack/spec/contracts';\n\nconst INDEX_NAME = 'idx_sys_metadata_overlay_active';\nconst TABLE = 'sys_metadata';\nconst COLUMNS = '(type, name, organization_id, environment_id, scope)';\nconst WHERE = \"state = 'active'\";\n\nexport interface AddSysMetadataOverlayIndexResult {\n index: string;\n status: 'created' | 'already_exists' | 'fallback_non_unique' | 'unsupported' | 'error';\n error?: string;\n}\n\n/**\n * Ensure the overlay-uniqueness index exists on `sys_metadata`.\n *\n * @param driver An `IDataDriver` exposing a `raw(sql, bindings?)` method.\n */\nexport async function addSysMetadataOverlayIndex(\n driver: IDataDriver,\n): Promise<AddSysMetadataOverlayIndexResult> {\n const driverAny = driver as any;\n const exec = async (sql: string): Promise<void> => {\n if (typeof driverAny.raw === 'function') {\n await driverAny.raw(sql);\n } else if (typeof driverAny.execute === 'function') {\n await driverAny.execute(sql);\n } else {\n throw new Error('driver has neither raw nor execute');\n }\n };\n\n const partialSql = `CREATE UNIQUE INDEX IF NOT EXISTS ${INDEX_NAME} ON ${TABLE} ${COLUMNS} WHERE ${WHERE}`;\n const fallbackSql = `CREATE INDEX IF NOT EXISTS ${INDEX_NAME} ON ${TABLE} ${COLUMNS}`;\n\n try {\n await exec(partialSql);\n return { index: INDEX_NAME, status: 'created' };\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n\n // Partial-index unsupported (typically MySQL): fall back to a plain composite index.\n if (/partial|where clause|syntax/i.test(msg)) {\n try {\n await exec(fallbackSql);\n return { index: INDEX_NAME, status: 'fallback_non_unique' };\n } catch (fallbackErr) {\n return {\n index: INDEX_NAME,\n status: 'error',\n error:\n fallbackErr instanceof Error\n ? fallbackErr.message\n : String(fallbackErr),\n };\n }\n }\n\n if (/already exists/i.test(msg)) {\n return { index: INDEX_NAME, status: 'already_exists' };\n }\n\n return { index: INDEX_NAME, status: 'error', error: msg };\n }\n}\n"],"mappings":"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","names":["list","AFFECTED_TABLES","_columnExists","list"]}
1
+ {"version":3,"sources":["../../src/migrations/migrate-env-id-to-project-id.ts","../../src/migrations/migrate-project-id-to-environment-id.ts","../../src/migrations/drop-projection-tables.ts","../../src/migrations/add-sys-metadata-overlay-index.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Migration: env_id → project_id\n *\n * Renames the `env_id` column to `project_id` on the metadata storage tables:\n * - sys_metadata\n * - sys_metadata_history\n *\n * (The per-type projection tables `sys_object` / `sys_view` / `sys_flow` /\n * `sys_agent` / `sys_tool` were removed in 2026-05 along with the projection\n * pipeline — see ADR 0005 addendum. They are intentionally not included.)\n *\n * Safe to run multiple times (idempotent): checks for column existence before\n * attempting to rename. If `project_id` already exists, the step is skipped.\n *\n * Usage:\n * import { migrateEnvIdToProjectId } from '@objectstack/metadata/migrations';\n * await migrateEnvIdToProjectId(driver);\n */\n\nimport type { IDataDriver } from '@objectstack/spec/contracts';\n\nconst AFFECTED_TABLES = [\n 'sys_metadata',\n 'sys_metadata_history',\n] as const;\n\nexport interface MigrationResult {\n table: string;\n status: 'renamed' | 'already_done' | 'table_missing' | 'error';\n error?: string;\n}\n\n/**\n * Rename `env_id` → `project_id` on all metadata tables.\n *\n * @param driver An IDataDriver with access to the target database.\n * Must expose a raw query method: `driver.raw(sql, bindings?)`.\n * @returns Per-table migration results.\n */\nexport async function migrateEnvIdToProjectId(driver: IDataDriver): Promise<MigrationResult[]> {\n const driverAny = driver as any;\n\n if (typeof driverAny.raw !== 'function') {\n throw new Error(\n 'migrateEnvIdToProjectId: driver must expose a .raw(sql, bindings?) method. ' +\n 'SqlDriver (better-sqlite3/knex) supports this; cloud-side TursoDriver also conforms.'\n );\n }\n\n const results: MigrationResult[] = [];\n\n for (const table of AFFECTED_TABLES) {\n try {\n // Detect dialect: SQLite uses PRAGMA, others use information_schema.\n const hasColumn = await _columnExists(driverAny, table, 'env_id');\n const alreadyMigrated = await _columnExists(driverAny, table, 'project_id');\n\n if (alreadyMigrated && !hasColumn) {\n results.push({ table, status: 'already_done' });\n continue;\n }\n\n if (!hasColumn) {\n // Neither column exists — table might not exist yet.\n results.push({ table, status: 'table_missing' });\n continue;\n }\n\n // Perform the rename. SQLite ≥ 3.25.0 supports ALTER TABLE RENAME COLUMN.\n await driverAny.raw(`ALTER TABLE \"${table}\" RENAME COLUMN env_id TO project_id`);\n\n results.push({ table, status: 'renamed' });\n } catch (err: any) {\n results.push({ table, status: 'error', error: err?.message ?? String(err) });\n }\n }\n\n return results;\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nasync function _columnExists(driver: any, table: string, column: string): Promise<boolean> {\n try {\n // SQLite: PRAGMA table_info returns rows with `name` column.\n const rows: any[] = await driver.raw(`PRAGMA table_info(\"${table}\")`);\n if (Array.isArray(rows) && rows.length > 0) {\n // knex wraps PRAGMA result; handle both `rows` and `rows[0]` shapes.\n const list: any[] = Array.isArray(rows[0]) ? rows[0] : rows;\n return list.some((r: any) => r?.name === column);\n }\n\n // Fallback for non-SQLite: query information_schema.\n const result: any[] = await driver.raw(\n `SELECT column_name FROM information_schema.columns WHERE table_name = ? AND column_name = ?`,\n [table, column]\n );\n const list: any[] = Array.isArray(result[0]) ? result[0] : result;\n return list.length > 0;\n } catch {\n return false;\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Migration: project_id → environment_id\n *\n * Renames the `project_id` column to `environment_id` on the metadata\n * storage tables:\n * - sys_metadata\n * - sys_metadata_history\n *\n * Forward counterpart of {@link migrateEnvIdToProjectId} (which performed the\n * earlier `env_id → project_id` rename). Together they let an operator walk an\n * old schema all the way forward in two steps:\n *\n * migrateEnvIdToProjectId(driver); // env_id → project_id (legacy)\n * migrateProjectIdToEnvironmentId(driver); // project_id → environment_id (v5)\n *\n * (The per-type projection tables `sys_object` / `sys_view` / `sys_flow` /\n * `sys_agent` / `sys_tool` were removed in 2026-05 along with the projection\n * pipeline — see ADR 0005 addendum. They are intentionally not included.)\n *\n * Safe to run multiple times (idempotent): checks for column existence before\n * attempting to rename. If `environment_id` already exists, the step is\n * skipped.\n *\n * Usage:\n * import { migrateProjectIdToEnvironmentId } from '@objectstack/metadata/migrations';\n * await migrateProjectIdToEnvironmentId(driver);\n */\n\nimport type { IDataDriver } from '@objectstack/spec/contracts';\n\nconst AFFECTED_TABLES = [\n 'sys_metadata',\n 'sys_metadata_history',\n] as const;\n\nexport interface ProjectIdToEnvironmentIdResult {\n table: string;\n status: 'renamed' | 'already_done' | 'table_missing' | 'error';\n error?: string;\n}\n\n/**\n * Rename `project_id` → `environment_id` on all metadata tables.\n *\n * @param driver An IDataDriver with access to the target database.\n * Must expose a raw query method: `driver.raw(sql, bindings?)`.\n * @returns Per-table migration results.\n */\nexport async function migrateProjectIdToEnvironmentId(\n driver: IDataDriver,\n): Promise<ProjectIdToEnvironmentIdResult[]> {\n const driverAny = driver as any;\n\n if (typeof driverAny.raw !== 'function') {\n throw new Error(\n 'migrateProjectIdToEnvironmentId: driver must expose a .raw(sql, bindings?) method. ' +\n 'migrateProjectIdToEnvironmentId: driver must expose a .raw(sql, bindings?) method. ' +\n 'SqlDriver (better-sqlite3/knex) supports this; cloud-side TursoDriver also conforms.'\n );\n }\n\n const results: ProjectIdToEnvironmentIdResult[] = [];\n\n for (const table of AFFECTED_TABLES) {\n try {\n const hasColumn = await _columnExists(driverAny, table, 'project_id');\n const alreadyMigrated = await _columnExists(driverAny, table, 'environment_id');\n\n if (alreadyMigrated && !hasColumn) {\n results.push({ table, status: 'already_done' });\n continue;\n }\n\n if (!hasColumn) {\n results.push({ table, status: 'table_missing' });\n continue;\n }\n\n await driverAny.raw(\n `ALTER TABLE \"${table}\" RENAME COLUMN project_id TO environment_id`,\n );\n\n results.push({ table, status: 'renamed' });\n } catch (err: any) {\n results.push({ table, status: 'error', error: err?.message ?? String(err) });\n }\n }\n\n return results;\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nasync function _columnExists(driver: any, table: string, column: string): Promise<boolean> {\n try {\n const rows: any[] = await driver.raw(`PRAGMA table_info(\"${table}\")`);\n if (Array.isArray(rows) && rows.length > 0) {\n const list: any[] = Array.isArray(rows[0]) ? rows[0] : rows;\n return list.some((r: any) => r?.name === column);\n }\n\n const result: any[] = await driver.raw(\n `SELECT column_name FROM information_schema.columns WHERE table_name = ? AND column_name = ?`,\n [table, column],\n );\n const list: any[] = Array.isArray(result[0]) ? result[0] : result;\n return list.length > 0;\n } catch {\n return false;\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Migration: drop deprecated metadata projection tables.\n *\n * In 2026-05 the per-type projection tables (`sys_object` / `sys_view` /\n * `sys_flow` / `sys_agent` / `sys_tool`) and the corresponding\n * `MetadataProjector` were removed (see ADR 0005 addendum). All metadata\n * now lives as JSON inside `sys_metadata` — these projection tables are\n * dead weight on any existing database.\n *\n * This migration drops them if present. It is idempotent and safe to run\n * on databases that never had them (the `DROP TABLE IF EXISTS` is a no-op).\n *\n * Usage:\n * import { dropProjectionTables } from '@objectstack/metadata/migrations';\n * await dropProjectionTables(driver);\n */\n\nimport type { IDataDriver } from '@objectstack/spec/contracts';\n\nconst DEPRECATED_TABLES = [\n 'sys_object',\n 'sys_view',\n 'sys_flow',\n 'sys_agent',\n 'sys_tool',\n] as const;\n\nexport interface DropProjectionResult {\n table: string;\n status: 'dropped' | 'not_present' | 'error';\n error?: string;\n}\n\n/**\n * Drop the deprecated per-type metadata projection tables.\n *\n * @param driver An `IDataDriver` with `driver.raw(sql, bindings?)` access.\n * @returns Per-table results.\n */\nexport async function dropProjectionTables(driver: IDataDriver): Promise<DropProjectionResult[]> {\n const driverAny = driver as any;\n if (typeof driverAny.raw !== 'function') {\n throw new Error('dropProjectionTables: driver must expose a raw(sql) method');\n }\n\n const results: DropProjectionResult[] = [];\n for (const table of DEPRECATED_TABLES) {\n try {\n await driverAny.raw(`DROP TABLE IF EXISTS ${table}`);\n results.push({ table, status: 'dropped' });\n } catch (error) {\n results.push({\n table,\n status: 'error',\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n return results;\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Migration: ensure overlay-uniqueness index exists on `sys_metadata`.\n *\n * ADR-0005 Phase 1 — Overlay rows must be uniquely keyed by\n * `(type, name, organization_id, environment_id, scope)` for active rows only.\n * The previous `(type, name, environment_id)` unique constraint pre-dated\n * multi-tenant overlays and would incorrectly reject per-org customizations.\n *\n * Behaviour:\n * - SQLite / Postgres: creates a partial UNIQUE INDEX with `WHERE state = 'active'`.\n * - MySQL (no partial-index support): falls back to a non-unique composite index\n * plus an application-level guard (handled in `protocol.ts saveMetaItem`).\n * - Idempotent — uses `CREATE INDEX IF NOT EXISTS`. Safe to run on every boot.\n * - Best-effort: failures are recorded but never throw, so tenant boot is\n * not blocked on a database that doesn't support partial indexes.\n *\n * Usage:\n * import { addSysMetadataOverlayIndex } from '@objectstack/metadata/migrations';\n * await addSysMetadataOverlayIndex(driver);\n *\n * The `DatabaseLoader.ensureSchema()` invokes this automatically after the\n * `sys_metadata` table is created/synced, so most callers do not need to\n * invoke it directly.\n */\n\nimport type { IDataDriver } from '@objectstack/spec/contracts';\n\nconst INDEX_NAME = 'idx_sys_metadata_overlay_active';\nconst TABLE = 'sys_metadata';\nconst COLUMNS = '(type, name, organization_id, environment_id, scope)';\nconst WHERE = \"state = 'active'\";\n\nexport interface AddSysMetadataOverlayIndexResult {\n index: string;\n status: 'created' | 'already_exists' | 'fallback_non_unique' | 'unsupported' | 'error';\n error?: string;\n}\n\n/**\n * Ensure the overlay-uniqueness index exists on `sys_metadata`.\n *\n * @param driver An `IDataDriver` exposing a `raw(sql, bindings?)` method.\n */\nexport async function addSysMetadataOverlayIndex(\n driver: IDataDriver,\n): Promise<AddSysMetadataOverlayIndexResult> {\n const driverAny = driver as any;\n const exec = async (sql: string): Promise<void> => {\n if (typeof driverAny.raw === 'function') {\n await driverAny.raw(sql);\n } else if (typeof driverAny.execute === 'function') {\n await driverAny.execute(sql);\n } else {\n throw new Error('driver has neither raw nor execute');\n }\n };\n\n const partialSql = `CREATE UNIQUE INDEX IF NOT EXISTS ${INDEX_NAME} ON ${TABLE} ${COLUMNS} WHERE ${WHERE}`;\n const fallbackSql = `CREATE INDEX IF NOT EXISTS ${INDEX_NAME} ON ${TABLE} ${COLUMNS}`;\n\n try {\n await exec(partialSql);\n return { index: INDEX_NAME, status: 'created' };\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n\n // Partial-index unsupported (typically MySQL): fall back to a plain composite index.\n if (/partial|where clause|syntax/i.test(msg)) {\n try {\n await exec(fallbackSql);\n return { index: INDEX_NAME, status: 'fallback_non_unique' };\n } catch (fallbackErr) {\n return {\n index: INDEX_NAME,\n status: 'error',\n error:\n fallbackErr instanceof Error\n ? fallbackErr.message\n : String(fallbackErr),\n };\n }\n }\n\n if (/already exists/i.test(msg)) {\n return { index: INDEX_NAME, status: 'already_exists' };\n }\n\n return { index: INDEX_NAME, status: 'error', error: msg };\n }\n}\n"],"mappings":"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","names":["list","AFFECTED_TABLES","_columnExists","list"]}
package/dist/node.cjs CHANGED
@@ -596,7 +596,7 @@ async function migrateProjectIdToEnvironmentId(driver) {
596
596
  const driverAny = driver;
597
597
  if (typeof driverAny.raw !== "function") {
598
598
  throw new Error(
599
- "migrateProjectIdToEnvironmentId: driver must expose a .raw(sql, bindings?) method. SqlDriver (better-sqlite3/knex) and TursoDriver both support this."
599
+ "migrateProjectIdToEnvironmentId: driver must expose a .raw(sql, bindings?) method. migrateProjectIdToEnvironmentId: driver must expose a .raw(sql, bindings?) method. SqlDriver (better-sqlite3/knex) supports this; cloud-side TursoDriver also conforms."
600
600
  );
601
601
  }
602
602
  const results = [];