diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bb2441701..b7000100e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -528,7 +528,7 @@ jobs: report_name: Coverage Backend type: lcov result_path: ./backend/coverage/lcov.info - min_coverage: 66 + min_coverage: 68 token: ${{ github.token }} ########################################################################## diff --git a/admin/src/graphql/listTransactionLinksAdmin.js b/admin/src/graphql/listTransactionLinksAdmin.js index 3c9eae100..2e4171f02 100644 --- a/admin/src/graphql/listTransactionLinksAdmin.js +++ b/admin/src/graphql/listTransactionLinksAdmin.js @@ -2,7 +2,12 @@ import gql from 'graphql-tag' export const listTransactionLinksAdmin = gql` query ($currentPage: Int = 1, $pageSize: Int = 5, $userId: Int!) { - listTransactionLinksAdmin(currentPage: $currentPage, pageSize: $pageSize, userId: $userId) { + listTransactionLinksAdmin( + currentPage: $currentPage + pageSize: $pageSize + userId: $userId + filters: { withRedeemed: true, withExpired: true, withDeleted: true } + ) { linkCount linkList { id diff --git a/admin/src/graphql/searchUsers.js b/admin/src/graphql/searchUsers.js index 5740e24cc..cc7f83b1c 100644 --- a/admin/src/graphql/searchUsers.js +++ b/admin/src/graphql/searchUsers.js @@ -1,12 +1,7 @@ import gql from 'graphql-tag' export const searchUsers = gql` - query ( - $searchText: String! - $currentPage: Int - $pageSize: Int - $filters: SearchUsersFiltersInput - ) { + query ($searchText: String!, $currentPage: Int, $pageSize: Int, $filters: SearchUsersFilters) { searchUsers( searchText: $searchText currentPage: $currentPage diff --git a/admin/src/pages/Creation.spec.js b/admin/src/pages/Creation.spec.js index 432cbe19b..9524fc5d6 100644 --- a/admin/src/pages/Creation.spec.js +++ b/admin/src/pages/Creation.spec.js @@ -72,8 +72,8 @@ describe('Creation', () => { currentPage: 1, pageSize: 25, filters: { - filterByActivated: true, - filterByDeleted: false, + byActivated: true, + byDeleted: false, }, }, }), @@ -274,8 +274,8 @@ describe('Creation', () => { currentPage: 1, pageSize: 25, filters: { - filterByActivated: true, - filterByDeleted: false, + byActivated: true, + byDeleted: false, }, }, }), @@ -293,8 +293,8 @@ describe('Creation', () => { currentPage: 1, pageSize: 25, filters: { - filterByActivated: true, - filterByDeleted: false, + byActivated: true, + byDeleted: false, }, }, }), @@ -312,8 +312,8 @@ describe('Creation', () => { currentPage: 2, pageSize: 25, filters: { - filterByActivated: true, - filterByDeleted: false, + byActivated: true, + byDeleted: false, }, }, }), diff --git a/admin/src/pages/Creation.vue b/admin/src/pages/Creation.vue index 17962bfff..9e554ff92 100644 --- a/admin/src/pages/Creation.vue +++ b/admin/src/pages/Creation.vue @@ -103,8 +103,8 @@ export default { currentPage: this.currentPage, pageSize: this.perPage, filters: { - filterByActivated: true, - filterByDeleted: false, + byActivated: true, + byDeleted: false, }, }, fetchPolicy: 'network-only', diff --git a/admin/src/pages/UserSearch.spec.js b/admin/src/pages/UserSearch.spec.js index a1d809a66..7d8be648f 100644 --- a/admin/src/pages/UserSearch.spec.js +++ b/admin/src/pages/UserSearch.spec.js @@ -83,8 +83,8 @@ describe('UserSearch', () => { currentPage: 1, pageSize: 25, filters: { - filterByActivated: null, - filterByDeleted: null, + byActivated: null, + byDeleted: null, }, }, }), @@ -104,8 +104,8 @@ describe('UserSearch', () => { currentPage: 1, pageSize: 25, filters: { - filterByActivated: false, - filterByDeleted: null, + byActivated: false, + byDeleted: null, }, }, }), @@ -126,8 +126,8 @@ describe('UserSearch', () => { currentPage: 1, pageSize: 25, filters: { - filterByActivated: null, - filterByDeleted: true, + byActivated: null, + byDeleted: true, }, }, }), @@ -148,8 +148,8 @@ describe('UserSearch', () => { currentPage: 2, pageSize: 25, filters: { - filterByActivated: null, - filterByDeleted: null, + byActivated: null, + byDeleted: null, }, }, }), @@ -170,8 +170,8 @@ describe('UserSearch', () => { currentPage: 1, pageSize: 25, filters: { - filterByActivated: null, - filterByDeleted: null, + byActivated: null, + byDeleted: null, }, }, }), @@ -189,8 +189,8 @@ describe('UserSearch', () => { currentPage: 1, pageSize: 25, filters: { - filterByActivated: null, - filterByDeleted: null, + byActivated: null, + byDeleted: null, }, }, }), diff --git a/admin/src/pages/UserSearch.vue b/admin/src/pages/UserSearch.vue index 7b638c316..f1ca31d14 100644 --- a/admin/src/pages/UserSearch.vue +++ b/admin/src/pages/UserSearch.vue @@ -4,9 +4,9 @@ {{ - filterByActivated === null + filters.byActivated === null ? $t('all_emails') - : filterByActivated === false + : filters.byActivated === false ? $t('unregistered_emails') : '' }} @@ -14,9 +14,9 @@ {{ - filterByDeleted === null + filters.byDeleted === null ? $t('all_emails') - : filterByDeleted === true + : filters.byDeleted === true ? $t('deleted_user') : '' }} @@ -72,8 +72,10 @@ export default { searchResult: [], massCreation: [], criteria: '', - filterByActivated: null, - filterByDeleted: null, + filters: { + byActivated: null, + byDeleted: null, + }, rows: 0, currentPage: 1, perPage: 25, @@ -82,11 +84,11 @@ export default { }, methods: { unconfirmedRegisterMails() { - this.filterByActivated = this.filterByActivated === null ? false : null + this.filters.byActivated = this.filters.byActivated === null ? false : null this.getUsers() }, deletedUserSearch() { - this.filterByDeleted = this.filterByDeleted === null ? true : null + this.filters.byDeleted = this.filters.byDeleted === null ? true : null this.getUsers() }, getUsers() { @@ -97,10 +99,7 @@ export default { searchText: this.criteria, currentPage: this.currentPage, pageSize: this.perPage, - filters: { - filterByActivated: this.filterByActivated, - filterByDeleted: this.filterByDeleted, - }, + filters: this.filters, }, fetchPolicy: 'no-cache', }) diff --git a/backend/src/graphql/arg/SearchUsersArgs.ts b/backend/src/graphql/arg/SearchUsersArgs.ts index 8db6bfc06..38057762d 100644 --- a/backend/src/graphql/arg/SearchUsersArgs.ts +++ b/backend/src/graphql/arg/SearchUsersArgs.ts @@ -12,6 +12,6 @@ export default class SearchUsersArgs { @Field(() => Int, { nullable: true }) pageSize?: number - @Field(() => SearchUsersFilters, { nullable: true }) + @Field(() => SearchUsersFilters, { nullable: true, defaultValue: null }) filters: SearchUsersFilters } diff --git a/backend/src/graphql/arg/SearchUsersFilters.ts b/backend/src/graphql/arg/SearchUsersFilters.ts index de7c7c20a..dc19d456c 100644 --- a/backend/src/graphql/arg/SearchUsersFilters.ts +++ b/backend/src/graphql/arg/SearchUsersFilters.ts @@ -1,11 +1,10 @@ -import { Field, InputType, ObjectType } from 'type-graphql' +import { Field, InputType } from 'type-graphql' -@ObjectType() -@InputType('SearchUsersFiltersInput') +@InputType() export default class SearchUsersFilters { @Field(() => Boolean, { nullable: true, defaultValue: null }) - filterByActivated?: boolean | null + byActivated: boolean @Field(() => Boolean, { nullable: true, defaultValue: null }) - filterByDeleted?: boolean | null + byDeleted: boolean } diff --git a/backend/src/graphql/arg/TransactionLinkFilters.ts b/backend/src/graphql/arg/TransactionLinkFilters.ts index b009a3180..291d244c3 100644 --- a/backend/src/graphql/arg/TransactionLinkFilters.ts +++ b/backend/src/graphql/arg/TransactionLinkFilters.ts @@ -1,13 +1,13 @@ -import { ArgsType, Field } from 'type-graphql' +import { Field, InputType } from 'type-graphql' -@ArgsType() +@InputType() export default class TransactionLinkFilters { - @Field(() => Boolean, { nullable: true, defaultValue: true }) - filterByDeleted?: boolean + @Field(() => Boolean, { nullable: true }) + withDeleted: boolean - @Field(() => Boolean, { nullable: true, defaultValue: true }) - filterByExpired?: boolean + @Field(() => Boolean, { nullable: true }) + withExpired: boolean - @Field(() => Boolean, { nullable: true, defaultValue: true }) - filterByRedeemed?: boolean + @Field(() => Boolean, { nullable: true }) + withRedeemed: boolean } diff --git a/backend/src/graphql/resolver/AdminResolver.test.ts b/backend/src/graphql/resolver/AdminResolver.test.ts index 4771232ea..acf880efb 100644 --- a/backend/src/graphql/resolver/AdminResolver.test.ts +++ b/backend/src/graphql/resolver/AdminResolver.test.ts @@ -1,10 +1,13 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -import { convertObjValuesToArray } from '@/util/utilities' +import { objectValuesToArray } from '@/util/utilities' import { testEnvironment, resetToken, cleanDB } from '@test/helpers' import { userFactory } from '@/seeds/factory/user' import { creationFactory } from '@/seeds/factory/creation' +import { creations } from '@/seeds/creation/index' +import { transactionLinkFactory } from '@/seeds/factory/transactionLink' +import { transactionLinks } from '@/seeds/transactionLink/index' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { peterLustig } from '@/seeds/users/peter-lustig' import { stephenHawking } from '@/seeds/users/stephen-hawking' @@ -12,14 +15,18 @@ import { garrickOllivander } from '@/seeds/users/garrick-ollivander' import { deleteUser, unDeleteUser, - searchUsers, createPendingCreation, createPendingCreations, updatePendingCreation, deletePendingCreation, confirmPendingCreation, } from '@/seeds/graphql/mutations' -import { getPendingCreations, login } from '@/seeds/graphql/queries' +import { + getPendingCreations, + login, + searchUsers, + listTransactionLinksAdmin, +} from '@/seeds/graphql/queries' import { GraphQLError } from 'graphql' import { User } from '@entity/User' /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ @@ -366,7 +373,7 @@ describe('AdminResolver', () => { data: { searchUsers: { userCount: 4, - userList: expect.arrayContaining(convertObjValuesToArray(allUsers)), + userList: expect.arrayContaining(objectValuesToArray(allUsers)), }, }, }), @@ -382,8 +389,8 @@ describe('AdminResolver', () => { variables: { ...variablesWithoutTextAndFilters, filters: { - filterByActivated: null, - filterByDeleted: null, + byActivated: null, + byDeleted: null, }, }, }), @@ -392,7 +399,7 @@ describe('AdminResolver', () => { data: { searchUsers: { userCount: 4, - userList: expect.arrayContaining(convertObjValuesToArray(allUsers)), + userList: expect.arrayContaining(objectValuesToArray(allUsers)), }, }, }), @@ -408,8 +415,8 @@ describe('AdminResolver', () => { variables: { ...variablesWithoutTextAndFilters, filters: { - filterByActivated: false, - filterByDeleted: null, + byActivated: false, + byDeleted: null, }, }, }), @@ -434,8 +441,8 @@ describe('AdminResolver', () => { variables: { ...variablesWithoutTextAndFilters, filters: { - filterByActivated: null, - filterByDeleted: true, + byActivated: null, + byDeleted: true, }, }, }), @@ -460,8 +467,8 @@ describe('AdminResolver', () => { variables: { ...variablesWithoutTextAndFilters, filters: { - filterByActivated: false, - filterByDeleted: true, + byActivated: false, + byDeleted: true, }, }, }), @@ -1324,4 +1331,266 @@ describe('AdminResolver', () => { }) }) }) + + describe('transaction links list', () => { + const variables = { + userId: 1, // dummy, may be replaced + filters: null, + currentPage: 1, + pageSize: 5, + } + + describe('unauthenticated', () => { + it('returns an error', async () => { + await expect( + query({ + query: listTransactionLinksAdmin, + variables, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + }) + + describe('authenticated', () => { + describe('without admin rights', () => { + beforeAll(async () => { + user = await userFactory(testEnv, bibiBloxberg) + await query({ + query: login, + variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, + }) + }) + + afterAll(async () => { + await cleanDB() + resetToken() + }) + + it('returns an error', async () => { + await expect( + query({ + query: listTransactionLinksAdmin, + variables, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + }) + + describe('with admin rights', () => { + beforeAll(async () => { + // admin 'peter@lustig.de' has to exists for 'creationFactory' + admin = await userFactory(testEnv, peterLustig) + + user = await userFactory(testEnv, bibiBloxberg) + variables.userId = user.id + variables.pageSize = 25 + // bibi needs GDDs + const bibisCreation = creations.find((creation) => creation.email === 'bibi@bloxberg.de') + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await creationFactory(testEnv, bibisCreation!) + // bibis transaktion links + const bibisTransaktionLinks = transactionLinks.filter( + (transactionLink) => transactionLink.email === 'bibi@bloxberg.de', + ) + for (let i = 0; i < bibisTransaktionLinks.length; i++) { + await transactionLinkFactory(testEnv, bibisTransaktionLinks[i]) + } + + // admin: only now log in + await query({ + query: login, + variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, + }) + }) + + afterAll(async () => { + await cleanDB() + resetToken() + }) + + describe('without any filters', () => { + it('finds 6 open transaction links and no deleted or redeemed', async () => { + await expect( + query({ + query: listTransactionLinksAdmin, + variables, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + listTransactionLinksAdmin: { + linkCount: 6, + linkList: expect.not.arrayContaining([ + expect.objectContaining({ + memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(', + createdAt: expect.any(String), + }), + expect.objectContaining({ + memo: 'Da habe ich mich wohl etwas übernommen.', + deletedAt: expect.any(String), + }), + ]), + }, + }, + }), + ) + }) + }) + + describe('all filters are null', () => { + it('finds 6 open transaction links and no deleted or redeemed', async () => { + await expect( + query({ + query: listTransactionLinksAdmin, + variables: { + ...variables, + filters: { + withDeleted: null, + withExpired: null, + withRedeemed: null, + }, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + listTransactionLinksAdmin: { + linkCount: 6, + linkList: expect.not.arrayContaining([ + expect.objectContaining({ + memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(', + createdAt: expect.any(String), + }), + expect.objectContaining({ + memo: 'Da habe ich mich wohl etwas übernommen.', + deletedAt: expect.any(String), + }), + ]), + }, + }, + }), + ) + }) + }) + + describe('filter with deleted', () => { + it('finds 6 open transaction links, 1 deleted, and no redeemed', async () => { + await expect( + query({ + query: listTransactionLinksAdmin, + variables: { + ...variables, + filters: { + withDeleted: true, + }, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + listTransactionLinksAdmin: { + linkCount: 7, + linkList: expect.arrayContaining([ + expect.not.objectContaining({ + memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(', + createdAt: expect.any(String), + }), + expect.objectContaining({ + memo: 'Da habe ich mich wohl etwas übernommen.', + deletedAt: expect.any(String), + }), + ]), + }, + }, + }), + ) + }) + }) + + describe('filter by expired', () => { + it('finds 5 open transaction links, 1 expired, and no redeemed', async () => { + await expect( + query({ + query: listTransactionLinksAdmin, + variables: { + ...variables, + filters: { + withExpired: true, + }, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + listTransactionLinksAdmin: { + linkCount: 7, + linkList: expect.arrayContaining([ + expect.objectContaining({ + memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(', + createdAt: expect.any(String), + }), + expect.not.objectContaining({ + memo: 'Da habe ich mich wohl etwas übernommen.', + deletedAt: expect.any(String), + }), + ]), + }, + }, + }), + ) + }) + }) + + // TODO: works not as expected, because 'redeemedAt' and 'redeemedBy' have to be added to the transaktion link factory + describe.skip('filter by redeemed', () => { + it('finds 6 open transaction links, 1 deleted, and no redeemed', async () => { + await expect( + query({ + query: listTransactionLinksAdmin, + variables: { + ...variables, + filters: { + withDeleted: null, + withExpired: null, + withRedeemed: true, + }, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + listTransactionLinksAdmin: { + linkCount: 6, + linkList: expect.arrayContaining([ + expect.not.objectContaining({ + memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(', + createdAt: expect.any(String), + }), + expect.objectContaining({ + memo: 'Yeah, eingelöst!', + redeemedAt: expect.any(String), + redeemedBy: expect.any(Number), + }), + expect.not.objectContaining({ + memo: 'Da habe ich mich wohl etwas übernommen.', + deletedAt: expect.any(String), + }), + ]), + }, + }, + }), + ) + }) + }) + }) + }) + }) }) diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index 8c3d71b73..4c94e48c8 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -58,12 +58,12 @@ export class AdminResolver { const filterCriteria: ObjectLiteral[] = [] if (filters) { - if (filters.filterByActivated !== null) { - filterCriteria.push({ emailChecked: filters.filterByActivated }) + if (filters.byActivated !== null) { + filterCriteria.push({ emailChecked: filters.byActivated }) } - if (filters.filterByDeleted !== null) { - filterCriteria.push({ deletedAt: filters.filterByDeleted ? Not(IsNull()) : IsNull() }) + if (filters.byDeleted !== null) { + filterCriteria.push({ deletedAt: filters.byDeleted ? Not(IsNull()) : IsNull() }) } } @@ -426,9 +426,10 @@ export class AdminResolver { async listTransactionLinksAdmin( @Args() { currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated, - @Args() + @Arg('filters', () => TransactionLinkFilters, { nullable: true }) filters: TransactionLinkFilters, - @Arg('userId', () => Int) userId: number, + @Arg('userId', () => Int) + userId: number, ): Promise { const user = await dbUser.findOneOrFail({ id: userId }) const where: { @@ -437,12 +438,16 @@ export class AdminResolver { validUntil?: FindOperator | null } = { userId, + redeemedBy: null, + validUntil: MoreThan(new Date()), + } + if (filters) { + if (filters.withRedeemed) delete where.redeemedBy + if (filters.withExpired) delete where.validUntil } - if (!filters.filterByRedeemed) where.redeemedBy = null - if (!filters.filterByExpired) where.validUntil = MoreThan(new Date()) const [transactionLinks, count] = await dbTransactionLink.findAndCount({ where, - withDeleted: filters.filterByDeleted, + withDeleted: filters ? filters.withDeleted : false, order: { createdAt: order, }, diff --git a/backend/src/seeds/graphql/mutations.ts b/backend/src/seeds/graphql/mutations.ts index 6e1fe9174..e66827566 100644 --- a/backend/src/seeds/graphql/mutations.ts +++ b/backend/src/seeds/graphql/mutations.ts @@ -105,35 +105,6 @@ export const unDeleteUser = gql` } ` -export const searchUsers = gql` - query ( - $searchText: String! - $currentPage: Int - $pageSize: Int - $filters: SearchUsersFiltersInput - ) { - searchUsers( - searchText: $searchText - currentPage: $currentPage - pageSize: $pageSize - filters: $filters - ) { - userCount - userList { - userId - firstName - lastName - email - creation - emailChecked - hasElopage - emailConfirmationSend - deletedAt - } - } - } -` - export const createPendingCreations = gql` mutation ($pendingCreations: [CreatePendingCreationArgs!]!) { createPendingCreations(pendingCreations: $pendingCreations) { diff --git a/backend/src/seeds/graphql/queries.ts b/backend/src/seeds/graphql/queries.ts index 16b2b71ae..03ee3b53e 100644 --- a/backend/src/seeds/graphql/queries.ts +++ b/backend/src/seeds/graphql/queries.ts @@ -91,6 +91,30 @@ export const sendResetPasswordEmail = gql` } ` +export const searchUsers = gql` + query ($searchText: String!, $currentPage: Int, $pageSize: Int, $filters: SearchUsersFilters) { + searchUsers( + searchText: $searchText + currentPage: $currentPage + pageSize: $pageSize + filters: $filters + ) { + userCount + userList { + userId + firstName + lastName + email + creation + emailChecked + hasElopage + emailConfirmationSend + deletedAt + } + } + } +` + export const listGDTEntriesQuery = gql` query ($currentPage: Int!, $pageSize: Int!) { listGDTEntries(currentPage: $currentPage, pageSize: $pageSize) { @@ -164,3 +188,32 @@ export const getPendingCreations = gql` } } ` + +export const listTransactionLinksAdmin = gql` + query ( + $userId: Int! + $filters: TransactionLinkFilters + $currentPage: Int = 1 + $pageSize: Int = 5 + ) { + listTransactionLinksAdmin( + userId: $userId + filters: $filters + currentPage: $currentPage + pageSize: $pageSize + ) { + linkCount + linkList { + id + amount + holdAvailableAmount + memo + code + createdAt + validUntil + redeemedAt + deletedAt + } + } + } +` diff --git a/backend/src/seeds/transactionLink/TransactionLinkInterface.ts b/backend/src/seeds/transactionLink/TransactionLinkInterface.ts index ddc835b33..eaacfdf92 100644 --- a/backend/src/seeds/transactionLink/TransactionLinkInterface.ts +++ b/backend/src/seeds/transactionLink/TransactionLinkInterface.ts @@ -3,5 +3,8 @@ export interface TransactionLinkInterface { amount: number memo: string createdAt?: Date + // TODO: for testing + // redeemedAt?: Date + // redeemedBy?: number deletedAt?: boolean } diff --git a/backend/src/seeds/transactionLink/index.ts b/backend/src/seeds/transactionLink/index.ts index 25755c0f2..c3245c2b6 100644 --- a/backend/src/seeds/transactionLink/index.ts +++ b/backend/src/seeds/transactionLink/index.ts @@ -30,6 +30,10 @@ bei Gradidio sei dabei!`, amount: 19.99, memo: `Kein Trick, keine Zauberrei, bei Gradidio sei dabei!`, + // TODO: for testing + // memo: `Yeah, eingelöst!`, + // redeemedAt: new Date(2022, 2, 2), + // redeemedBy: not null, }, { email: 'bibi@bloxberg.de', diff --git a/backend/src/util/utilities.ts b/backend/src/util/utilities.ts index f77ad05ec..9abb31554 100644 --- a/backend/src/util/utilities.ts +++ b/backend/src/util/utilities.ts @@ -1,4 +1,4 @@ -export const convertObjValuesToArray = (obj: { [x: string]: string }): Array => { +export const objectValuesToArray = (obj: { [x: string]: string }): Array => { return Object.keys(obj).map(function (key) { return obj[key] })