add_subscription_members_csv.mjs 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. import fs from 'node:fs'
  2. import minimist from 'minimist'
  3. import { parse } from 'csv'
  4. import Stream from 'node:stream/promises'
  5. import SubscriptionGroupHandler from '../app/src/Features/Subscription/SubscriptionGroupHandler.js'
  6. import { Subscription } from '../app/src/models/Subscription.js'
  7. import { InvalidEmailError } from '../app/src/Features/Errors/Errors.js'
  8. function usage() {
  9. console.log(
  10. 'Usage: node scripts/add_subscription_members_csv.mjs -f <filename> -i <inviter_id> -s <subscription_id> [options]'
  11. )
  12. console.log('Required arguments:')
  13. console.log(
  14. ' -s, --subscriptionId <id> The ID of the subscription to update'
  15. )
  16. console.log(
  17. ' -i, --inviterId <id> The ID of the user sending the invites'
  18. )
  19. console.log(
  20. ' -f, --filename <filename> The path to the file to read data from'
  21. )
  22. console.log('Options:')
  23. console.log(
  24. ' --commit, -c Whether changes should be committed to the DB invites should be sent/revoked'
  25. )
  26. console.log(
  27. ' --removeMembersNotIncluded -r Remove members that are not in the CSV. Disabled when managed users are enabled for the subscription'
  28. )
  29. console.log(
  30. ' --verbose, -v Prints detailed information about the affected group members'
  31. )
  32. console.log(' -h, --help Show this help message')
  33. process.exit(0)
  34. }
  35. let {
  36. commit,
  37. removeMembersNotIncluded,
  38. inviterId,
  39. subscriptionId,
  40. filename,
  41. help,
  42. verbose,
  43. } = minimist(process.argv.slice(2), {
  44. string: ['filename', 'subscriptionId', 'inviterId'],
  45. boolean: ['commit', 'removeMembersNotIncluded', 'help', 'verbose'],
  46. alias: {
  47. commit: 'c',
  48. removeMembersNotIncluded: 'r',
  49. filename: 'f',
  50. help: 'h',
  51. inviterId: 'i',
  52. subscriptionId: 's',
  53. verbose: 'v',
  54. },
  55. default: {
  56. commit: false,
  57. removeMembersNotIncluded: false,
  58. help: false,
  59. verbose: false,
  60. },
  61. })
  62. const EMAIL_FIELD = 'email'
  63. if (help) {
  64. usage()
  65. process.exit(0)
  66. }
  67. if (!subscriptionId || !inviterId || !filename) {
  68. usage()
  69. process.exit(1)
  70. }
  71. async function processRows(rows) {
  72. const emailList = []
  73. for await (const row of rows) {
  74. const email = row[EMAIL_FIELD]
  75. if (email) {
  76. emailList.push(email)
  77. }
  78. }
  79. if (emailList.length === 0) {
  80. console.error(`CSV error: 'email' column doesn't exist or it's empty'`)
  81. process.exit(1)
  82. }
  83. let previewResult
  84. try {
  85. previewResult =
  86. await SubscriptionGroupHandler.promises.updateGroupMembersBulk(
  87. inviterId,
  88. subscriptionId,
  89. emailList,
  90. { removeMembersNotIncluded }
  91. )
  92. } catch (error) {
  93. if (error instanceof InvalidEmailError) {
  94. console.error(`${filename} contains invalid email addresses:`)
  95. console.error(error.info?.invalidEmails.join(','))
  96. process.exit(1)
  97. } else {
  98. throw error
  99. }
  100. }
  101. console.log('Result Preview:')
  102. logResult(previewResult)
  103. if (previewResult.newTotalCount > previewResult.membersLimit) {
  104. console.warn(
  105. 'WARNING: the invite list has reached the membership limit (newTotalCount > membersLimit)'
  106. )
  107. if (commit) {
  108. console.error(`Invites won't be sent and users won't be deleted`)
  109. }
  110. process.exit(1)
  111. }
  112. if (!commit) {
  113. console.log(
  114. 'this is a dry-run, use the --commit option to send the invite and make any DB changes'
  115. )
  116. return
  117. }
  118. console.log(
  119. `Sending invites to ${previewResult.emailsToSendInvite.length} email addresses`
  120. )
  121. if (previewResult.membersToRemove > 0) {
  122. console.log(
  123. `${previewResult.membersToRemove.length} members will be removed from the group`
  124. )
  125. }
  126. const commitResult =
  127. await SubscriptionGroupHandler.promises.updateGroupMembersBulk(
  128. inviterId,
  129. subscriptionId,
  130. emailList,
  131. { removeMembersNotIncluded, commit }
  132. )
  133. console.log('Result:')
  134. logResult(commitResult)
  135. }
  136. function logResult(result) {
  137. console.log(
  138. JSON.stringify(
  139. {
  140. ...result,
  141. emailsToSendInvite: verbose
  142. ? result.emailsToSendInvite
  143. : result.emailsToSendInvite.length,
  144. membersToRemove: verbose
  145. ? result.membersToRemove
  146. : result.membersToRemove.length,
  147. emailsToRevokeInvite: verbose
  148. ? result.emailsToRevokeInvite
  149. : result.emailsToRevokeInvite.length,
  150. },
  151. null,
  152. 2
  153. )
  154. )
  155. }
  156. async function main() {
  157. const subscription = await Subscription.findOne({
  158. _id: subscriptionId,
  159. }).exec()
  160. if (!subscription) {
  161. console.error(`subscription with id=${subscriptionId} not found`)
  162. process.exit(1)
  163. }
  164. if (subscription.managedUsersEnabled && removeMembersNotIncluded) {
  165. console.warn(
  166. `subscription with id=${subscriptionId} has 'managedUsersEnabled=true'` +
  167. `'--removeMembersNotIncluded' has been disabled`
  168. )
  169. removeMembersNotIncluded = false
  170. }
  171. await Stream.pipeline(
  172. fs.createReadStream(filename),
  173. parse({
  174. columns: true,
  175. }),
  176. processRows
  177. )
  178. }
  179. main()
  180. .then(() => process.exit(0))
  181. .catch(error => {
  182. console.error(error)
  183. process.exit(1)
  184. })