@squiz/db-lib 1.12.0-alpha.8 → 1.12.1-alpha.0

Sign up to get free protection for your applications and to get access to all the features.
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@squiz/db-lib",
3
- "version": "1.12.0-alpha.8",
3
+ "version": "1.12.1-alpha.0",
4
4
  "description": "",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
7
7
  "compile": "node build.js && tsc",
8
- "test": "jest -c jest.config.ts --passWithNoTests",
8
+ "test": "jest -c jest.config.ts --testMatch=\"**/*.spec.ts\" --testMatch=\"!**/*.integration.spec.ts\" --passWithNoTests",
9
+ "test:integration": "jest --testMatch=\"**/*.integration.spec.ts\" --passWithNoTests",
9
10
  "test:update-snapshots": "jest -c jest.config.ts --updateSnapshot",
10
11
  "clean": "rimraf \"tsconfig.tsbuildinfo\" \"./lib\""
11
12
  },
@@ -19,16 +20,16 @@
19
20
  "fs-extra": "11.1.0",
20
21
  "jest": "29.4.1",
21
22
  "rimraf": "4.1.2",
22
- "ts-jest": "28.0.8",
23
+ "ts-jest": "29.0.5",
23
24
  "ts-loader": "9.3.1",
24
25
  "ts-node": "10.9.1",
25
26
  "typescript": "4.9.4"
26
27
  },
27
28
  "dependencies": {
28
29
  "@aws-sdk/client-secrets-manager": "3.264.0",
29
- "@squiz/dx-logger-lib": "1.12.0-alpha.8",
30
+ "@squiz/dx-logger-lib": "1.12.1-alpha.0",
30
31
  "dotenv": "16.0.3",
31
32
  "pg": "8.9.0"
32
33
  },
33
- "gitHead": "3a835dd86ed757cb28f4fc51a80114215f4fea41"
34
+ "gitHead": "4a0c81e76ca50706d137dee82869e3ea7844376e"
34
35
  }
@@ -0,0 +1,155 @@
1
+ import { AbstractRepository } from './AbstractRepository';
2
+ import { ConnectionManager } from './ConnectionManager';
3
+
4
+ interface IRepoTest {
5
+ lowercase: string;
6
+ camelCase: string;
7
+ PascalCase: string;
8
+ snake_case: string;
9
+ 'kebab-case': string;
10
+ UPPERCASE: string;
11
+ }
12
+
13
+ class RepoTest implements IRepoTest {
14
+ public lowercase: string;
15
+ public camelCase: string;
16
+ public PascalCase: string;
17
+ public snake_case: string;
18
+ public 'kebab-case': string;
19
+ public UPPERCASE: string;
20
+
21
+ constructor(data: Partial<IRepoTest> = {}) {
22
+ this.lowercase = data.lowercase ?? 'some lowercase';
23
+ this.camelCase = data.camelCase ?? 'some camelCase';
24
+ this.PascalCase = data.PascalCase ?? 'some PascalCase';
25
+ this.snake_case = data.snake_case ?? 'some snake_case';
26
+ this['kebab-case'] = data['kebab-case'] ?? 'some kebab-case';
27
+ this.UPPERCASE = data.UPPERCASE ?? 'some UPPERCASE';
28
+ }
29
+ }
30
+
31
+ function defaultMapping(): { [key in keyof IRepoTest]: string } {
32
+ return {
33
+ lowercase: 'lowercase',
34
+ camelCase: 'camel_case',
35
+ PascalCase: 'pascal_case',
36
+ snake_case: 'snake_case',
37
+ 'kebab-case': 'kebab_case',
38
+ UPPERCASE: 'uppercase',
39
+ };
40
+ }
41
+
42
+ const allProperties = Object.keys(defaultMapping()) as Array<keyof IRepoTest>;
43
+
44
+ class AbstractRepositoryIntegrationTest extends AbstractRepository<IRepoTest, RepoTest> {}
45
+
46
+ describe('AbstractRepository', () => {
47
+ let db: ConnectionManager<{ test: AbstractRepositoryIntegrationTest }>;
48
+ beforeAll(async () => {
49
+ db = new ConnectionManager(
50
+ 'AbstractRepositoryIntegrationTest',
51
+ process.env.COMPONENT_DB_CONNECTION_STRING!,
52
+ '',
53
+ [],
54
+ (db) => ({
55
+ test: new AbstractRepositoryIntegrationTest(
56
+ db.repositories,
57
+ db.pool,
58
+ 'abstract_repository_db_test',
59
+ defaultMapping(),
60
+ RepoTest,
61
+ ),
62
+ }),
63
+ );
64
+ await db.pool.query(`
65
+ CREATE TABLE IF NOT EXISTS abstract_repository_db_test (
66
+ lowercase text,
67
+ camel_case text,
68
+ pascal_case text,
69
+ snake_case text,
70
+ kebab_case text,
71
+ uppercase text
72
+ );
73
+ `);
74
+ });
75
+
76
+ afterAll(async () => {
77
+ await db.pool.query('DROP TABLE IF EXISTS abstract_repository_db_test;');
78
+ await db.close();
79
+ });
80
+
81
+ describe('properties are always mapped to columns', () => {
82
+ beforeEach(async () => {
83
+ await db.pool.query('DELETE FROM abstract_repository_db_test;');
84
+ });
85
+ it('create', async () => {
86
+ const repo = db.repositories.test;
87
+ const data = new RepoTest();
88
+ const result = await repo.create(data);
89
+ expect(result).toEqual(data);
90
+ });
91
+
92
+ it.each(allProperties)('update %s', async (property) => {
93
+ const repo = db.repositories.test;
94
+ const data = await repo.create(new RepoTest());
95
+ const result = await repo.update({ [property]: data[property] }, { [property]: `new ${data[property]}` });
96
+ expect(result).toEqual([{ ...data, [property]: `new ${data[property]}` }]);
97
+ });
98
+
99
+ it.each(allProperties)('delete %s', async (property) => {
100
+ const repo = db.repositories.test;
101
+ const data = await repo.create(new RepoTest());
102
+ const result = await repo.delete({ [property]: data[property] });
103
+ expect(result).toEqual(1);
104
+ });
105
+
106
+ it.each(allProperties)('findOne %s', async (property) => {
107
+ const repo = db.repositories.test;
108
+ const findData = await repo.create(new RepoTest({ [property]: 'some value' }));
109
+ const _differentData = await repo.create(new RepoTest({ [property]: 'some other value' }));
110
+
111
+ const result = await repo.findOne({ [property]: findData[property] });
112
+ expect(result).toEqual(findData);
113
+ });
114
+
115
+ it.each(allProperties)('find %s', async (property) => {
116
+ const repo = db.repositories.test;
117
+ const findData = await repo.create(new RepoTest({ [property]: 'some value' }));
118
+ const _differentData = await repo.create(new RepoTest({ [property]: 'some other value' }));
119
+
120
+ const result = await repo.find({ [property]: findData[property] });
121
+ expect(result).toEqual([findData]);
122
+ });
123
+
124
+ it.each(allProperties)('findAll %s', async (property) => {
125
+ const repo = db.repositories.test;
126
+ const findData = await repo.create(new RepoTest({ [property]: 'some value' }));
127
+ const differentData = await repo.create(new RepoTest({ [property]: 'some other value' }));
128
+
129
+ const result = await repo.findAll();
130
+ expect(result).toEqual([findData, differentData]);
131
+ });
132
+
133
+ it.each(allProperties)('getPage %s', async (property) => {
134
+ const repo = db.repositories.test;
135
+ const firstData = await repo.create(new RepoTest({ [property]: 'aaaaa' }));
136
+ const secondData = await repo.create(new RepoTest({ [property]: 'bbbb' }));
137
+
138
+ const ascResult = await repo.getPage(1, [property], 'asc');
139
+ expect(ascResult).toMatchObject({ items: [firstData, secondData], totalCount: 2 });
140
+ const descResult = await repo.getPage(1, [property], 'desc');
141
+ expect(descResult).toMatchObject({ items: [secondData, firstData], totalCount: 2 });
142
+ });
143
+
144
+ it.each(allProperties)('getPageRaw %s', async (property) => {
145
+ const repo = db.repositories.test;
146
+ const firstData = await repo.create(new RepoTest({ [property]: 'aaaaa' }));
147
+ const secondData = await repo.create(new RepoTest({ [property]: 'bbbb' }));
148
+
149
+ const ascResult = await repo.getPageRaw(1, [property], 'asc');
150
+ expect(ascResult).toMatchObject({ items: [firstData, secondData], totalCount: 2 });
151
+ const descResult = await repo.getPageRaw(1, [property], 'desc');
152
+ expect(descResult).toMatchObject({ items: [secondData, firstData], totalCount: 2 });
153
+ });
154
+ });
155
+ });
@@ -23,11 +23,13 @@ export type PageResult<T> = {
23
23
  export type SortDirection = 'desc' | 'asc';
24
24
  export const DEFAULT_PAGE_SIZE = 20;
25
25
 
26
- export abstract class AbstractRepository<T extends object, ObjT extends T> implements Reader<T>, Writer<T> {
26
+ export abstract class AbstractRepository<SHAPE extends object, DATA_CLASS extends SHAPE>
27
+ implements Reader<SHAPE>, Writer<SHAPE>
28
+ {
27
29
  protected tableName: string;
28
30
 
29
31
  /** object where the key is the model property name amd the value is sql column name */
30
- protected modelPropertyToSqlColumn: { [key in keyof T]: string };
32
+ protected modelPropertyToSqlColumn: { [key in keyof SHAPE]: string };
31
33
  /** object where the key is the sql column name and the value is the model property name */
32
34
  protected sqlColumnToModelProperty: { [key: string]: string };
33
35
 
@@ -35,8 +37,8 @@ export abstract class AbstractRepository<T extends object, ObjT extends T> imple
35
37
  protected repositories: Repositories,
36
38
  protected pool: Pool,
37
39
  tableName: string,
38
- mapping: { [key in keyof T]: string },
39
- protected classRef: { new (data?: Record<string, unknown>): ObjT },
40
+ mapping: { [key in keyof SHAPE]: string },
41
+ protected classRef: { new (data?: Record<string, unknown>): DATA_CLASS },
40
42
  ) {
41
43
  this.tableName = `"${tableName}"`;
42
44
 
@@ -53,9 +55,9 @@ export abstract class AbstractRepository<T extends object, ObjT extends T> imple
53
55
  return await this.pool.connect();
54
56
  }
55
57
 
56
- async create(value: ObjT, transactionClient: PoolClient | null = null): Promise<T> {
58
+ async create(value: DATA_CLASS, transactionClient: PoolClient | null = null): Promise<SHAPE> {
57
59
  const columns = Object.keys(value)
58
- .map((a) => `"${this.modelPropertyToSqlColumn[a as keyof T]}"`)
60
+ .map((a) => `"${this.modelPropertyToSqlColumn[a as keyof SHAPE]}"`)
59
61
  .join(', ');
60
62
 
61
63
  const values = Object.values(value)
@@ -71,12 +73,16 @@ export abstract class AbstractRepository<T extends object, ObjT extends T> imple
71
73
  return result[0];
72
74
  }
73
75
 
74
- async update(where: Partial<T>, newValue: Partial<T>, transactionClient: PoolClient | null = null): Promise<T[]> {
76
+ async update(
77
+ where: Partial<SHAPE>,
78
+ newValue: Partial<SHAPE>,
79
+ transactionClient: PoolClient | null = null,
80
+ ): Promise<SHAPE[]> {
75
81
  const whereValues = Object.values(where);
76
82
  const newValues = Object.values(newValue);
77
83
 
78
84
  const setString = Object.keys(newValue)
79
- .map((a, index) => `"${this.modelPropertyToSqlColumn[a as keyof T]}" = $${index + 1}`)
85
+ .map((a, index) => `"${this.modelPropertyToSqlColumn[a as keyof SHAPE]}" = $${index + 1}`)
80
86
  .join(', ');
81
87
 
82
88
  const whereString = this.createWhereStringFromPartialModel(where, newValues.length);
@@ -93,7 +99,7 @@ export abstract class AbstractRepository<T extends object, ObjT extends T> imple
93
99
  return result;
94
100
  }
95
101
 
96
- async delete(where: Partial<T>, transactionClient: PoolClient | null = null): Promise<number> {
102
+ async delete(where: Partial<SHAPE>, transactionClient: PoolClient | null = null): Promise<number> {
97
103
  const client = transactionClient ?? (await this.getConnection());
98
104
 
99
105
  try {
@@ -109,7 +115,7 @@ export abstract class AbstractRepository<T extends object, ObjT extends T> imple
109
115
  }
110
116
  }
111
117
 
112
- protected createWhereStringFromPartialModel(values: Partial<T>, initialIndex: number = 0) {
118
+ protected createWhereStringFromPartialModel(values: Partial<SHAPE>, initialIndex: number = 0) {
113
119
  const keys = Object.keys(values);
114
120
 
115
121
  if (keys.length == 0) {
@@ -117,7 +123,7 @@ export abstract class AbstractRepository<T extends object, ObjT extends T> imple
117
123
  }
118
124
 
119
125
  const sql = keys.reduce((acc, key, index) => {
120
- const condition = `"${this.modelPropertyToSqlColumn[key as keyof T]}" = $${1 + index + initialIndex}`;
126
+ const condition = `"${this.modelPropertyToSqlColumn[key as keyof SHAPE]}" = $${1 + index + initialIndex}`;
121
127
 
122
128
  return acc === '' ? `${acc} ${condition}` : `${acc} AND ${condition}`;
123
129
  }, '');
@@ -146,12 +152,12 @@ export abstract class AbstractRepository<T extends object, ObjT extends T> imple
146
152
  query: string,
147
153
  values: any[],
148
154
  transactionClient: PoolClient | null = null,
149
- ): Promise<T[]> {
155
+ ): Promise<SHAPE[]> {
150
156
  const rows = await this.executeQueryRaw(query, values, transactionClient);
151
157
  return rows.map((a) => this.createAndHydrateModel(a));
152
158
  }
153
159
 
154
- protected createAndHydrateModel(row: any): T {
160
+ protected createAndHydrateModel(row: any): SHAPE {
155
161
  const inputData: Record<string, unknown> = {};
156
162
 
157
163
  for (const key of Object.keys(row)) {
@@ -162,7 +168,7 @@ export abstract class AbstractRepository<T extends object, ObjT extends T> imple
162
168
  return new this.classRef(inputData);
163
169
  }
164
170
 
165
- async findOne(item: Partial<T>): Promise<T | undefined> {
171
+ async findOne(item: Partial<SHAPE>): Promise<SHAPE | undefined> {
166
172
  const result = await this.executeQuery(
167
173
  `SELECT *
168
174
  FROM ${this.tableName}
@@ -174,7 +180,7 @@ export abstract class AbstractRepository<T extends object, ObjT extends T> imple
174
180
  return result[0];
175
181
  }
176
182
 
177
- async find(item: Partial<T>): Promise<T[]> {
183
+ async find(item: Partial<SHAPE>): Promise<SHAPE[]> {
178
184
  const result = await this.executeQuery(
179
185
  `SELECT *
180
186
  FROM ${this.tableName}
@@ -185,7 +191,7 @@ export abstract class AbstractRepository<T extends object, ObjT extends T> imple
185
191
  return result;
186
192
  }
187
193
 
188
- async findAll(): Promise<T[]> {
194
+ async findAll(): Promise<SHAPE[]> {
189
195
  const result = await this.executeQuery(
190
196
  `SELECT *
191
197
  FROM ${this.tableName}`,
@@ -195,7 +201,7 @@ export abstract class AbstractRepository<T extends object, ObjT extends T> imple
195
201
  return result;
196
202
  }
197
203
 
198
- async getCount(item: Partial<T> | null = null): Promise<number> {
204
+ async getCount(item: Partial<SHAPE> | null = null): Promise<number> {
199
205
  let whereClause = '';
200
206
 
201
207
  if (item) {
@@ -207,46 +213,25 @@ export abstract class AbstractRepository<T extends object, ObjT extends T> imple
207
213
 
208
214
  async getPage(
209
215
  pageNumber: number,
210
- sortBy: (keyof T)[] = [],
216
+ sortBy: (keyof SHAPE)[] = [],
211
217
  direction: SortDirection = 'asc',
212
218
  pageSize: number | null = null,
213
- item: Partial<T> | null = null,
214
- ): Promise<PageResult<T>> {
215
- if (pageSize === null) {
216
- pageSize = DEFAULT_PAGE_SIZE;
217
- }
218
- if (pageNumber <= 0) {
219
- throw new Error(`Page number value cannot be less than 1`);
220
- }
221
- if (pageSize <= 0) {
222
- throw new Error(`Page size value cannot be less than 1`);
223
- }
219
+ item: Partial<SHAPE> | null = null,
220
+ ): Promise<PageResult<SHAPE>> {
224
221
  let whereClause = '';
225
222
  if (item) {
226
223
  whereClause = `WHERE ${this.createWhereStringFromPartialModel(item)}`;
227
224
  }
228
225
 
229
- let orderByClause = '';
230
- if (sortBy.length) {
231
- orderByClause = `ORDER BY ${sortBy
232
- .map((a) => this.modelPropertyToSqlColumn[a as keyof T])
233
- .join(',')} ${direction}`;
234
- }
235
- const offset = (pageNumber - 1) * pageSize;
236
-
237
- const items = await this.executeQuery(
238
- `SELECT *
239
- FROM ${this.tableName} ${whereClause} ${orderByClause}
240
- OFFSET ${offset}
241
- LIMIT ${pageSize}`,
242
- item ? Object.values(item) : [],
243
- );
244
-
245
- return {
246
- items,
247
- totalCount: await this.getCount(item),
226
+ return this.getPageRaw(
227
+ pageNumber,
228
+ sortBy,
229
+ direction,
230
+ whereClause,
231
+ this.tableName,
232
+ Object.values(item ?? {}),
248
233
  pageSize,
249
- };
234
+ );
250
235
  }
251
236
 
252
237
  async getCountRaw(whereClause: string = '', values: any[] = [], tableRef: string = ''): Promise<number> {
@@ -260,13 +245,13 @@ export abstract class AbstractRepository<T extends object, ObjT extends T> imple
260
245
 
261
246
  async getPageRaw(
262
247
  pageNumber: number,
263
- sortBy: (keyof T)[] = [],
248
+ sortBy: (keyof SHAPE)[] = [],
264
249
  direction: SortDirection = 'asc',
265
250
  whereClause: string = '',
266
251
  tableRef: string = '',
267
252
  values: any[] = [],
268
253
  pageSize: number | null = null,
269
- ): Promise<PageResult<T>> {
254
+ ): Promise<PageResult<SHAPE>> {
270
255
  if (pageSize === null) {
271
256
  pageSize = DEFAULT_PAGE_SIZE;
272
257
  }
@@ -279,7 +264,9 @@ export abstract class AbstractRepository<T extends object, ObjT extends T> imple
279
264
 
280
265
  let orderByClause = '';
281
266
  if (sortBy.length) {
282
- orderByClause = `ORDER BY ${sortBy.join(',')} ${direction}`;
267
+ orderByClause = `ORDER BY ${sortBy
268
+ .map((a) => this.modelPropertyToSqlColumn[a as keyof SHAPE])
269
+ .join(',')} ${direction}`;
283
270
  }
284
271
 
285
272
  const offset = (pageNumber - 1) * pageSize;
package/test.env ADDED
@@ -0,0 +1 @@
1
+ COMPONENT_DB_CONNECTION_STRING=postgresql://root:root@localhost:5432/cmp_db?schema=public