add_feature_override.js 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  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. const user = await UserGetter.promises.getUser(userId, {
  56. features: 1,
  57. featuresOverrides: 1,
  58. })
  59. if (!user) {
  60. console.log(userId, 'does not exist, failed')
  61. processLogger.failed.push(userId)
  62. return
  63. }
  64. const desiredFeatures = OVERRIDE.features
  65. // Does the user have the requested features already?
  66. if (
  67. SKIP_EXISTING &&
  68. FeaturesHelper.isFeatureSetBetter(user.features, desiredFeatures)
  69. ) {
  70. console.log(
  71. userId,
  72. `already has ${JSON.stringify(desiredFeatures)}, skipping`
  73. )
  74. processLogger.skipped.push(userId)
  75. return
  76. }
  77. // Would the user have the requested feature if the features were refreshed?
  78. const freshFeatures = await FeaturesUpdater.promises.computeFeatures(userId)
  79. if (
  80. SKIP_EXISTING &&
  81. FeaturesHelper.isFeatureSetBetter(freshFeatures, desiredFeatures)
  82. ) {
  83. console.log(
  84. userId,
  85. `would have ${JSON.stringify(
  86. desiredFeatures
  87. )} if refreshed, skipping override`
  88. )
  89. } else {
  90. // create the override (if not in dry-run mode)
  91. if (COMMIT) {
  92. await UserFeaturesUpdater.promises.createFeaturesOverride(
  93. userId,
  94. OVERRIDE
  95. )
  96. }
  97. }
  98. if (!COMMIT) {
  99. // not saving features; nothing else to do
  100. return
  101. }
  102. const refreshResult = await FeaturesUpdater.promises.refreshFeatures(
  103. userId,
  104. 'add-feature-override-script'
  105. )
  106. const featureSetIncludesNewFeatures = FeaturesHelper.isFeatureSetBetter(
  107. refreshResult.features,
  108. desiredFeatures
  109. )
  110. if (featureSetIncludesNewFeatures) {
  111. // features added successfully
  112. processLogger.success.push(userId)
  113. } else {
  114. console.log('FEATURE NOT ADDED', refreshResult)
  115. processLogger.failed.push(userId)
  116. }
  117. }
  118. const argv = minimist(process.argv.slice(2))
  119. const CONCURRENCY = argv.async ? argv.async : 10
  120. const overridesFilename = argv.override
  121. const expires = argv.expires
  122. const note = argv.note
  123. const SKIP_EXISTING = argv['skip-existing'] || false
  124. const COMMIT = argv.commit !== undefined
  125. if (!COMMIT) {
  126. console.warn('Doing dry run without --commit')
  127. }
  128. const idsFilename = argv.ids
  129. if (!idsFilename) throw new Error('missing ids list filename')
  130. const usersFile = fs.readFileSync(idsFilename, 'utf8')
  131. const userIds = usersFile
  132. .trim()
  133. .split('\n')
  134. .map(id => id.trim())
  135. const overridesFile = fs.readFileSync(overridesFilename, 'utf8')
  136. const features = JSON.parse(overridesFile)
  137. const OVERRIDE = { features }
  138. if (note) {
  139. OVERRIDE.note = note
  140. }
  141. if (expires) {
  142. OVERRIDE.expiresAt = new Date(expires)
  143. }
  144. async function processUsers(userIds) {
  145. console.log('---Starting add feature override script---')
  146. console.log('Will update users to have', OVERRIDE)
  147. console.log(
  148. SKIP_EXISTING
  149. ? 'Users with this feature already will be skipped'
  150. : 'Every user in file will get feature override'
  151. )
  152. await waitForDb()
  153. _validateUserIdList(userIds)
  154. console.log(`---Starting to process ${userIds.length} users---`)
  155. const limit = pLimit(CONCURRENCY)
  156. await Promise.all(
  157. userIds.map(userId => limit(() => _handleUser(ObjectId(userId))))
  158. )
  159. processLogger.printSummary()
  160. process.exit()
  161. }
  162. processUsers(userIds)