graph_user_lib.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. const { execFileSync } = require("node:child_process");
  2. const path = require("node:path");
  3. const repoRoot = path.resolve(__dirname, "..", "..");
  4. const defaultConfigPath = path.join(repoRoot, "worker", "wrangler.toml");
  5. function fail(message) {
  6. console.error(message);
  7. process.exit(1);
  8. }
  9. function escapeSqlValue(value) {
  10. return value.replaceAll("'", "''");
  11. }
  12. function sqlBooleanToBool(value) {
  13. if (value === null || value === undefined) return null;
  14. return Number(value) === 1;
  15. }
  16. function sqlTimestampToIso(value) {
  17. if (value === null || value === undefined || value === "") return null;
  18. const timestamp = Number(value);
  19. return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
  20. }
  21. function buildUserGraphsSql({ lookupField, lookupValue, ownedOnly = false }) {
  22. const escapedValue = escapeSqlValue(lookupValue);
  23. const memberUnion = ownedOnly
  24. ? ""
  25. : `
  26. union all
  27. select g.graph_id,
  28. g.graph_name,
  29. g.user_id as owner_user_id,
  30. m.role as access_role,
  31. m.invited_by,
  32. g.schema_version,
  33. g.graph_e2ee,
  34. g.graph_ready_for_use,
  35. g.created_at,
  36. g.updated_at
  37. from graph_members m
  38. join graphs g on g.graph_id = m.graph_id
  39. join target_user u on m.user_id = u.id
  40. where g.user_id <> u.id`;
  41. return `with target_user as (
  42. select id, email, username
  43. from users
  44. where ${lookupField} = '${escapedValue}'
  45. limit 1
  46. ),
  47. matching_graphs as (
  48. select g.graph_id,
  49. g.graph_name,
  50. g.user_id as owner_user_id,
  51. 'owner' as access_role,
  52. null as invited_by,
  53. g.schema_version,
  54. g.graph_e2ee,
  55. g.graph_ready_for_use,
  56. g.created_at,
  57. g.updated_at
  58. from graphs g
  59. join target_user u on g.user_id = u.id${memberUnion}
  60. )
  61. select u.id as user_id,
  62. u.email as user_email,
  63. u.username as user_username,
  64. g.graph_id,
  65. g.graph_name,
  66. g.access_role,
  67. g.invited_by,
  68. g.owner_user_id,
  69. owner.email as owner_email,
  70. owner.username as owner_username,
  71. g.schema_version,
  72. g.graph_e2ee,
  73. g.graph_ready_for_use,
  74. g.created_at,
  75. g.updated_at
  76. from target_user u
  77. left join matching_graphs g on 1 = 1
  78. left join users owner on owner.id = g.owner_user_id
  79. order by g.updated_at desc;`;
  80. }
  81. function buildWranglerArgs({ database, config, env, sql }) {
  82. return [
  83. "--yes",
  84. "wrangler",
  85. "d1",
  86. "execute",
  87. database,
  88. "--config",
  89. config,
  90. "--env",
  91. env,
  92. "--remote",
  93. "--json",
  94. "--command",
  95. sql,
  96. ];
  97. }
  98. function runWranglerQuery(args) {
  99. const output = execFileSync("npx", args, {
  100. cwd: repoRoot,
  101. encoding: "utf8",
  102. stdio: ["ignore", "pipe", "inherit"],
  103. });
  104. return JSON.parse(output);
  105. }
  106. function parseWranglerResults(output) {
  107. if (!Array.isArray(output) || output.length === 0) {
  108. throw new Error("Unexpected empty response from wrangler.");
  109. }
  110. const [statement] = output;
  111. if (!statement.success) {
  112. throw new Error("Wrangler reported an unsuccessful D1 query.");
  113. }
  114. return Array.isArray(statement.results) ? statement.results : [];
  115. }
  116. function formatUserGraphsResult(rows) {
  117. if (rows.length === 0) {
  118. return null;
  119. }
  120. const [firstRow] = rows;
  121. const graphs = rows
  122. .filter((row) => row.graph_id)
  123. .map((row) => ({
  124. graph_id: row.graph_id,
  125. graph_name: row.graph_name,
  126. access_role: row.access_role,
  127. invited_by: row.invited_by ?? null,
  128. owner_user_id: row.owner_user_id,
  129. owner_username: row.owner_username ?? null,
  130. owner_email: row.owner_email ?? null,
  131. schema_version: row.schema_version ?? null,
  132. graph_e2ee: sqlBooleanToBool(row.graph_e2ee),
  133. graph_ready_for_use: sqlBooleanToBool(row.graph_ready_for_use),
  134. created_at: sqlTimestampToIso(row.created_at),
  135. updated_at: sqlTimestampToIso(row.updated_at),
  136. }));
  137. return {
  138. user: {
  139. user_id: firstRow.user_id,
  140. username: firstRow.user_username ?? null,
  141. email: firstRow.user_email ?? null,
  142. },
  143. graphs,
  144. };
  145. }
  146. function printUserGraphsTable(result, countLabel = "Graphs") {
  147. console.log(
  148. `User: ${result.user.user_id}` +
  149. (result.user.username ? ` (${result.user.username})` : "") +
  150. (result.user.email ? ` <${result.user.email}>` : ""),
  151. );
  152. console.log(`${countLabel}: ${result.graphs.length}`);
  153. if (result.graphs.length > 0) {
  154. console.table(result.graphs);
  155. }
  156. }
  157. function buildAdminGraphDeleteUrl(baseUrl, graphId) {
  158. const normalizedBaseUrl = baseUrl.replace(/\/+$/, "");
  159. return `${normalizedBaseUrl}/admin/graphs/${encodeURIComponent(graphId)}`;
  160. }
  161. module.exports = {
  162. buildAdminGraphDeleteUrl,
  163. buildUserGraphsSql,
  164. buildWranglerArgs,
  165. defaultConfigPath,
  166. fail,
  167. formatUserGraphsResult,
  168. parseWranglerResults,
  169. printUserGraphsTable,
  170. repoRoot,
  171. runWranglerQuery,
  172. };