207 lines
8.5 KiB
JavaScript
207 lines
8.5 KiB
JavaScript
const { Pool } = require('pg');
|
|
require('dotenv').config({ path: '.env.local' });
|
|
|
|
const SYSTEM_USER_ID = '00000000-0000-0000-0000-000000000000';
|
|
|
|
function short(value, length = 160) {
|
|
if (value == null) return value;
|
|
const text = typeof value === 'string' ? value : JSON.stringify(value);
|
|
return text.length > length ? `${text.slice(0, length)}...` : text;
|
|
}
|
|
|
|
async function main() {
|
|
const pool = new Pool({ connectionString: process.env.LOCAL_DB_URL });
|
|
const client = await pool.connect();
|
|
try {
|
|
const tableColumns = await client.query(`
|
|
SELECT table_schema, table_name, column_name, data_type
|
|
FROM information_schema.columns
|
|
WHERE (table_schema = 'public' AND table_name IN ('profiles', 'works', 'user_api_keys', 'orders', 'credit_transactions'))
|
|
OR (table_schema = 'auth' AND table_name = 'users')
|
|
ORDER BY table_schema, table_name, ordinal_position
|
|
`);
|
|
|
|
const userSummary = await client.query(`
|
|
SELECT
|
|
(SELECT COUNT(*)::int FROM auth.users) AS auth_users,
|
|
(SELECT COUNT(*)::int FROM profiles) AS profiles,
|
|
(SELECT COUNT(*)::int FROM profiles WHERE role = 'admin') AS admins,
|
|
(SELECT COUNT(*)::int FROM profiles WHERE role <> 'admin') AS non_admin_profiles,
|
|
(SELECT COUNT(*)::int FROM auth.users u LEFT JOIN profiles p ON p.id = u.id WHERE p.id IS NULL) AS auth_without_profile,
|
|
(SELECT COUNT(*)::int FROM profiles p LEFT JOIN auth.users u ON u.id = p.id WHERE u.id IS NULL) AS profile_without_auth,
|
|
(SELECT COUNT(*)::int FROM auth.users WHERE password_hash IS NULL OR password_hash = '') AS auth_without_password_hash,
|
|
(SELECT COUNT(*)::int FROM auth.users WHERE password_hash IS NOT NULL AND password_hash <> '') AS auth_with_password_hash
|
|
`);
|
|
|
|
const userSamples = await client.query(`
|
|
SELECT p.id, p.email, p.nickname, p.role, p.membership_tier, p.is_active,
|
|
u.id IS NOT NULL AS has_auth,
|
|
(u.password_hash IS NOT NULL AND u.password_hash <> '') AS has_password_hash,
|
|
p.created_at
|
|
FROM profiles p
|
|
LEFT JOIN auth.users u ON u.id = p.id
|
|
ORDER BY p.created_at DESC NULLS LAST
|
|
LIMIT 80
|
|
`);
|
|
|
|
const workSummary = await client.query(`
|
|
SELECT
|
|
COUNT(*)::int AS total,
|
|
COUNT(*) FILTER (WHERE status = 'completed')::int AS completed,
|
|
COUNT(*) FILTER (WHERE is_public = true AND status = 'completed')::int AS public_completed,
|
|
COUNT(*) FILTER (WHERE is_public = false AND status = 'completed')::int AS private_completed,
|
|
COUNT(*) FILTER (WHERE user_id IS NULL)::int AS null_user_id,
|
|
COUNT(*) FILTER (WHERE user_id = $1)::int AS system_user_id,
|
|
COUNT(*) FILTER (WHERE p.id IS NULL)::int AS missing_profile,
|
|
COUNT(*) FILTER (WHERE p.id IS NOT NULL)::int AS linked_profile
|
|
FROM works w
|
|
LEFT JOIN profiles p ON p.id = w.user_id
|
|
`, [SYSTEM_USER_ID]);
|
|
|
|
const publicWorkSummary = await client.query(`
|
|
SELECT
|
|
COUNT(*)::int AS public_total,
|
|
COUNT(*) FILTER (WHERE w.user_id IS NULL)::int AS null_user_id,
|
|
COUNT(*) FILTER (WHERE w.user_id = $1)::int AS system_user_id,
|
|
COUNT(*) FILTER (WHERE p.id IS NULL)::int AS missing_profile,
|
|
COUNT(*) FILTER (WHERE p.id IS NOT NULL)::int AS linked_profile
|
|
FROM works w
|
|
LEFT JOIN profiles p ON p.id = w.user_id
|
|
WHERE w.is_public = true AND w.status = 'completed'
|
|
`, [SYSTEM_USER_ID]);
|
|
|
|
const workByUser = await client.query(`
|
|
SELECT
|
|
COALESCE(p.email, '[missing-profile]') AS email,
|
|
COALESCE(p.nickname, '') AS nickname,
|
|
COALESCE(p.role, '') AS role,
|
|
w.user_id,
|
|
COUNT(*)::int AS total_works,
|
|
COUNT(*) FILTER (WHERE w.status = 'completed')::int AS completed_works,
|
|
COUNT(*) FILTER (WHERE w.is_public = true AND w.status = 'completed')::int AS public_works,
|
|
COUNT(*) FILTER (WHERE w.is_public = false AND w.status = 'completed')::int AS history_works
|
|
FROM works w
|
|
LEFT JOIN profiles p ON p.id = w.user_id
|
|
GROUP BY w.user_id, p.email, p.nickname, p.role
|
|
ORDER BY total_works DESC
|
|
LIMIT 120
|
|
`);
|
|
|
|
const orphanSamples = await client.query(`
|
|
SELECT w.id, w.user_id, w.type, w.status, w.is_public, w.result_url,
|
|
LEFT(COALESCE(w.prompt, ''), 140) AS prompt,
|
|
w.params,
|
|
w.created_at
|
|
FROM works w
|
|
LEFT JOIN profiles p ON p.id = w.user_id
|
|
WHERE p.id IS NULL OR w.user_id = $1 OR w.user_id IS NULL
|
|
ORDER BY w.created_at DESC NULLS LAST
|
|
LIMIT 80
|
|
`, [SYSTEM_USER_ID]);
|
|
|
|
const paramKeys = await client.query(`
|
|
SELECT key, COUNT(*)::int AS count
|
|
FROM works w
|
|
CROSS JOIN LATERAL jsonb_object_keys(COALESCE(w.params, '{}'::jsonb)) AS key
|
|
LEFT JOIN profiles p ON p.id = w.user_id
|
|
WHERE w.is_public = true
|
|
AND w.status = 'completed'
|
|
AND (p.id IS NULL OR w.user_id = $1 OR w.user_id IS NULL)
|
|
GROUP BY key
|
|
ORDER BY count DESC, key
|
|
LIMIT 80
|
|
`, [SYSTEM_USER_ID]);
|
|
|
|
const possibleOwnerFields = await client.query(`
|
|
SELECT
|
|
id,
|
|
user_id,
|
|
params->>'user_id' AS params_user_id,
|
|
params->>'userId' AS params_user_id_camel,
|
|
params->>'publisher_id' AS publisher_id,
|
|
params->>'publisherId' AS publisher_id_camel,
|
|
params->>'owner_id' AS owner_id,
|
|
params->>'ownerId' AS owner_id_camel,
|
|
params->>'created_by' AS created_by,
|
|
params->>'createdBy' AS created_by_camel,
|
|
params->>'email' AS params_email,
|
|
params->>'userEmail' AS params_user_email,
|
|
params->>'publisherEmail' AS params_publisher_email,
|
|
params->>'nickname' AS params_nickname,
|
|
params->>'userName' AS params_user_name,
|
|
LEFT(COALESCE(prompt, ''), 120) AS prompt
|
|
FROM works
|
|
WHERE is_public = true
|
|
AND status = 'completed'
|
|
AND (user_id IS NULL OR user_id = $1 OR NOT EXISTS (SELECT 1 FROM profiles p WHERE p.id = works.user_id))
|
|
ORDER BY created_at DESC NULLS LAST
|
|
LIMIT 80
|
|
`, [SYSTEM_USER_ID]);
|
|
|
|
const duplicateCandidates = await client.query(`
|
|
SELECT
|
|
public.id AS orphan_id,
|
|
public.user_id AS orphan_user_id,
|
|
private.id AS owned_id,
|
|
private.user_id AS owner_user_id,
|
|
p.email,
|
|
p.nickname,
|
|
CASE
|
|
WHEN private.result_url = public.result_url THEN 'result_url'
|
|
WHEN COALESCE(private.thumbnail_url, '') <> '' AND private.thumbnail_url = public.thumbnail_url THEN 'thumbnail_url'
|
|
WHEN COALESCE(private.prompt, '') <> '' AND private.prompt = public.prompt THEN 'prompt_time'
|
|
ELSE 'unknown'
|
|
END AS match_type,
|
|
ABS(EXTRACT(EPOCH FROM (private.created_at - public.created_at)))::int AS seconds_apart,
|
|
LEFT(COALESCE(public.prompt, ''), 120) AS prompt
|
|
FROM works public
|
|
JOIN works private
|
|
ON private.id <> public.id
|
|
AND private.user_id IS NOT NULL
|
|
AND private.user_id <> $1
|
|
AND (
|
|
private.result_url = public.result_url
|
|
OR (
|
|
COALESCE(public.thumbnail_url, '') <> ''
|
|
AND private.thumbnail_url = public.thumbnail_url
|
|
)
|
|
OR (
|
|
COALESCE(private.prompt, '') <> ''
|
|
AND private.prompt = public.prompt
|
|
AND private.created_at BETWEEN public.created_at - INTERVAL '30 minutes' AND public.created_at + INTERVAL '30 minutes'
|
|
)
|
|
)
|
|
JOIN profiles p ON p.id = private.user_id
|
|
LEFT JOIN profiles public_profile ON public_profile.id = public.user_id
|
|
WHERE public.is_public = true
|
|
AND public.status = 'completed'
|
|
AND (public_profile.id IS NULL OR public.user_id = $1 OR public.user_id IS NULL)
|
|
ORDER BY public.created_at DESC NULLS LAST, match_type, seconds_apart
|
|
LIMIT 100
|
|
`, [SYSTEM_USER_ID]);
|
|
|
|
const output = {
|
|
columns: tableColumns.rows,
|
|
userSummary: userSummary.rows[0],
|
|
userSamples: userSamples.rows,
|
|
workSummary: workSummary.rows[0],
|
|
publicWorkSummary: publicWorkSummary.rows[0],
|
|
workByUser: workByUser.rows,
|
|
orphanSamples: orphanSamples.rows.map(row => ({ ...row, result_url: short(row.result_url, 120), params: short(row.params, 300) })),
|
|
anonymousParamKeys: paramKeys.rows,
|
|
possibleOwnerFields: possibleOwnerFields.rows,
|
|
duplicateCandidates: duplicateCandidates.rows,
|
|
};
|
|
|
|
console.log(JSON.stringify(output, null, 2));
|
|
} finally {
|
|
client.release();
|
|
await pool.end();
|
|
}
|
|
}
|
|
|
|
main().catch(error => {
|
|
console.error(error);
|
|
process.exit(1);
|
|
});
|