Skip to content

Commit 34b01ed

Browse files
authored
Added endpoint for listing automations (TryGhost#27607)
closes https://linear.app/ghost/issue/NY-1258 This PR adds `GET /automations`, which lists all of your automations. For now, it only includes the ID, name, and status. We can add more fields in the future.
1 parent 637680f commit 34b01ed

5 files changed

Lines changed: 106 additions & 2 deletions

File tree

ghost/core/core/server/api/endpoints/automations.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,28 @@
11
const domainEvents = require('@tryghost/domain-events');
2+
const models = require('../../models');
23
const StartAutomationsPollEvent = require('../../services/welcome-email-automations/events/start-automations-poll-event');
34

45
/** @type {import('@tryghost/api-framework').Controller} */
56
const controller = {
67
docName: 'automations',
78

9+
browse: {
10+
headers: {
11+
cacheInvalidate: false
12+
},
13+
permissions: true,
14+
async query() {
15+
const automations = await models.WelcomeEmailAutomation.findAll();
16+
return {
17+
data: automations.map(automation => ({
18+
id: automation.get('id'),
19+
name: automation.get('name'),
20+
status: automation.get('status')
21+
}))
22+
};
23+
}
24+
},
25+
826
poll: {
927
statusCode: 204,
1028
headers: {

ghost/core/core/server/models/welcome-email-automation.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ const WelcomeEmailAutomation = ghostBookshelf.Model.extend({
5050
}
5151
}, isEnableTransition ? 'Welcome email automation enabled' : 'Welcome email automation disabled');
5252
}
53+
}, {
54+
orderDefaultRaw() {
55+
return '`created_at` ASC';
56+
}
5357
});
5458

5559
module.exports = {

ghost/core/core/server/web/api/endpoints/admin/routes.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ module.exports = function apiRoutes() {
186186
router.delete('/labels/:id', mw.authAdminApi, http(api.labels.destroy));
187187

188188
// ## Automations
189+
router.get('/automations', mw.authAdminApi, http(api.automations.browse));
189190
router.put('/automations/poll', mw.authAdminApiWithUrl, http(api.automations.poll));
190191

191192
// ## Automated Emails

ghost/core/test/e2e-api/admin/automations.test.js

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
const sinon = require('sinon');
22
const domainEvents = require('@tryghost/domain-events');
3+
const assert = require('node:assert/strict');
34
const models = require('../../../core/server/models');
45
const {getSignedAdminToken} = require('../../../core/server/adapters/scheduling/utils');
5-
const {agentProvider, fixtureManager, matchers, assertions} = require('../../utils/e2e-framework');
6+
const {agentProvider, dbUtils, fixtureManager, matchers, assertions} = require('../../utils/e2e-framework');
67
const StartAutomationsPollEvent = require('../../../core/server/services/welcome-email-automations/events/start-automations-poll-event');
78

89
const {anyContentVersion, anyEtag, anyErrorId} = matchers;
@@ -15,7 +16,8 @@ describe('Automations API', function () {
1516

1617
before(async function () {
1718
agent = await agentProvider.getAdminAPIAgent();
18-
await fixtureManager.init('integrations', 'api_keys');
19+
await fixtureManager.init('users', 'integrations', 'api_keys');
20+
await agent.loginAsOwner();
1921

2022
schedulerIntegration = await models.Integration.findOne(
2123
{slug: 'ghost-scheduler'},
@@ -33,6 +35,42 @@ describe('Automations API', function () {
3335
sinon.restore();
3436
});
3537

38+
describe('browse', function () {
39+
beforeEach(async function () {
40+
await dbUtils.truncate('welcome_email_automated_emails');
41+
await dbUtils.truncate('welcome_email_automations');
42+
});
43+
44+
it('returns welcome email automations ordered by creation time', async function () {
45+
const second = await models.WelcomeEmailAutomation.add({
46+
name: 'Welcome Email (Premium)',
47+
slug: 'member-welcome-email-premium',
48+
status: 'inactive',
49+
created_at: new Date('2025-01-02T00:00:00Z')
50+
});
51+
const first = await models.WelcomeEmailAutomation.add({
52+
name: 'Welcome Email (Free)',
53+
slug: 'member-welcome-email-free',
54+
status: 'active',
55+
created_at: new Date('2025-01-01T00:00:00Z')
56+
});
57+
58+
const {body} = await agent
59+
.get('automations')
60+
.expectStatus(200);
61+
62+
assert.deepEqual(body.automations, [{
63+
id: first.id,
64+
name: first.get('name'),
65+
status: first.get('status')
66+
}, {
67+
id: second.id,
68+
name: second.get('name'),
69+
status: second.get('status')
70+
}]);
71+
});
72+
});
73+
3674
describe('poll', function () {
3775
/** @type {sinon.SinonStub} */
3876
let dispatchStub;

ghost/core/test/unit/api/endpoints/automations.test.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,63 @@
11
const assert = require('node:assert/strict');
22
const sinon = require('sinon');
33
const domainEvents = require('@tryghost/domain-events');
4+
const models = require('../../../../core/server/models');
45
const automationsController = require('../../../../core/server/api/endpoints/automations');
56
const StartAutomationsPollEvent = require('../../../../core/server/services/welcome-email-automations/events/start-automations-poll-event');
67

78
describe('Automations controller', function () {
9+
before(function () {
10+
models.init();
11+
});
12+
13+
function createMockAutomation(id, name, status) {
14+
return {
15+
get(key) {
16+
switch (key) {
17+
case 'id':
18+
return id;
19+
case 'name':
20+
return name;
21+
case 'status':
22+
return status;
23+
default:
24+
throw new Error(`Unexpected field: ${key}`);
25+
}
26+
}
27+
};
28+
}
29+
830
let dispatchStub;
931

1032
beforeEach(function () {
33+
sinon.stub(models.WelcomeEmailAutomation, 'findAll').resolves([
34+
createMockAutomation('automation-id-1', 'Welcome Email (Free)', 'active'),
35+
createMockAutomation('automation-id-2', 'Welcome Email (Premium)', 'inactive')
36+
]);
37+
1138
dispatchStub = sinon.stub(domainEvents, 'dispatch');
1239
});
1340

1441
afterEach(function () {
1542
sinon.restore();
1643
});
1744

45+
describe('browse', function () {
46+
it('returns only id, name, and status fields', async function () {
47+
const result = await automationsController.browse.query({});
48+
49+
assert.deepEqual(result.data, [{
50+
id: 'automation-id-1',
51+
name: 'Welcome Email (Free)',
52+
status: 'active'
53+
}, {
54+
id: 'automation-id-2',
55+
name: 'Welcome Email (Premium)',
56+
status: 'inactive'
57+
}]);
58+
});
59+
});
60+
1861
describe('poll', function () {
1962
it('dispatches a StartAutomationsPollEvent', function () {
2063
const result = automationsController.poll.query({});

0 commit comments

Comments
 (0)