deckhearth/migrations/1781000000003_reconcile-collaboration-tables.js

106 lines
4.4 KiB
JavaScript
Raw Permalink Normal View History

/**
* Reconcile collaboration tables missed by the initial-schema backfill.
*
* Captures the DDL half of `scripts/add-collaboration-features.js` (a
* historical no-go-zone script) that the `initial-schema` backfill missed:
*
* 1. `collection_permissions` (collaboration roles + invite tokens)
* 2. `collection_activity` (audit-trail with JSONB details)
* 3. `users.is_pending` (column for invited-but-not-yet-accepted users)
* 4. 3 indexes on the tables this migration creates
*
* `idx_collections_visibility` is NOT created here. This file's timestamp
* runs before `1781442330002_reconcile-collections-columns`, which is the
* migration that adds `collections.visibility`. Creating the index here
* fails on a fresh CI database (`column "visibility" does not exist`).
* The index ships in `1786757312884_add-collections-visibility-index`.
*
* `lib/permission-middleware.js` reads `collection_permissions` in
* `withCollectionPermission` and writes `collection_activity` from
* `logCollectionActivity` both are runtime invariants, so a fresh Neon
* branch onboarded by `npm run setup-db` MUST land these tables. Prod was
* brought to this shape by `add-collaboration-features.js` running
* historically; this migration brings fresh envs to parity per
* `.convoys/reconcile-historical-add-scripts.md` § Brief outline B3.
*
* Idempotent against:
* - A brand-new Neon branch (creates everything fresh).
* - Any long-lived env where `add-collaboration-features.js` already ran
* (every CREATE TABLE / ADD COLUMN / CREATE INDEX is `IF NOT EXISTS`-
* guarded no-op except recording the `pgmigrations` row).
*
* The owner-permission DML backfill from the historical script is NOT
* captured: a fresh env has no pre-existing collections needing
* back-fill, and prod's backfill is already applied.
*
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
*/
export const shorthands = undefined;
/**
* @param {import('node-pg-migrate').MigrationBuilder} pgm
* @returns {void}
*/
export const up = (pgm) => {
pgm.sql(`
CREATE TABLE IF NOT EXISTS collection_permissions (
id SERIAL PRIMARY KEY,
collection_id INTEGER REFERENCES collections(id) ON DELETE CASCADE,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
role VARCHAR(20) NOT NULL CHECK (role IN ('owner', 'editor', 'viewer')),
status VARCHAR(20) DEFAULT 'active' CHECK (status IN ('active', 'pending', 'declined')),
invite_token VARCHAR(255) UNIQUE,
invited_by INTEGER REFERENCES users(id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(collection_id, user_id)
)
`);
pgm.sql(`
CREATE TABLE IF NOT EXISTS collection_activity (
id SERIAL PRIMARY KEY,
collection_id INTEGER REFERENCES collections(id) ON DELETE CASCADE,
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
action VARCHAR(50) NOT NULL,
details JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
pgm.sql(`
ALTER TABLE users
ADD COLUMN IF NOT EXISTS is_pending BOOLEAN DEFAULT false
`);
pgm.sql(`
CREATE INDEX IF NOT EXISTS idx_collection_permissions_collection_id
ON collection_permissions(collection_id);
CREATE INDEX IF NOT EXISTS idx_collection_permissions_user_id
ON collection_permissions(user_id);
CREATE INDEX IF NOT EXISTS idx_collection_activity_collection_id
ON collection_activity(collection_id);
`);
};
/**
* Down-migration is a hard stub. Dropping `collection_permissions` /
* `collection_activity` would erase every collaboration role assignment
* and every audit-trail row in the database; dropping `users.is_pending`
* would discard the invited-user state for every pending invite. If a
* future schema correction needs to mutate one of these surfaces, write a
* NEW dated migration with a real `down` do NOT remove this stub.
*
* @returns {void}
*/
export const down = () => {
throw new Error(
'[migration:1781000000003_reconcile-collaboration-tables] Refusing to drop ' +
'collection_permissions / collection_activity / users.is_pending. Rolling back ' +
'this migration would erase every collaboration role assignment, every audit-trail ' +
'row, and every invited-user pending state in the database. If you need a clean ' +
'schema for testing, branch the Neon database and run `npm run migrate up` against ' +
'the branch instead.'
);
};