-
-
Notifications
You must be signed in to change notification settings - Fork 11.9k
Changed limit-service to TypeScript and moved Ghost's queries out of it #30511
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| import type { Counter, Formatter } from '@tryghost/limit-service'; | ||
|
|
||
| const db = require('../../data/db'); | ||
|
|
||
| /** | ||
| * How Ghost counts the things it limits. | ||
| * | ||
| * These queries are here rather than in the limit service because they are Ghost's schema: | ||
| * what a staff user is, that Contributors do not count towards one, that a newsletter only | ||
| * counts while it is active. The service asks for a number and has no idea where it comes | ||
| * from, which is the same reason Admin can answer the same questions over HTTP. | ||
| * | ||
| * A counted limit with no counter here cannot be applied, and says so rather than going | ||
| * quiet: the service reports it and Ghost logs it at boot. | ||
| */ | ||
| export const counters: Record<string, Counter> = { | ||
| members: async ({ transacting } = {}) => { | ||
| const knex = (transacting ?? db.knex) as ReturnType<typeof require>; | ||
| const result = await knex('members').count('id', { as: 'count' }).first(); | ||
|
|
||
| return Number(result.count); | ||
| }, | ||
|
|
||
| newsletters: async ({ transacting } = {}) => { | ||
| const knex = (transacting ?? db.knex) as ReturnType<typeof require>; | ||
| const result = await knex('newsletters') | ||
| .count('id', { as: 'count' }) | ||
| .where('status', '=', 'active') | ||
| .first(); | ||
|
|
||
| return Number(result.count); | ||
| }, | ||
|
|
||
| emails: async ({ transacting, periodStart } = {}) => { | ||
| const knex = (transacting ?? db.knex) as ReturnType<typeof require>; | ||
| const result = await knex('emails') | ||
| .sum('email_count', { as: 'count' }) | ||
| .where('created_at', '>=', periodStart) | ||
| .first(); | ||
|
|
||
| // A sum over no rows is null, and some drivers return these aggregates as strings. | ||
| // Either would be compared against the limit as something other than a number. | ||
| return Number(result.count ?? 0); | ||
| }, | ||
|
|
||
| staff: async ({ transacting } = {}) => { | ||
| const knex = (transacting ?? db.knex) as ReturnType<typeof require>; | ||
| const result = await knex('users') | ||
| .select('users.id') | ||
| .leftJoin('roles_users', 'users.id', 'roles_users.user_id') | ||
| .leftJoin('roles', 'roles_users.role_id', 'roles.id') | ||
| .whereNot('roles.name', 'Contributor') | ||
| .andWhereNot('users.status', 'inactive') | ||
| .union([ | ||
| knex('invites') | ||
| .select('invites.id') | ||
| .leftJoin('roles', 'invites.role_id', 'roles.id') | ||
| .whereNot('roles.name', 'Contributor'), | ||
| ]); | ||
|
|
||
| return result.length; | ||
| }, | ||
|
|
||
| // Uploads compare against the size of the file being uploaded, which the caller passes in | ||
| // as `currentCount`, so nothing is ever counted here. The limit still needs a counter to | ||
| // exist, and saying so plainly beats a noop that reads as an oversight. | ||
| uploads: () => 0, | ||
| }; | ||
|
|
||
| /** A size reads better as megabytes than as a number of bytes. */ | ||
| export const formatters: Record<string, Formatter> = { | ||
| uploads: (count: number) => `${count / 1000000}MB`, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| const errors = require('@tryghost/errors'); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win Convert this new service module to TypeScript.
🤖 Prompt for AI Agents |
||
| const logging = require('@tryghost/logging'); | ||
| const { LimitService } = require('@tryghost/limit-service'); | ||
|
|
||
| const config = require('../../../shared/config'); | ||
| const db = require('../../data/db'); | ||
| const { counters, formatters } = require('./counters'); | ||
|
|
||
| const limitService = new LimitService(); | ||
|
|
||
| /** | ||
| * Build this site's limits from the configuration its host supplied. | ||
| * | ||
| * Safe to call again whenever that configuration changes: limits are resolved from scratch | ||
| * and swapped in, and nothing holding a reference to this service needs to know. | ||
| */ | ||
| const init = () => { | ||
| const hostSettings = config.get('hostSettings') || {}; | ||
|
|
||
| const helpLink = | ||
| hostSettings.billing?.enabled === true && hostSettings.billing?.url | ||
| ? hostSettings.billing.url | ||
| : 'https://ghost.org/help/'; | ||
|
|
||
| const subscription = hostSettings.subscription | ||
| ? { startDate: hostSettings.subscription.start, interval: 'month' } | ||
| : undefined; | ||
|
|
||
| try { | ||
| limitService.loadLimits({ | ||
| limits: hostSettings.limits || {}, | ||
| counters, | ||
| formatters, | ||
| subscription, | ||
| helpLink, | ||
| db, | ||
| errors, | ||
| }); | ||
| } catch (error) { | ||
| // Misusing the limit service is a programming error, not a reason to stop a site | ||
| // booting. Kept from before: configuration problems are reported rather than thrown | ||
| // now, but a mistake here still must not take a site down. | ||
| if (error instanceof errors.IncorrectUsageError) { | ||
| logging.warn(error); | ||
| return; | ||
| } | ||
|
|
||
| throw error; | ||
| } | ||
|
|
||
| // A limit its host is charging for that cannot be applied here is worth saying out loud. | ||
| // Previously the first one of these took down every limit after it, silently. | ||
| for (const problem of limitService.problems) { | ||
| logging.warn(`Host limit "${problem.limit}" was configured but not applied: ${problem.reason}`); | ||
| } | ||
| }; | ||
|
|
||
| module.exports = limitService; | ||
|
|
||
| module.exports.init = init; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: TryGhost/Ghost
Length of output: 32734
🤖 get_repo_knowledge executed:
get_repo_knowledge TryGhost/Ghost /tmp/coderabbit-repo-knowledge/tryghost-ghost-4c979634/conventions /tmp/coderabbit-repo-knowledge/tryghost-ghost-4c979634/learningsLength of output: 35126
🏁 Script executed:
Repository: TryGhost/Ghost
Length of output: 18436
🏁 Script executed:
Repository: TryGhost/Ghost
Length of output: 17066
Validate database aggregate rows before conversion.
The
require()cast discards the declared Knex and aggregate-row types. Ifresult.countis malformed,Number(...)returnsNaN; the limit checks use>comparisons, which return false forNaNand can allow an over-limit operation. Keep the query typed and validate each aggregate row with Zod before conversion.🤖 Prompt for AI Agents
Sources: Coding guidelines, Path instructions