add_feature_override.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. // Script to add feature overrides
  2. //
  3. // A feature override is appended to the user's featuresOverride list if they do
  4. // not already have the feature. The features are refreshed after adding the
  5. // override.
  6. //
  7. // If the script detects that the user would have the feature just by refreshing
  8. // then it skips adding the override and just refreshes the users features --
  9. // this is to minimise the creation of unnecessary overrides.
  10. //
  11. // Usage:
  12. //
  13. // $ node scripts/add_feature_override.js --commit --note 'text description' --expires 2022-01-01 --override JSONFILE --ids IDFILE
  14. //
  15. // --commit do the update, remove this option for dry-run testing
  16. // --note text description [optional]
  17. // --expires expiry date for override [optional]
  18. // --skip-existing don't create the override for users who already have the feature (e.g. via a subscription)
  19. //
  20. // IDFILE: file containing list of user ids, one per line
  21. // JSONFILE: file containing JSON of the desired feature overrides e.g. {"symbolPalette": true}
  22. //
  23. // The feature override is specified with JSON to allow types to be set as string/number/boolean.
  24. // It is contained in a file to avoid any issues with shell quoting.
  25. const minimist = require('minimist')
  26. const fs = require('fs')
  27. const { ObjectId, waitForDb } = require('../app/src/infrastructure/mongodb')
  28. const pLimit = require('p-limit')
  29. const FeaturesUpdater = require('../app/src/Features/Subscription/FeaturesUpdater')
  30. const FeaturesHelper = require('../app/src/Features/Subscription/FeaturesHelper')
  31. const UserFeaturesUpdater = require('../app/src/Features/Subscription/UserFeaturesUpdater')
  32. const UserGetter = require('../app/src/Features/User/UserGetter')
  33. const processLogger = {
  34. failed: [],
  35. success: [],
  36. skipped: [],
  37. printSummary: () => {
  38. console.log(
  39. {
  40. success: processLogger.success,
  41. failed: processLogger.failed,
  42. skipped: processLogger.skipped,
  43. },
  44. `\nDONE. ${processLogger.success.length} successful. ${processLogger.skipped.length} skipped. ${processLogger.failed.length} failed to update.`
  45. )
  46. },
  47. }
  48. function _validateUserIdList(userIds) {
  49. userIds.forEach(userId => {
  50. if (!ObjectId.isValid(userId))
  51. throw new Error(`user ID not valid: ${userId}`)
  52. })
  53. }
  54. async function _handleUser(userId) {
  55. console.log('updating user', userId)
  56. const user = await UserGetter.promises.getUser(userId, {
  57. features: 1,
  58. featuresOverrides: 1,
  59. })
  60. if (!user) {
  61. console.log(userId, 'does not exist, failed')
  62. processLogger.failed.push(userId)
  63. return
  64. }
  65. const desiredFeatures = OVERRIDE.features
  66. // Does the user have the requested features already?
  67. if (
  68. SKIP_EXISTING &&
  69. FeaturesHelper.isFeatureSetBetter(user.features, desiredFeatures)
  70. ) {
  71. console.log(
  72. userId,
  73. `already has ${JSON.stringify(desiredFeatures)}, skipping`
  74. )
  75. processLogger.skipped.push(userId)
  76. return
  77. }
  78. // Would the user have the requested feature if the features were refreshed?
  79. const freshFeatures = await FeaturesUpdater.promises.computeFeatures(userId)
  80. if (
  81. SKIP_EXISTING &&
  82. FeaturesHelper.isFeatureSetBetter(freshFeatures, desiredFeatures)
  83. ) {
  84. console.log(
  85. userId,
  86. `would have ${JSON.stringify(
  87. desiredFeatures
  88. )} if refreshed, skipping override`
  89. )
  90. } else {
  91. // create the override (if not in dry-run mode)
  92. if (COMMIT) {
  93. await UserFeaturesUpdater.promises.createFeaturesOverride(
  94. userId,
  95. OVERRIDE
  96. )
  97. }
  98. }
  99. if (!COMMIT) {
  100. // not saving features; nothing else to do
  101. return
  102. }
  103. const refreshResult = await FeaturesUpdater.promises.refreshFeatures(
  104. userId,
  105. 'add-feature-override-script'
  106. )
  107. const featureSetIncludesNewFeatures = FeaturesHelper.isFeatureSetBetter(
  108. refreshResult.features,
  109. desiredFeatures
  110. )
  111. if (featureSetIncludesNewFeatures) {
  112. // features added successfully
  113. processLogger.success.push(userId)
  114. } else {
  115. console.log('FEATURE NOT ADDED', refreshResult)
  116. processLogger.failed.push(userId)
  117. }
  118. }
  119. const argv = minimist(process.argv.slice(2))
  120. const CONCURRENCY = argv.async ? argv.async : 10
  121. const overridesFilename = argv.override
  122. const expires = argv.expires
  123. const note = argv.note
  124. const SKIP_EXISTING = argv['skip-existing'] || false
  125. const COMMIT = argv.commit !== undefined
  126. if (!COMMIT) {
  127. console.warn('Doing dry run without --commit')
  128. }
  129. const idsFilename = argv.ids
  130. if (!idsFilename) throw new Error('missing ids list filename')
  131. const usersFile = fs.readFileSync(idsFilename, 'utf8')
  132. const userIds = usersFile
  133. .trim()
  134. .split('\n')
  135. .map(id => id.trim())
  136. const overridesFile = fs.readFileSync(overridesFilename, 'utf8')
  137. const features = JSON.parse(overridesFile)
  138. const OVERRIDE = { features }
  139. if (note) {
  140. OVERRIDE.note = note
  141. }
  142. if (expires) {
  143. OVERRIDE.expiresAt = new Date(expires)
  144. }
  145. async function processUsers(userIds) {
  146. console.log('---Starting add feature override script---')
  147. console.log('Will update users to have', OVERRIDE)
  148. console.log(
  149. SKIP_EXISTING
  150. ? 'Users with this feature already will be skipped'
  151. : 'Every user in file will get feature override'
  152. )
  153. await waitForDb()
  154. _validateUserIdList(userIds)
  155. console.log(`---Starting to process ${userIds.length} users---`)
  156. const limit = pLimit(CONCURRENCY)
  157. const results = await Promise.allSettled(
  158. userIds.map(userId => limit(() => _handleUser(new ObjectId(userId))))
  159. )
  160. results.forEach((result, idx) => {
  161. if (result.status !== 'fulfilled') {
  162. console.log(userIds[idx], 'failed', result.reason)
  163. processLogger.failed.push(userIds[idx])
  164. }
  165. })
  166. processLogger.printSummary()
  167. process.exit()
  168. }
  169. processUsers(userIds)