2023-01-17 04:52:54 -05:00
import pg from 'pg' ;
import assert from 'node:assert' ;
2024-09-10 02:10:59 -05:00
const omit = ( object , ... keys ) =>
Object . fromEntries (
Object . entries ( object ) . filter ( ( [ key ] ) => ! keys . includes ( key ) )
) ;
const omitArray = ( arrayOfObjects , ... keys ) =>
arrayOfObjects . map ( ( value ) => omit ( value , ... keys ) ) ;
2023-01-17 04:52:54 -05:00
2023-07-09 23:54:09 -05:00
const schemas = [ 'cloud' , 'public' ] ;
const schemasArray = ` ( ${ schemas . map ( ( schema ) => ` ' ${ schema } ' ` ) . join ( ', ' ) } ) ` ;
2023-01-17 04:52:54 -05:00
2023-12-15 03:50:14 -05:00
const tryCompare = ( a , b ) => {
try {
assert . deepStrictEqual ( a , b ) ;
} catch ( error ) {
console . error ( error . toString ( ) ) ;
process . exit ( 1 ) ;
}
} ;
2023-01-17 04:52:54 -05:00
const queryDatabaseManifest = async ( database ) => {
2024-09-10 02:10:59 -05:00
const pool = new pg . Pool ( {
database ,
user : 'postgres' ,
password : 'postgres' ,
} ) ;
2023-01-17 04:52:54 -05:00
2024-09-10 02:10:59 -05:00
const { rows : tables } = await pool . query ( /* sql */ `
2023-01-17 04:52:54 -05:00
select *
from information _schema . tables
2023-07-09 23:54:09 -05:00
where table _schema in $ { schemasArray }
order by table _schema , table _name asc ;
2023-01-17 04:52:54 -05:00
` );
2024-09-10 02:10:59 -05:00
const { rows : columns } = await pool . query ( /* sql */ `
2023-01-17 04:52:54 -05:00
select *
from information _schema . columns
2023-07-09 23:54:09 -05:00
where table _schema in $ { schemasArray }
order by table _schema , table _name , column _name asc ;
2023-01-17 04:52:54 -05:00
` );
2024-09-10 02:10:59 -05:00
const { rows : enums } = await pool . query ( /* sql */ `
2023-01-17 04:52:54 -05:00
select pg _type . typname , pg _enum . enumlabel
from pg _type
join pg _enum
on pg _enum . enumtypid = pg _type . oid
order by pg _type . typname , pg _enum . enumlabel asc ;
` );
2024-09-10 02:10:59 -05:00
const { rows : constraints } = await pool . query ( /* sql */ `
2024-07-21 06:59:30 -05:00
select conrelid : : regclass as r _table , con . * , pg _get _constraintdef ( con . oid ) as def
2023-01-17 04:52:54 -05:00
from pg _catalog . pg _constraint con
inner join pg _catalog . pg _class rel
on rel . oid = con . conrelid
inner join pg _catalog . pg _namespace nsp
on nsp . oid = connamespace
where nsp . nspname = 'public'
2024-11-27 06:08:25 -05:00
order by conname asc , def asc , conrelid : : regclass asc ;
2023-01-17 04:52:54 -05:00
` );
2024-09-10 02:10:59 -05:00
const { rows : indexes } = await pool . query ( /* sql */ `
2023-01-17 04:52:54 -05:00
select *
from pg _indexes
2023-07-09 23:54:09 -05:00
where schemaname in $ { schemasArray }
order by schemaname , indexname asc ;
2023-01-17 04:52:54 -05:00
` );
2024-09-10 02:10:59 -05:00
const { rows : funcs } = await pool . query ( /* sql */ `
2023-01-28 06:26:29 -05:00
select n . nspname as schema _name ,
2023-01-28 07:03:07 -05:00
p . proname as specific _name ,
case p . prokind
when 'f' then 'FUNCTION'
when 'p' then 'PROCEDURE'
when 'a' then 'AGGREGATE'
when 'w' then 'WINDOW'
end as kind ,
l . lanname as language ,
case when l . lanname = 'internal' then p . prosrc
else pg _get _functiondef ( p . oid )
end as definition ,
pg _get _function _arguments ( p . oid ) as arguments ,
t . typname as return _type
2023-01-28 06:26:29 -05:00
from pg _proc p
left join pg _namespace n on p . pronamespace = n . oid
left join pg _language l on p . prolang = l . oid
left join pg _type t on t . oid = p . prorettype
where n . nspname not in ( 'pg_catalog' , 'information_schema' )
and l . lanname != 'c' -- Filter out c functions since we don ' t use them
2023-01-28 07:03:07 -05:00
order by schema _name , specific _name ;
2023-01-28 06:26:29 -05:00
` );
2024-09-10 02:10:59 -05:00
const { rows : triggers } = await pool . query (
/* sql */ ` select * from information_schema.triggers; `
) ;
const { rows : policies } = await pool . query (
/* sql */ ` select * from pg_policies order by tablename, policyname; `
) ;
const { rows : columnGrants } = await pool . query ( /* sql */ `
2023-03-08 06:16:42 -05:00
select * from information _schema . role _column _grants
2023-07-09 23:54:09 -05:00
where table _schema in $ { schemasArray }
2023-03-08 06:16:42 -05:00
and grantee != 'postgres'
2023-07-09 23:54:09 -05:00
order by table _schema , grantee , table _name , column _name , privilege _type ;
2023-03-08 06:16:42 -05:00
` );
2024-09-10 02:10:59 -05:00
const { rows : tableGrants } = await pool . query ( /* sql */ `
2023-03-08 06:16:42 -05:00
select * from information _schema . role _table _grants
2023-07-09 23:54:09 -05:00
where table _schema in $ { schemasArray }
2023-03-08 06:16:42 -05:00
and grantee != 'postgres'
2023-07-09 23:54:09 -05:00
order by table _schema , grantee , table _name , privilege _type ;
2023-03-08 06:16:42 -05:00
` );
// This function removes the last segment of grantee since Logto will use 'logto_tenant_fresh/alteration' for the role name.
2024-07-01 22:25:01 -05:00
const normalizeRoleName = ( roleName ) => {
if ( roleName . startsWith ( 'logto_tenant_' ) ) {
return 'logto_tenant' ;
2023-03-08 06:16:42 -05:00
}
2024-07-01 02:51:41 -05:00
// Removes the last segment of region grantee since Logto will use 'logto_region_xxx' for the role name for different regions.
2024-07-01 22:25:01 -05:00
if ( roleName . startsWith ( 'logto_region_' ) ) {
return 'logto_region' ;
2024-07-01 02:51:41 -05:00
}
2024-07-01 22:25:01 -05:00
return roleName ;
2023-03-08 06:16:42 -05:00
} ;
2024-07-01 22:25:01 -05:00
const normalizeGrantee = ( { grantee , ... rest } ) => ( {
... rest ,
grantee : normalizeRoleName ( grantee ) ,
} ) ;
2023-03-08 06:16:42 -05:00
// Ditto.
const normalizeRoles = ( { roles : raw , ... rest } ) => {
2024-07-01 22:25:01 -05:00
const roles = raw
. slice ( 1 , - 1 )
. split ( ',' )
. map ( ( name ) => normalizeRoleName ( name ) ) ;
2023-03-08 06:16:42 -05:00
return { roles , ... rest } ;
} ;
2023-01-28 06:26:29 -05:00
2024-07-01 22:25:01 -05:00
const normalizePolicyname = ( { policyname , ... rest } ) => {
const prefix = 'allow_' ;
const suffix = '_access' ;
2024-09-10 02:10:59 -05:00
if (
policyname &&
policyname . startsWith ( prefix ) &&
policyname . endsWith ( suffix )
) {
2024-07-01 22:25:01 -05:00
// This is a naming convention in Logto cloud, it is formatted as `allow_{role_name}_access`, we need to normalize the role name part for the convenience of comparing DB updates.
// Ref: https://github.com/logto-io/cloud/pull/738
2024-09-10 02:10:59 -05:00
return {
policyname : ` ${ prefix } ${ normalizeRoleName (
policyname . slice ( prefix . length , - suffix . length )
) } $ { suffix } ` ,
... rest ,
} ;
2024-07-01 22:25:01 -05:00
}
return { policyname , ... rest } ;
} ;
2023-01-18 00:12:57 -05:00
// Omit generated ids and values
2023-01-17 04:52:54 -05:00
return {
tables : omitArray ( tables , 'table_catalog' ) ,
2024-09-10 02:10:59 -05:00
columns : omitArray (
columns ,
'table_catalog' ,
'udt_catalog' ,
'ordinal_position' ,
'dtd_identifier'
) ,
2023-01-17 04:52:54 -05:00
enums ,
constraints : omitArray (
constraints ,
'oid' ,
2024-09-10 02:10:59 -05:00
/ * *
2023-09-10 22:27:49 -05:00
* See https : //www.postgresql.org/docs/current/catalog-pg-constraint.html, better to use `pg_get_constraintdef()`
* to extract the definition of check constraint , so this can be omitted since conbin changes with the status of the computing resources .
* /
'conbin' ,
2023-01-17 04:52:54 -05:00
'connamespace' ,
'conrelid' ,
'contypid' ,
'conindid' ,
'conparentid' ,
'confrelid' ,
'conkey' ,
'confkey' ,
'conpfeqop' ,
'conppeqop' ,
'conffeqop' ,
'confdelsetcols' ,
2024-09-10 02:10:59 -05:00
'conexclop'
2023-01-17 04:52:54 -05:00
) ,
indexes ,
2023-01-28 06:26:29 -05:00
funcs ,
triggers : omitArray ( triggers , 'trigger_catalog' , 'event_object_catalog' ) ,
2024-07-01 22:25:01 -05:00
policies : policies . map ( normalizeRoles ) . map ( normalizePolicyname ) ,
2024-09-10 02:10:59 -05:00
columnGrants : omitArray ( columnGrants , 'table_catalog' ) . map (
normalizeGrantee
) ,
2023-03-08 06:16:42 -05:00
tableGrants : omitArray ( tableGrants , 'table_catalog' ) . map ( normalizeGrantee ) ,
2023-01-17 04:52:54 -05:00
} ;
} ;
2024-09-10 02:10:59 -05:00
const [ , , database1 , database2 ] = process . argv ;
2023-01-17 04:52:54 -05:00
2023-02-02 05:36:18 -05:00
console . log ( 'Compare database manifest between' , database1 , 'and' , database2 ) ;
2023-01-17 04:52:54 -05:00
2023-02-02 05:36:18 -05:00
const manifests = [
2023-01-17 04:52:54 -05:00
await queryDatabaseManifest ( database1 ) ,
await queryDatabaseManifest ( database2 ) ,
2023-02-02 05:36:18 -05:00
] ;
2023-12-15 03:50:14 -05:00
tryCompare ( ... manifests ) ;
2023-02-02 05:36:18 -05:00
2023-03-03 01:11:19 -05:00
const autoCompare = ( a , b ) => {
if ( typeof a !== typeof b ) {
return ( typeof a ) . localeCompare ( typeof b ) ;
}
2024-09-10 02:10:59 -05:00
if ( typeof a === 'object' && a !== null && b !== null ) {
const aKeys = Object . keys ( a ) . sort ( ) ;
const bKeys = Object . keys ( b ) . sort ( ) ;
for ( let i = 0 ; i < Math . min ( aKeys . length , bKeys . length ) ; i ++ ) {
if ( aKeys [ i ] !== bKeys [ i ] ) {
return aKeys [ i ] . localeCompare ( bKeys [ i ] ) ;
}
const comparison = autoCompare ( a [ aKeys [ i ] ] , b [ bKeys [ i ] ] ) ;
if ( comparison !== 0 ) {
return comparison ;
}
}
return aKeys . length - bKeys . length ;
}
2023-03-03 01:11:19 -05:00
return String ( a ) . localeCompare ( String ( b ) ) ;
} ;
const buildSortByKeys = ( keys ) => ( a , b ) => {
const found = keys . find ( ( key ) => a [ key ] !== b [ key ] ) ;
return found ? autoCompare ( a [ found ] , b [ found ] ) : 0 ;
} ;
2023-02-02 05:36:18 -05:00
const queryDatabaseData = async ( database ) => {
2024-09-10 02:10:59 -05:00
const pool = new pg . Pool ( {
database ,
user : 'postgres' ,
password : 'postgres' ,
} ) ;
const result = await Promise . all (
manifests [ 0 ] . tables . map ( async ( { table _schema , table _name } ) => {
const { rows } = await pool . query (
/* sql */ ` select * from ${ table _schema } . ${ table _name } ; `
) ;
2023-02-02 05:36:18 -05:00
2023-02-11 09:55:37 -05:00
// check config rows except the value column
if ( [ 'logto_configs' , '_logto_configs' , 'systems' ] . includes ( table _name ) ) {
2023-03-03 01:11:19 -05:00
const data = omitArray ( rows , 'value' ) ;
2024-09-10 02:10:59 -05:00
return [
table _name ,
data . sort ( buildSortByKeys ( Object . keys ( data [ 0 ] ? ? { } ) ) ) ,
] ;
2023-02-11 09:55:37 -05:00
}
2023-03-03 01:11:19 -05:00
const data = omitArray (
2023-02-11 09:55:37 -05:00
rows ,
'id' ,
'resource_id' ,
'role_id' ,
2023-03-11 20:34:15 -05:00
'application_id' ,
2023-02-11 09:55:37 -05:00
'scope_id' ,
'created_at' ,
'updated_at' ,
'secret' ,
'db_user' ,
2024-12-08 21:55:38 -05:00
'db_user_password' ,
'add_on_sku_id'
2023-03-03 01:11:19 -05:00
) ;
return [ table _name , data . sort ( buildSortByKeys ( Object . keys ( data [ 0 ] ? ? { } ) ) ) ] ;
2023-02-02 05:36:18 -05:00
} )
) ;
return Object . fromEntries ( result ) ;
} ;
console . log ( 'Compare database data between' , database1 , 'and' , database2 ) ;
2024-09-10 02:10:59 -05:00
tryCompare (
await queryDatabaseData ( database1 ) ,
await queryDatabaseData ( database2 )
) ;