no issue
- `for .. of` loop contained a `return` rather than a `continue` so the whole function was being exited with an unexpected `undefined` return value after the first loop where an unknown setting is removed
refs https://github.com/TryGhost/Team/issues/1070
- receives settings array as provided by through Ghost's API
- errors if a provided setting key does not already exist in the database (settings are only created when syncing with a theme)
- errors if a provided select setting value does not match the theme-provided options
- providing there were no errors, updates each value in the database if it's changed along with the internal cache of full setting objects and the public key/value cache
- returns the same list of settings as `.listSettings()` for API consistency
refs https://github.com/TryGhost/Team/issues/1070
- if the cache is repopulated, keys that already existed in the cache but that don't exist in the new group of settings were left in the cache
- added a `.clear()` method that removes all items from the cache and call it when populating so the cache only contains what it was last populated with
- deletes properties on the internal content object so that references aren't lost
refs https://github.com/TryGhost/Team/issues/1070
- return re-fetched settings after sync so they can be used in later activation steps
- added internal `activeThemeSettings` cache that stores full setting data along with saved values for use in API output
- added `listSettings()` that converts internal cache object into a settings array that matches API format
refs https://github.com/TryGhost/Team/issues/1070
- having a dependency on a model in the cache service meant that Ghost had to know about that and pre-initialize the cache during boot, even though that didn't actually do anything except create a cache instance
- by making the cache a simple key/value store able to be populated from the cache settings service when a theme is activated it means that Ghost doesn't need to perform any extra initialization when the cache is initialized via `require`
refs https://github.com/TryGhost/Team/issues/1070
- added `bread` util that acts as a wrapper for the provided model, if we have any business functionality needed when settings are added/removed then it will go here
- added primary "server" service that handles syncing of custom theme data extracted from a theme with the settings that are in the database and exported as "Service". Syncing rules on theme activation:
- if a new setting is seen, create it with the default value
- if a setting has it's type changed, remove it and create a new setting with the default value
- if a select setting's value is not a valid option, reset it to the default value
- added shared "frontend/server" service that exposes an in-memory cache of key/value pairs for the currently active theme
no issue
- fixed destructuring error when creating instances of `EmailAnalyticsService` or `EventProcessor` with no options object
- fixed unprocessable complained event incrementing complained count rather than unprocessable count
- fixed `fetchAll()` and `fetchLatest()` returning `undefined` instead of a blank EventProcessingResult
refs https://github.com/TryGhost/Ghost/pull/12541
- make `EventProcessor` a super-class designed to be inherited from in consumer applications for application-level implementation
- helps to keep application-level concerns for event handling (eg, what to do with spam complaints) and things like application database knowledge in the consumer
- removed all database knowledge from `EmailAnalyticsService`
- requires a `queries` option to be passed in that lets the consuming application provide knowledge and define how fetched stats should be aggregated
refs https://github.com/TryGhost/Ghost/issues/12421
requires https://github.com/TryGhost/Ghost/pull/12457
- updates stats aggregator to calculate and store an open rate for each member
- uses two queries because I couldn't find a reasonable approach to perform the update in a single query as per the email aggregation
- benchmarked locally at <1sec/1000members
- will not store an open rate unless the number of tracked emails sent to a member is above a certain threshold (defaults to 5) to avoid new members being heavily weighted
- fixes typo in EmailAnalytics that was stopping member stats from being aggregated
no issue
- added `EmailAnalyticsService`
- `.fetchAll()` grabs and processes all available events
- `.fetchLatest()` grabs and processes all events since the last seen event timestamp
- `EventProcessor` passed event objects and updates `email_recipients` or `members` records depending on the event being analytics or list hygiene
- always returns a `EventProcessingResult` instance so that progress can be tracked and merged across individual events, batches (pages of events), and total runs
- adds email_id and member_id to the returned result where appropriate so that the stats aggregator can limit processing to data that has changed
- sets `email_recipients.{delivered_at, opened_at, failed_at}` for analytics events
- sets `members.subscribed = false` for permanent failure/unsubscribed/complained list hygiene events
- `StatsAggregator` takes an `EventProcessingResult`-like object containing arrays of email ids and member ids on which to aggregate statistics.
- jobs for `fetch-latest` and `fetch-all` ready for use with the JobsService
- added `initialiseRecurringJobs()` function to Ghost bootup procedure that schedules the email analytics "fetch latest" job to run every minute