diff options
Diffstat (limited to 'server/models')
-rw-r--r-- | server/models/account/account-follow-interface.ts | 16 | ||||
-rw-r--r-- | server/models/account/account-follow.ts | 130 | ||||
-rw-r--r-- | server/models/account/account-interface.ts | 8 | ||||
-rw-r--r-- | server/models/account/account.ts | 132 |
4 files changed, 149 insertions, 137 deletions
diff --git a/server/models/account/account-follow-interface.ts b/server/models/account/account-follow-interface.ts index efdff915e..413dad190 100644 --- a/server/models/account/account-follow-interface.ts +++ b/server/models/account/account-follow-interface.ts | |||
@@ -1,13 +1,26 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import * as Bluebird from 'bluebird' | 2 | import * as Bluebird from 'bluebird' |
3 | import { FollowState } from '../../../shared/models/accounts/follow.model' | 3 | import { FollowState } from '../../../shared/models/accounts/follow.model' |
4 | import { ResultList } from '../../../shared/models/result-list.model' | ||
5 | import { AccountInstance } from './account-interface' | ||
4 | 6 | ||
5 | export namespace AccountFollowMethods { | 7 | export namespace AccountFollowMethods { |
6 | export type LoadByAccountAndTarget = (accountId: number, targetAccountId: number) => Bluebird<AccountFollowInstance> | 8 | export type LoadByAccountAndTarget = (accountId: number, targetAccountId: number) => Bluebird<AccountFollowInstance> |
9 | |||
10 | export type ListFollowingForApi = (id: number, start: number, count: number, sort: string) => Bluebird< ResultList<AccountInstance> > | ||
11 | export type ListFollowersForApi = (id: number, start: number, count: number, sort: string) => Bluebird< ResultList<AccountInstance> > | ||
12 | |||
13 | export type ListAcceptedFollowerUrlsForApi = (id: number, start: number, count?: number) => Promise< ResultList<string> > | ||
14 | export type ListAcceptedFollowingUrlsForApi = (id: number, start: number, count?: number) => Promise< ResultList<string> > | ||
7 | } | 15 | } |
8 | 16 | ||
9 | export interface AccountFollowClass { | 17 | export interface AccountFollowClass { |
10 | loadByAccountAndTarget: AccountFollowMethods.LoadByAccountAndTarget | 18 | loadByAccountAndTarget: AccountFollowMethods.LoadByAccountAndTarget |
19 | listFollowersForApi: AccountFollowMethods.ListFollowersForApi | ||
20 | listFollowingForApi: AccountFollowMethods.ListFollowingForApi | ||
21 | |||
22 | listAcceptedFollowerUrlsForApi: AccountFollowMethods.ListAcceptedFollowerUrlsForApi | ||
23 | listAcceptedFollowingUrlsForApi: AccountFollowMethods.ListAcceptedFollowingUrlsForApi | ||
11 | } | 24 | } |
12 | 25 | ||
13 | export interface AccountFollowAttributes { | 26 | export interface AccountFollowAttributes { |
@@ -20,6 +33,9 @@ export interface AccountFollowInstance extends AccountFollowClass, AccountFollow | |||
20 | id: number | 33 | id: number |
21 | createdAt: Date | 34 | createdAt: Date |
22 | updatedAt: Date | 35 | updatedAt: Date |
36 | |||
37 | AccountFollower?: AccountInstance | ||
38 | AccountFollowing?: AccountInstance | ||
23 | } | 39 | } |
24 | 40 | ||
25 | export interface AccountFollowModel extends AccountFollowClass, Sequelize.Model<AccountFollowInstance, AccountFollowAttributes> {} | 41 | export interface AccountFollowModel extends AccountFollowClass, Sequelize.Model<AccountFollowInstance, AccountFollowAttributes> {} |
diff --git a/server/models/account/account-follow.ts b/server/models/account/account-follow.ts index 7c129ab9d..6d7592326 100644 --- a/server/models/account/account-follow.ts +++ b/server/models/account/account-follow.ts | |||
@@ -1,12 +1,16 @@ | |||
1 | import { values } from 'lodash' | 1 | import { values } from 'lodash' |
2 | import * as Sequelize from 'sequelize' | 2 | import * as Sequelize from 'sequelize' |
3 | 3 | ||
4 | import { addMethodsToModel } from '../utils' | 4 | import { addMethodsToModel, getSort } from '../utils' |
5 | import { AccountFollowAttributes, AccountFollowInstance, AccountFollowMethods } from './account-follow-interface' | 5 | import { AccountFollowAttributes, AccountFollowInstance, AccountFollowMethods } from './account-follow-interface' |
6 | import { FOLLOW_STATES } from '../../initializers/constants' | 6 | import { FOLLOW_STATES } from '../../initializers/constants' |
7 | 7 | ||
8 | let AccountFollow: Sequelize.Model<AccountFollowInstance, AccountFollowAttributes> | 8 | let AccountFollow: Sequelize.Model<AccountFollowInstance, AccountFollowAttributes> |
9 | let loadByAccountAndTarget: AccountFollowMethods.LoadByAccountAndTarget | 9 | let loadByAccountAndTarget: AccountFollowMethods.LoadByAccountAndTarget |
10 | let listFollowingForApi: AccountFollowMethods.ListFollowingForApi | ||
11 | let listFollowersForApi: AccountFollowMethods.ListFollowersForApi | ||
12 | let listAcceptedFollowerUrlsForApi: AccountFollowMethods.ListAcceptedFollowerUrlsForApi | ||
13 | let listAcceptedFollowingUrlsForApi: AccountFollowMethods.ListAcceptedFollowingUrlsForApi | ||
10 | 14 | ||
11 | export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { | 15 | export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { |
12 | AccountFollow = sequelize.define<AccountFollowInstance, AccountFollowAttributes>('AccountFollow', | 16 | AccountFollow = sequelize.define<AccountFollowInstance, AccountFollowAttributes>('AccountFollow', |
@@ -34,7 +38,11 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da | |||
34 | 38 | ||
35 | const classMethods = [ | 39 | const classMethods = [ |
36 | associate, | 40 | associate, |
37 | loadByAccountAndTarget | 41 | loadByAccountAndTarget, |
42 | listFollowingForApi, | ||
43 | listFollowersForApi, | ||
44 | listAcceptedFollowerUrlsForApi, | ||
45 | listAcceptedFollowingUrlsForApi | ||
38 | ] | 46 | ] |
39 | addMethodsToModel(AccountFollow, classMethods) | 47 | addMethodsToModel(AccountFollow, classMethods) |
40 | 48 | ||
@@ -49,7 +57,7 @@ function associate (models) { | |||
49 | name: 'accountId', | 57 | name: 'accountId', |
50 | allowNull: false | 58 | allowNull: false |
51 | }, | 59 | }, |
52 | as: 'accountFollowers', | 60 | as: 'AccountFollower', |
53 | onDelete: 'CASCADE' | 61 | onDelete: 'CASCADE' |
54 | }) | 62 | }) |
55 | 63 | ||
@@ -58,7 +66,7 @@ function associate (models) { | |||
58 | name: 'targetAccountId', | 66 | name: 'targetAccountId', |
59 | allowNull: false | 67 | allowNull: false |
60 | }, | 68 | }, |
61 | as: 'accountFollowing', | 69 | as: 'AccountFollowing', |
62 | onDelete: 'CASCADE' | 70 | onDelete: 'CASCADE' |
63 | }) | 71 | }) |
64 | } | 72 | } |
@@ -73,3 +81,117 @@ loadByAccountAndTarget = function (accountId: number, targetAccountId: number) { | |||
73 | 81 | ||
74 | return AccountFollow.findOne(query) | 82 | return AccountFollow.findOne(query) |
75 | } | 83 | } |
84 | |||
85 | listFollowingForApi = function (id: number, start: number, count: number, sort: string) { | ||
86 | const query = { | ||
87 | distinct: true, | ||
88 | offset: start, | ||
89 | limit: count, | ||
90 | order: [ getSort(sort) ], | ||
91 | include: [ | ||
92 | { | ||
93 | model: AccountFollow[ 'sequelize' ].models.Account, | ||
94 | required: true, | ||
95 | as: 'AccountFollower', | ||
96 | where: { | ||
97 | id | ||
98 | } | ||
99 | }, | ||
100 | { | ||
101 | model: AccountFollow['sequelize'].models.Account, | ||
102 | as: 'AccountFollowing', | ||
103 | required: true, | ||
104 | include: [ AccountFollow['sequelize'].models.Pod ] | ||
105 | } | ||
106 | ] | ||
107 | } | ||
108 | |||
109 | return AccountFollow.findAndCountAll(query).then(({ rows, count }) => { | ||
110 | return { | ||
111 | data: rows.map(r => r.AccountFollowing), | ||
112 | total: count | ||
113 | } | ||
114 | }) | ||
115 | } | ||
116 | |||
117 | listFollowersForApi = function (id: number, start: number, count: number, sort: string) { | ||
118 | const query = { | ||
119 | distinct: true, | ||
120 | offset: start, | ||
121 | limit: count, | ||
122 | order: [ getSort(sort) ], | ||
123 | include: [ | ||
124 | { | ||
125 | model: AccountFollow[ 'sequelize' ].models.Account, | ||
126 | required: true, | ||
127 | as: 'AccountFollower', | ||
128 | include: [ AccountFollow['sequelize'].models.Pod ] | ||
129 | }, | ||
130 | { | ||
131 | model: AccountFollow['sequelize'].models.Account, | ||
132 | as: 'AccountFollowing', | ||
133 | required: true, | ||
134 | where: { | ||
135 | id | ||
136 | } | ||
137 | } | ||
138 | ] | ||
139 | } | ||
140 | |||
141 | return AccountFollow.findAndCountAll(query).then(({ rows, count }) => { | ||
142 | return { | ||
143 | data: rows.map(r => r.AccountFollower), | ||
144 | total: count | ||
145 | } | ||
146 | }) | ||
147 | } | ||
148 | |||
149 | listAcceptedFollowerUrlsForApi = function (id: number, start: number, count?: number) { | ||
150 | return createListAcceptedFollowForApiQuery('followers', id, start, count) | ||
151 | } | ||
152 | |||
153 | listAcceptedFollowingUrlsForApi = function (id: number, start: number, count?: number) { | ||
154 | return createListAcceptedFollowForApiQuery('following', id, start, count) | ||
155 | } | ||
156 | |||
157 | // ------------------------------ UTILS ------------------------------ | ||
158 | |||
159 | async function createListAcceptedFollowForApiQuery (type: 'followers' | 'following', id: number, start: number, count?: number) { | ||
160 | let firstJoin: string | ||
161 | let secondJoin: string | ||
162 | |||
163 | if (type === 'followers') { | ||
164 | firstJoin = 'targetAccountId' | ||
165 | secondJoin = 'accountId' | ||
166 | } else { | ||
167 | firstJoin = 'accountId' | ||
168 | secondJoin = 'targetAccountId' | ||
169 | } | ||
170 | |||
171 | const selections = [ '"Followers"."url" AS "url"', 'COUNT(*) AS "total"' ] | ||
172 | const tasks: Promise<any>[] = [] | ||
173 | |||
174 | for (const selection of selections) { | ||
175 | let query = 'SELECT ' + selection + ' FROM "Account" ' + | ||
176 | 'INNER JOIN "AccountFollow" ON "AccountFollow"."' + firstJoin + '" = "Account"."id" ' + | ||
177 | 'INNER JOIN "Account" AS "Follows" ON "Followers"."id" = "Follows"."' + secondJoin + '" ' + | ||
178 | 'WHERE "Account"."id" = $id AND "AccountFollow"."state" = \'accepted\' ' + | ||
179 | 'LIMIT ' + start | ||
180 | |||
181 | if (count !== undefined) query += ', ' + count | ||
182 | |||
183 | const options = { | ||
184 | bind: { id }, | ||
185 | type: Sequelize.QueryTypes.SELECT | ||
186 | } | ||
187 | tasks.push(AccountFollow['sequelize'].query(query, options)) | ||
188 | } | ||
189 | |||
190 | const [ followers, [ { total } ]] = await Promise.all(tasks) | ||
191 | const urls: string[] = followers.map(f => f.url) | ||
192 | |||
193 | return { | ||
194 | data: urls, | ||
195 | total: parseInt(total, 10) | ||
196 | } | ||
197 | } | ||
diff --git a/server/models/account/account-interface.ts b/server/models/account/account-interface.ts index 6fc36ae9d..ce1afec02 100644 --- a/server/models/account/account-interface.ts +++ b/server/models/account/account-interface.ts | |||
@@ -15,10 +15,6 @@ export namespace AccountMethods { | |||
15 | export type LoadLocalByName = (name: string) => Bluebird<AccountInstance> | 15 | export type LoadLocalByName = (name: string) => Bluebird<AccountInstance> |
16 | export type LoadByNameAndHost = (name: string, host: string) => Bluebird<AccountInstance> | 16 | export type LoadByNameAndHost = (name: string, host: string) => Bluebird<AccountInstance> |
17 | export type ListOwned = () => Bluebird<AccountInstance[]> | 17 | export type ListOwned = () => Bluebird<AccountInstance[]> |
18 | export type ListAcceptedFollowerUrlsForApi = (id: number, start: number, count?: number) => Promise< ResultList<string> > | ||
19 | export type ListAcceptedFollowingUrlsForApi = (id: number, start: number, count?: number) => Promise< ResultList<string> > | ||
20 | export type ListFollowingForApi = (id: number, start: number, count: number, sort: string) => Bluebird< ResultList<AccountInstance> > | ||
21 | export type ListFollowersForApi = (id: number, start: number, count: number, sort: string) => Bluebird< ResultList<AccountInstance> > | ||
22 | 18 | ||
23 | export type ToActivityPubObject = (this: AccountInstance) => ActivityPubActor | 19 | export type ToActivityPubObject = (this: AccountInstance) => ActivityPubActor |
24 | export type ToFormattedJSON = (this: AccountInstance) => FormattedAccount | 20 | export type ToFormattedJSON = (this: AccountInstance) => FormattedAccount |
@@ -38,10 +34,6 @@ export interface AccountClass { | |||
38 | loadLocalByName: AccountMethods.LoadLocalByName | 34 | loadLocalByName: AccountMethods.LoadLocalByName |
39 | loadByNameAndHost: AccountMethods.LoadByNameAndHost | 35 | loadByNameAndHost: AccountMethods.LoadByNameAndHost |
40 | listOwned: AccountMethods.ListOwned | 36 | listOwned: AccountMethods.ListOwned |
41 | listAcceptedFollowerUrlsForApi: AccountMethods.ListAcceptedFollowerUrlsForApi | ||
42 | listAcceptedFollowingUrlsForApi: AccountMethods.ListAcceptedFollowingUrlsForApi | ||
43 | listFollowingForApi: AccountMethods.ListFollowingForApi | ||
44 | listFollowersForApi: AccountMethods.ListFollowersForApi | ||
45 | } | 37 | } |
46 | 38 | ||
47 | export interface AccountAttributes { | 39 | export interface AccountAttributes { |
diff --git a/server/models/account/account.ts b/server/models/account/account.ts index d2293a939..e90eaae5e 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts | |||
@@ -23,7 +23,7 @@ import { | |||
23 | AccountMethods | 23 | AccountMethods |
24 | } from './account-interface' | 24 | } from './account-interface' |
25 | import { sendDeleteAccount } from '../../lib/activitypub/send-request' | 25 | import { sendDeleteAccount } from '../../lib/activitypub/send-request' |
26 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' | 26 | import { CONFIG, CONSTRAINTS_FIELDS } from '../../initializers/constants' |
27 | 27 | ||
28 | let Account: Sequelize.Model<AccountInstance, AccountAttributes> | 28 | let Account: Sequelize.Model<AccountInstance, AccountAttributes> |
29 | let loadAccountByPodAndUUID: AccountMethods.LoadAccountByPodAndUUID | 29 | let loadAccountByPodAndUUID: AccountMethods.LoadAccountByPodAndUUID |
@@ -34,10 +34,6 @@ let loadByUrl: AccountMethods.LoadByUrl | |||
34 | let loadLocalByName: AccountMethods.LoadLocalByName | 34 | let loadLocalByName: AccountMethods.LoadLocalByName |
35 | let loadByNameAndHost: AccountMethods.LoadByNameAndHost | 35 | let loadByNameAndHost: AccountMethods.LoadByNameAndHost |
36 | let listOwned: AccountMethods.ListOwned | 36 | let listOwned: AccountMethods.ListOwned |
37 | let listAcceptedFollowerUrlsForApi: AccountMethods.ListAcceptedFollowerUrlsForApi | ||
38 | let listAcceptedFollowingUrlsForApi: AccountMethods.ListAcceptedFollowingUrlsForApi | ||
39 | let listFollowingForApi: AccountMethods.ListFollowingForApi | ||
40 | let listFollowersForApi: AccountMethods.ListFollowersForApi | ||
41 | let isOwned: AccountMethods.IsOwned | 37 | let isOwned: AccountMethods.IsOwned |
42 | let toActivityPubObject: AccountMethods.ToActivityPubObject | 38 | let toActivityPubObject: AccountMethods.ToActivityPubObject |
43 | let toFormattedJSON: AccountMethods.ToFormattedJSON | 39 | let toFormattedJSON: AccountMethods.ToFormattedJSON |
@@ -185,7 +181,7 @@ export default function defineAccount (sequelize: Sequelize.Sequelize, DataTypes | |||
185 | unique: true | 181 | unique: true |
186 | }, | 182 | }, |
187 | { | 183 | { |
188 | fields: [ 'name', 'podId' ], | 184 | fields: [ 'name', 'podId', 'applicationId' ], |
189 | unique: true | 185 | unique: true |
190 | } | 186 | } |
191 | ], | 187 | ], |
@@ -202,11 +198,7 @@ export default function defineAccount (sequelize: Sequelize.Sequelize, DataTypes | |||
202 | loadByUrl, | 198 | loadByUrl, |
203 | loadLocalByName, | 199 | loadLocalByName, |
204 | loadByNameAndHost, | 200 | loadByNameAndHost, |
205 | listOwned, | 201 | listOwned |
206 | listAcceptedFollowerUrlsForApi, | ||
207 | listAcceptedFollowingUrlsForApi, | ||
208 | listFollowingForApi, | ||
209 | listFollowersForApi | ||
210 | ] | 202 | ] |
211 | const instanceMethods = [ | 203 | const instanceMethods = [ |
212 | isOwned, | 204 | isOwned, |
@@ -286,9 +278,11 @@ function afterDestroy (account: AccountInstance) { | |||
286 | } | 278 | } |
287 | 279 | ||
288 | toFormattedJSON = function (this: AccountInstance) { | 280 | toFormattedJSON = function (this: AccountInstance) { |
281 | let host = this.Pod ? this.Pod.host : CONFIG.WEBSERVER.HOST | ||
282 | |||
289 | const json = { | 283 | const json = { |
290 | id: this.id, | 284 | id: this.id, |
291 | host: this.Pod.host, | 285 | host, |
292 | name: this.name | 286 | name: this.name |
293 | } | 287 | } |
294 | 288 | ||
@@ -346,7 +340,7 @@ getFollowerSharedInboxUrls = function (this: AccountInstance) { | |||
346 | } | 340 | } |
347 | 341 | ||
348 | getFollowingUrl = function (this: AccountInstance) { | 342 | getFollowingUrl = function (this: AccountInstance) { |
349 | return this.url + '/followers' | 343 | return this.url + '/following' |
350 | } | 344 | } |
351 | 345 | ||
352 | getFollowersUrl = function (this: AccountInstance) { | 346 | getFollowersUrl = function (this: AccountInstance) { |
@@ -369,76 +363,6 @@ listOwned = function () { | |||
369 | return Account.findAll(query) | 363 | return Account.findAll(query) |
370 | } | 364 | } |
371 | 365 | ||
372 | listAcceptedFollowerUrlsForApi = function (id: number, start: number, count?: number) { | ||
373 | return createListAcceptedFollowForApiQuery('followers', id, start, count) | ||
374 | } | ||
375 | |||
376 | listAcceptedFollowingUrlsForApi = function (id: number, start: number, count?: number) { | ||
377 | return createListAcceptedFollowForApiQuery('following', id, start, count) | ||
378 | } | ||
379 | |||
380 | listFollowingForApi = function (id: number, start: number, count: number, sort: string) { | ||
381 | const query = { | ||
382 | distinct: true, | ||
383 | offset: start, | ||
384 | limit: count, | ||
385 | order: [ getSort(sort) ], | ||
386 | include: [ | ||
387 | { | ||
388 | model: Account['sequelize'].models.AccountFollow, | ||
389 | required: true, | ||
390 | as: 'following', | ||
391 | include: [ | ||
392 | { | ||
393 | model: Account['sequelize'].models.Account, | ||
394 | as: 'accountFollowing', | ||
395 | required: true, | ||
396 | include: [ Account['sequelize'].models.Pod ] | ||
397 | } | ||
398 | ] | ||
399 | } | ||
400 | ] | ||
401 | } | ||
402 | |||
403 | return Account.findAndCountAll(query).then(({ rows, count }) => { | ||
404 | return { | ||
405 | data: rows, | ||
406 | total: count | ||
407 | } | ||
408 | }) | ||
409 | } | ||
410 | |||
411 | listFollowersForApi = function (id: number, start: number, count: number, sort: string) { | ||
412 | const query = { | ||
413 | distinct: true, | ||
414 | offset: start, | ||
415 | limit: count, | ||
416 | order: [ getSort(sort) ], | ||
417 | include: [ | ||
418 | { | ||
419 | model: Account['sequelize'].models.AccountFollow, | ||
420 | required: true, | ||
421 | as: 'followers', | ||
422 | include: [ | ||
423 | { | ||
424 | model: Account['sequelize'].models.Account, | ||
425 | as: 'accountFollowers', | ||
426 | required: true, | ||
427 | include: [ Account['sequelize'].models.Pod ] | ||
428 | } | ||
429 | ] | ||
430 | } | ||
431 | ] | ||
432 | } | ||
433 | |||
434 | return Account.findAndCountAll(query).then(({ rows, count }) => { | ||
435 | return { | ||
436 | data: rows, | ||
437 | total: count | ||
438 | } | ||
439 | }) | ||
440 | } | ||
441 | |||
442 | loadApplication = function () { | 366 | loadApplication = function () { |
443 | return Account.findOne({ | 367 | return Account.findOne({ |
444 | include: [ | 368 | include: [ |
@@ -527,45 +451,3 @@ loadAccountByPodAndUUID = function (uuid: string, podId: number, transaction: Se | |||
527 | 451 | ||
528 | return Account.find(query) | 452 | return Account.find(query) |
529 | } | 453 | } |
530 | |||
531 | // ------------------------------ UTILS ------------------------------ | ||
532 | |||
533 | async function createListAcceptedFollowForApiQuery (type: 'followers' | 'following', id: number, start: number, count?: number) { | ||
534 | let firstJoin: string | ||
535 | let secondJoin: string | ||
536 | |||
537 | if (type === 'followers') { | ||
538 | firstJoin = 'targetAccountId' | ||
539 | secondJoin = 'accountId' | ||
540 | } else { | ||
541 | firstJoin = 'accountId' | ||
542 | secondJoin = 'targetAccountId' | ||
543 | } | ||
544 | |||
545 | const selections = [ '"Followers"."url" AS "url"', 'COUNT(*) AS "total"' ] | ||
546 | const tasks: Promise<any>[] = [] | ||
547 | |||
548 | for (const selection of selections) { | ||
549 | let query = 'SELECT ' + selection + ' FROM "Account" ' + | ||
550 | 'INNER JOIN "AccountFollow" ON "AccountFollow"."' + firstJoin + '" = "Account"."id" ' + | ||
551 | 'INNER JOIN "Account" AS "Follows" ON "Followers"."id" = "Follows"."' + secondJoin + '" ' + | ||
552 | 'WHERE "Account"."id" = $id AND "AccountFollow"."state" = \'accepted\' ' + | ||
553 | 'LIMIT ' + start | ||
554 | |||
555 | if (count !== undefined) query += ', ' + count | ||
556 | |||
557 | const options = { | ||
558 | bind: { id }, | ||
559 | type: Sequelize.QueryTypes.SELECT | ||
560 | } | ||
561 | tasks.push(Account['sequelize'].query(query, options)) | ||
562 | } | ||
563 | |||
564 | const [ followers, [ { total } ]] = await Promise.all(tasks) | ||
565 | const urls: string[] = followers.map(f => f.url) | ||
566 | |||
567 | return { | ||
568 | data: urls, | ||
569 | total: parseInt(total, 10) | ||
570 | } | ||
571 | } | ||