Skip to content

Commit 83029b7

Browse files
committed
improve type safety with generic Entity interface and extracted test utilities
1 parent aeb2405 commit 83029b7

File tree

6 files changed

+64
-60
lines changed

6 files changed

+64
-60
lines changed

src/lib/mutation.test.ts

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,5 @@
11
import buildMutation from './mutation';
2-
import type { Entity } from './resolver';
3-
4-
function makeEntity(initial: Array<Record<string, any>> = []): Entity & {
5-
_store: Array<Record<string, any>>;
6-
} {
7-
const store = initial.map((r) => ({ ...r }));
8-
return {
9-
_store: store,
10-
rawAttributes: { id: {}, name: {} },
11-
findOne: async ({ where: { id } }: any) => store.find((r) => r.id === id) ?? null,
12-
findAndCountAll: async () => ({ rows: store }),
13-
create: async (item: any) => {
14-
store.push(item);
15-
return item;
16-
},
17-
update: async (item: any, { where: { id } }: any) => {
18-
const idx = store.findIndex((r) => r.id === id);
19-
if (idx >= 0) store[idx] = { ...store[idx], ...item };
20-
return [1];
21-
},
22-
} as unknown as Entity & { _store: Array<Record<string, any>> };
23-
}
2+
import { makeEntity } from './testUtils/fakeEntity';
243

254
describe('buildMutation', () => {
265
test('create mutation resolves to created entity', async () => {

src/lib/query.test.ts

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,6 @@
11
import buildQuery from './query';
2-
import type { Entity } from './resolver';
32
import { GraphQLObjectType, GraphQLInt } from 'graphql';
4-
5-
// Minimal fake entity implementation
6-
function makeEntity(initial: Array<Record<string, any>> = []): Entity & {
7-
_store: Array<Record<string, any>>;
8-
} {
9-
const store = initial.map((r) => ({ ...r }));
10-
return {
11-
_store: store,
12-
rawAttributes: { id: {}, name: {} },
13-
findOne: async ({ where: { id } }: any) => store.find((r) => r.id === id) ?? null,
14-
findAndCountAll: async () => ({ rows: store }),
15-
create: async (item: any) => {
16-
store.push(item);
17-
return item;
18-
},
19-
update: async (item: any, { where: { id } }: any) => {
20-
const idx = store.findIndex((r) => r.id === id);
21-
if (idx >= 0) store[idx] = { ...store[idx], ...item };
22-
return [1];
23-
},
24-
} as unknown as Entity & { _store: Array<Record<string, any>> };
25-
}
3+
import { makeEntity } from './testUtils/fakeEntity';
264

275
describe('buildQuery', () => {
286
test('resolves by id returns array of one resolved item', async () => {

src/lib/resolver.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,18 @@ type AllOptions = {
2323
all?: boolean;
2424
};
2525

26-
class Resolver {
27-
private entity: Entity;
26+
class Resolver<T extends Record<string, unknown> = Record<string, unknown>> {
27+
private entity: Entity<T>;
2828

29-
constructor(entity: Entity) {
29+
constructor(entity: Entity<T>) {
3030
this.entity = entity;
3131
}
3232

33-
find(id: number | string) {
33+
find(id: number | string): Promise<T | null> {
3434
return this.entity.findOne({ where: { id: id } }).then((items) => items);
3535
}
3636

37-
all(options?: AllOptions) {
37+
all(options?: AllOptions): Promise<T[]> {
3838
const hasNameField = Object.getOwnPropertyNames(this.entity.rawAttributes).indexOf('name') == 1;
3939
const defaults = {
4040
perPage: Number(process.env.APP_PERPAGE),
@@ -58,20 +58,22 @@ class Resolver {
5858
return this.entity.findAndCountAll(queryOptions).then((items) => items.rows);
5959
}
6060

61-
create(item: Record<string, unknown>) {
61+
create(item: Partial<T>): Promise<T> {
6262
return this.entity.create(item).then((result) => result);
6363
}
6464

65-
update(id: number | string, item: Record<string, unknown>) {
65+
update<TId extends number | string, TInput extends Partial<T>>(id: TId, item: TInput): Promise<T | null> {
6666
return this.entity.update(item, { where: { id: id } }).then(() => this.find(id));
6767
}
6868

6969
page(pageNumber: number) {
7070
return Number(process.env.APP_PERPAGE) * pageNumber;
7171
}
7272

73-
destroy(id: number | string) {
74-
return this.entity.update({ enable: false }, { where: { id: id } }).then(() => this.find(id));
73+
destroy(id: number | string): Promise<T | null> {
74+
return this.entity
75+
.update({ enable: false } as unknown as Partial<T>, { where: { id: id } })
76+
.then(() => this.find(id));
7577
}
7678
}
7779

src/lib/testUtils/fakeEntity.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import type { Entity } from '../resolver';
2+
3+
export type FakeRow = { id: number | string } & Record<string, unknown>;
4+
5+
export function makeEntity(initial: FakeRow[] = []): (Entity<FakeRow> & { _store: FakeRow[] }) {
6+
const store = initial.map((r) => ({ ...r })) as FakeRow[];
7+
return {
8+
_store: store,
9+
rawAttributes: { id: {}, name: {} },
10+
findOne: async ({ where: { id } }: { where: { id: number | string } }) =>
11+
(store.find((r) => r.id === id) ?? null),
12+
findAndCountAll: async () => ({ rows: store }),
13+
create: async (item: Partial<FakeRow>) => {
14+
const next = { ...item } as FakeRow;
15+
store.push(next);
16+
return next;
17+
},
18+
update: async (item: Partial<FakeRow>, { where: { id } }: { where: { id: number | string } }) => {
19+
const idx = store.findIndex((r) => r.id === id);
20+
if (idx >= 0) store[idx] = { ...store[idx], ...item } as FakeRow;
21+
return [1];
22+
},
23+
} as unknown as Entity<FakeRow> & { _store: FakeRow[] };
24+
}
25+
26+
export function makeEntityNoName(initial: FakeRow[] = []): (Entity<FakeRow> & { _store: FakeRow[] }) {
27+
const store = initial.map((r) => ({ ...r })) as FakeRow[];
28+
return {
29+
_store: store,
30+
rawAttributes: { id: {} },
31+
findOne: async ({ where: { id } }: { where: { id: number | string } }) =>
32+
(store.find((r) => r.id === id) ?? null),
33+
findAndCountAll: async () => ({ rows: store }),
34+
create: async (item: Partial<FakeRow>) => {
35+
const next = { ...item } as FakeRow;
36+
store.push(next);
37+
return next;
38+
},
39+
update: async (item: Partial<FakeRow>, { where: { id } }: { where: { id: number | string } }) => {
40+
const idx = store.findIndex((r) => r.id === id);
41+
if (idx >= 0) store[idx] = { ...store[idx], ...item } as FakeRow;
42+
return [1];
43+
},
44+
} as unknown as Entity<FakeRow> & { _store: FakeRow[] };
45+
}

src/types/express-graphql.d.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ declare module 'express-graphql' {
88
graphiql?: boolean;
99
context?: unknown;
1010
pretty?: boolean;
11-
formatError?: (error: any) => any;
12-
extensions?: (info: any) => any;
13-
validationRules?: any[];
14-
customFormatErrorFn?: (error: any) => any;
11+
formatError?: (error: unknown) => unknown;
12+
extensions?: (info: unknown) => unknown;
13+
validationRules?: unknown[];
14+
customFormatErrorFn?: (error: unknown) => unknown;
1515
}
1616

17-
export default function graphqlHTTP(options: Options | ((req: any) => Options)): RequestHandler;
17+
export default function graphqlHTTP(options: Options | ((req: unknown) => Options)): RequestHandler;
1818
}

src/types/sequelize.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
declare module 'sequelize' {
2-
const Sequelize: any;
2+
const Sequelize: unknown;
33
export default Sequelize;
44
}

0 commit comments

Comments
 (0)