Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
165 lines
5.0 KiB
JavaScript
165 lines
5.0 KiB
JavaScript
/**
|
|
* Adds entity_id/entity_type columns to document_folders, fixes PK for CitusDB,
|
|
* and replaces uniqueness constraint to allow entity-scoped folder paths.
|
|
*
|
|
* Combines:
|
|
* - add_entity_scope_to_document_folders
|
|
* - expand_document_folder_uniqueness_to_entity_scope
|
|
*
|
|
* @param { import("knex").Knex } knex
|
|
* @returns { Promise<void> }
|
|
*/
|
|
async function distributeIfCitus(knex, tableName) {
|
|
const citusFn = await knex.raw(`
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM pg_proc WHERE proname = 'create_distributed_table'
|
|
) AS exists;
|
|
`);
|
|
|
|
if (citusFn.rows?.[0]?.exists) {
|
|
const alreadyDistributed = await knex.raw(`
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM pg_dist_partition
|
|
WHERE logicalrelid = '${tableName}'::regclass
|
|
) AS is_distributed;
|
|
`);
|
|
|
|
if (!alreadyDistributed.rows?.[0]?.is_distributed) {
|
|
await knex.raw(`SELECT create_distributed_table('${tableName}', 'tenant')`);
|
|
}
|
|
}
|
|
}
|
|
|
|
exports.up = async function up(knex) {
|
|
const hasTable = await knex.schema.hasTable('document_folders');
|
|
if (!hasTable) {
|
|
return;
|
|
}
|
|
|
|
// --- Step 1: Add entity_id and entity_type columns ---
|
|
const hasEntityId = await knex.schema.hasColumn('document_folders', 'entity_id');
|
|
const hasEntityType = await knex.schema.hasColumn('document_folders', 'entity_type');
|
|
|
|
if (!hasEntityId || !hasEntityType) {
|
|
await knex.schema.alterTable('document_folders', (table) => {
|
|
if (!hasEntityId) {
|
|
table.uuid('entity_id').nullable();
|
|
}
|
|
|
|
if (!hasEntityType) {
|
|
table.text('entity_type').nullable();
|
|
}
|
|
});
|
|
}
|
|
|
|
// --- Step 2: Fix primary key for CitusDB (distribution column must be in PK) ---
|
|
const pkResult = await knex.raw(`
|
|
SELECT conname FROM pg_constraint
|
|
WHERE conrelid = 'document_folders'::regclass AND contype = 'p'
|
|
`);
|
|
const pkName = pkResult.rows?.[0]?.conname;
|
|
|
|
if (pkName) {
|
|
const pkCols = await knex.raw(`
|
|
SELECT a.attname FROM pg_constraint c
|
|
JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = ANY(c.conkey)
|
|
WHERE c.conname = ? AND c.conrelid = 'document_folders'::regclass
|
|
`, [pkName]);
|
|
const colNames = pkCols.rows.map((r) => r.attname);
|
|
|
|
if (!colNames.includes('tenant')) {
|
|
// Drop self-referential FK on parent_folder_id (not tenant-scoped, incompatible with CitusDB)
|
|
const fks = await knex.raw(`
|
|
SELECT conname FROM pg_constraint
|
|
WHERE conrelid = 'document_folders'::regclass
|
|
AND contype = 'f'
|
|
AND confrelid = 'document_folders'::regclass
|
|
`);
|
|
for (const fk of fks.rows) {
|
|
await knex.raw(`ALTER TABLE document_folders DROP CONSTRAINT IF EXISTS "${fk.conname}"`);
|
|
}
|
|
// Self-referential relationship (parent_folder_id) enforced at application level
|
|
|
|
// Drop old single-column PK and add composite PK
|
|
await knex.raw(`ALTER TABLE document_folders DROP CONSTRAINT IF EXISTS "${pkName}" CASCADE`);
|
|
await knex.raw(`ALTER TABLE document_folders ADD CONSTRAINT "${pkName}" PRIMARY KEY (tenant, folder_id)`);
|
|
}
|
|
}
|
|
|
|
await distributeIfCitus(knex, 'document_folders');
|
|
|
|
// --- Step 3: Replace uniqueness constraint with entity-scoped version ---
|
|
await knex.raw(`
|
|
ALTER TABLE document_folders
|
|
DROP CONSTRAINT IF EXISTS uq_document_folders_tenant_path;
|
|
`);
|
|
|
|
await knex.raw(`
|
|
DROP INDEX IF EXISTS uq_document_folders_tenant_path;
|
|
`);
|
|
|
|
await knex.raw(`
|
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_document_folders_tenant_path_entity_scope
|
|
ON document_folders (
|
|
tenant,
|
|
folder_path,
|
|
COALESCE(entity_id, '00000000-0000-0000-0000-000000000000'::uuid),
|
|
COALESCE(entity_type, '')
|
|
);
|
|
`);
|
|
};
|
|
|
|
exports.config = { transaction: false };
|
|
|
|
exports.down = async function down(knex) {
|
|
const hasTable = await knex.schema.hasTable('document_folders');
|
|
if (!hasTable) {
|
|
return;
|
|
}
|
|
|
|
// Reverse step 3: restore original uniqueness constraint
|
|
await knex.raw(`
|
|
DROP INDEX IF EXISTS uq_document_folders_tenant_path_entity_scope;
|
|
`);
|
|
|
|
const duplicatePaths = await knex.raw(`
|
|
SELECT EXISTS (
|
|
SELECT 1
|
|
FROM document_folders
|
|
GROUP BY tenant, folder_path
|
|
HAVING COUNT(*) > 1
|
|
) AS has_duplicates;
|
|
`);
|
|
|
|
if (duplicatePaths.rows?.[0]?.has_duplicates) {
|
|
throw new Error(
|
|
'Cannot rollback: duplicate (tenant, folder_path) rows exist due to entity-scoped folders. ' +
|
|
'Remove entity-scoped duplicate rows before retrying rollback.'
|
|
);
|
|
}
|
|
|
|
await knex.raw(`
|
|
ALTER TABLE document_folders
|
|
ADD CONSTRAINT uq_document_folders_tenant_path
|
|
UNIQUE (tenant, folder_path);
|
|
`);
|
|
|
|
// Reverse step 1: drop entity columns
|
|
const hasEntityId = await knex.schema.hasColumn('document_folders', 'entity_id');
|
|
const hasEntityType = await knex.schema.hasColumn('document_folders', 'entity_type');
|
|
|
|
if (!hasEntityId && !hasEntityType) {
|
|
return;
|
|
}
|
|
|
|
await knex.schema.alterTable('document_folders', (table) => {
|
|
if (hasEntityType) {
|
|
table.dropColumn('entity_type');
|
|
}
|
|
|
|
if (hasEntityId) {
|
|
table.dropColumn('entity_id');
|
|
}
|
|
});
|
|
};
|