14 KiB
| convoy | brief_number | depends_on | files | |
|---|---|---|---|---|
| reconcile-historical-add-scripts | 3 |
|
Brief 3: Reconcile collaboration tables into migration history
Goal (1 sentence)
Capture the collection_permissions + collection_activity table creation, the users.is_pending column, and the 3 related indexes from scripts/add-collaboration-features.js into a single new node-pg-migrate migration so a brand-new Neon branch ends up with the collaboration / sharing surface after npm run setup-db.
Scope (files in scope — do not edit anything else)
migrations/1781000000003_reconcile-collaboration-tables.js— new
Source script (read-only audit reference; DO NOT EDIT — no-go-zone)
scripts/add-collaboration-features.js lines 24-81 (relevant DDL only; the collections-columns half is reconciled by Brief 2):
// collection_permissions table
await 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)
)
`;
// collection_activity table
await 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
)
`;
// users.is_pending column
await sql`
ALTER TABLE users
ADD COLUMN IF NOT EXISTS is_pending BOOLEAN DEFAULT false
`;
// 3 indexes (the 4th — idx_collections_visibility — is Brief 2's scope)
await sql`
CREATE INDEX IF NOT EXISTS idx_collection_permissions_collection_id
ON collection_permissions(collection_id)
`;
await sql`
CREATE INDEX IF NOT EXISTS idx_collection_permissions_user_id
ON collection_permissions(user_id)
`;
await sql`
CREATE INDEX IF NOT EXISTS idx_collection_activity_collection_id
ON collection_activity(collection_id)
`;
Owner-permission backfill DML at lines 84-99 inserts ('owner', 'active') rows for every pre-existing collection. Do NOT fold the backfill DML into this migration. Fresh envs have no pre-existing collections needing backfill; on prod, the backfill ran once historically and is baked in. The current runtime invariant for new collection creation lives in pages/api/collections.js (verify post-merge if needed — out of scope for this brief).
Target migration file
Path: migrations/1781000000003_reconcile-collaboration-tables.js
Contents:
/**
* Reconcile the collaboration / sharing half of
* scripts/add-collaboration-features.js into the migration history:
*
* - CREATE TABLE collection_permissions (with role/status CHECK
* constraints inline + invite_token UNIQUE + UNIQUE(collection_id,
* user_id))
* - CREATE TABLE collection_activity (with JSONB details column)
* - ALTER users ADD COLUMN is_pending BOOLEAN DEFAULT false
* - 3 indexes (idx_collection_permissions_collection_id,
* idx_collection_permissions_user_id,
* idx_collection_activity_collection_id)
*
* The 4th index from the source script (idx_collections_visibility)
* is reconciled by migrations/1781000000002_reconcile-collections-columns.js
* because it indexes a column added in that brief.
*
* The collections-columns half (visibility, tcg, tags) of
* add-collaboration-features.js is reconciled by
* migrations/1781000000002_reconcile-collections-columns.js.
*
* The owner-permission backfill DML from the source script (INSERT
* INTO collection_permissions ... 'owner', 'active' for every
* pre-existing collection) is intentionally NOT folded in — fresh
* envs have no pre-existing collections to backfill; on prod, the
* backfill ran once historically and is baked in. The runtime
* invariant for owner-permission creation on new collections is the
* responsibility of pages/api/collections.js (out of scope here).
*
* Both CREATE TABLE statements use IF NOT EXISTS, with CHECK
* constraints declared inline (no idempotency issue — IF NOT EXISTS
* on the parent table makes the whole CREATE a no-op when the table
* already exists, CHECK constraints and all).
*
* Idempotent re-apply: every statement uses IF NOT EXISTS.
*
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
*/
export const shorthands = undefined;
/**
* @param {import('node-pg-migrate').MigrationBuilder} pgm
*/
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)
);
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
);
ALTER TABLE users
ADD COLUMN IF NOT EXISTS is_pending BOOLEAN DEFAULT false;
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 intentionally throws. Dropping collection_permissions
* + collection_activity on a long-lived env would erase every active
* sharing relationship + every audit trail row. The runtime in
* pages/api/collections/[identifier]/permissions.js, pages/api/invite/*.js,
* and lib/permission-middleware.js all read these tables; rolling back
* would break the live app.
*
* @returns {void}
*/
export const down = () => {
throw new Error(
'[migration:1781000000003_reconcile-collaboration-tables] Down not supported. ' +
'Dropping collection_permissions / collection_activity would erase every ' +
'sharing relationship and audit trail, and break runtime reads in ' +
'pages/api/collections/[identifier]/permissions.js, pages/api/invite/*.js, ' +
'lib/permission-middleware.js. Write a new dated migration for any future ' +
'schema correction.'
);
};
Conventions to follow
.cursor/rules/db-and-schema.mdc§ "Schema source of truth" —migrations/is canonical; one migration per feature surface..cursor/rules/no-go-zones.mdc—scripts/add-collaboration-features.jsis append-only history. Do not edit it.- Style: raw
pgm.sql(...)template literals matching the other migrations. Per D2. - ESM exports;
"type": "module". IF NOT EXISTSon every CREATE / ALTER. Inline CHECK constraints onCREATE TABLEare fine —CREATE TABLE IF NOT EXISTSskips the entire statement (constraints and all) when the table exists.- FK declarations match the source script verbatim (
ON DELETE CASCADEfor primary FKs,ON DELETE SET NULLwhere the source uses it).
Acceptance criteria
migrations/1781000000003_reconcile-collaboration-tables.jsexists with the exact filename above.- The file's
up():- Creates
collection_permissionstable with all 10 columns + role/status CHECKs + invite_token UNIQUE + UNIQUE(collection_id, user_id), all viaCREATE TABLE IF NOT EXISTS. - Creates
collection_activitytable with 6 columns including JSONBdetails, viaCREATE TABLE IF NOT EXISTS. - Adds
users.is_pending BOOLEAN DEFAULT falseviaADD COLUMN IF NOT EXISTS. - Creates 3 indexes via
CREATE INDEX IF NOT EXISTS.
- Creates
- The 4th index from the source script (
idx_collections_visibility) is NOT in this migration — it belongs to Brief 2. - The owner-permission backfill DML is NOT in this migration.
- The file's
down()throws with a clear message. - The file's docstring cites the source script + the convoy file + the Brief 2 split.
node --check migrations/1781000000003_reconcile-collaboration-tables.jspasses.node -e "import('./migrations/1781000000003_reconcile-collaboration-tables.js').then(m => console.log(typeof m.up, typeof m.down, typeof m.shorthands))"printsfunction function undefined.npm run lintmatches baseline.npm run test:runreports 21/21 passing.
Verification
node --check migrations/1781000000003_reconcile-collaboration-tables.js
node -e "import('./migrations/1781000000003_reconcile-collaboration-tables.js').then(m => console.log(typeof m.up, typeof m.down, typeof m.shorthands))"
npm run lint
npm run test:run
Expected: node --check silent, node -e prints function function undefined, lint baseline, 21/21 tests pass.
Do NOT run npm run migrate up against any environment.
Commit message
feat(migrations): reconcile collaboration tables into migration history (brief 3/7)
Captures the collaboration / sharing half of
scripts/add-collaboration-features.js into one new node-pg-migrate
migration:
- CREATE TABLE collection_permissions (role/status CHECKs,
invite_token UNIQUE, UNIQUE(collection_id, user_id))
- CREATE TABLE collection_activity (JSONB details)
- ALTER users ADD COLUMN is_pending BOOLEAN DEFAULT false
- 3 indexes
The collections-columns half (visibility, tcg, tags +
idx_collections_visibility) is reconciled by Brief 2. The 4th index
(idx_collections_visibility) belongs to Brief 2 because it indexes a
column added there.
Per-row owner-permission backfill DML from the source script is
intentionally NOT folded in — fresh envs have no pre-existing
collections to backfill; the runtime invariant for new-collection
owner-perm creation lives in pages/api/collections.js.
Per .convoys/reconcile-historical-add-scripts.md § Brief outline → B3.
Idempotent re-apply (IF NOT EXISTS guards).
PR shape
Title: feat(migrations): reconcile collaboration tables into migration history (brief 3/7)
Body template:
Brief 3 of the `reconcile-historical-add-scripts` convoy. See
[`.convoys/reconcile-historical-add-scripts.md`](../.convoys/reconcile-historical-add-scripts.md)
for the full plan and rationale.
## What this PR does
Adds `migrations/1781000000003_reconcile-collaboration-tables.js` —
captures the collaboration / sharing DDL of
`scripts/add-collaboration-features.js` (the table-and-column half;
the columns-on-collections half is Brief 2).
- `CREATE TABLE IF NOT EXISTS collection_permissions` (10 columns
including inline role/status CHECK constraints + invite_token
UNIQUE + UNIQUE(collection_id, user_id))
- `CREATE TABLE IF NOT EXISTS collection_activity` (6 columns
including JSONB `details`)
- `ALTER TABLE users ADD COLUMN IF NOT EXISTS is_pending BOOLEAN DEFAULT false`
- 3 indexes via `CREATE INDEX IF NOT EXISTS`
## What this PR does NOT do
- Does **NOT** edit `scripts/add-collaboration-features.js` (no-go-zone).
- Does **NOT** capture the columns-on-collections half (`visibility`,
`tcg`, `tags`, `idx_collections_visibility`) — that's **Brief 2**.
- Does **NOT** fold in the per-row owner-permission backfill DML from
the source script — fresh envs have no pre-existing collections to
backfill.
- Does **NOT** edit `scripts/setup-neon-db.js`, `package.json`, README,
`AGENTS.md`, or `docs/SCHEMA_MAP.md` (B7 owns SCHEMA_MAP).
- Does **NOT** run `npm run migrate up` against any environment.
## Verification checklist
- [ ] `node --check migrations/1781000000003_reconcile-collaboration-tables.js` exits 0
- [ ] `node -e "import('./migrations/1781000000003_reconcile-collaboration-tables.js').then(m => console.log(typeof m.up, typeof m.down, typeof m.shorthands))"` prints `function function undefined`
- [ ] `npm run lint` matches baseline
- [ ] `npm run test:run` reports 21/21 passing
- [ ] Did not run `npm run migrate up` against any environment in this PR
## Cross-references
- Convoy file: `.convoys/reconcile-historical-add-scripts.md`
- Source script (no-go-zone, audit reference only):
`scripts/add-collaboration-features.js`
DO NOT
- DO NOT edit
scripts/add-collaboration-features.jsor any file underscripts/. - DO NOT edit any other file under
migrations/. - DO NOT edit
scripts/setup-neon-db.js,package.json, README,AGENTS.md, ordocs/SCHEMA_MAP.md. - DO NOT run
npm run migrate upagainst any environment. - DO NOT include the owner-permission backfill DML.
- DO NOT add work for the collections-columns half (
visibility,tcg,tags,idx_collections_visibility) — that's Brief 2. - DO NOT call
npm run migrate create.
Rationale (≤3 sentences)
Splitting add-collaboration-features.js between Brief 2 (collections columns + the visibility index that indexes one of those columns) and Brief 3 (collaboration tables + the users.is_pending column + the 3 indexes that index collaboration-table columns) keeps each migration scoped to the table surface it touches, matching D1. Inline CHECK constraints on CREATE TABLE are idempotent for free under CREATE TABLE IF NOT EXISTS (the whole statement no-ops when the table exists). The backfill DML is intentionally out of scope because fresh envs need no backfill and prod's backfill already ran.