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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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