@squiz/db-lib 1.12.0-alpha.9 → 1.12.1-alpha.0
Sign up to get free protection for your applications and to get access to all the features.
- package/.npm/_logs/2023-02-27T04_50_30_941Z-debug-0.log +39 -0
- package/jest.config.ts +7 -4
- package/lib/AbstractRepository.d.ts +17 -17
- package/lib/AbstractRepository.integration.spec.d.ts +1 -0
- package/lib/index.js +9 -26
- package/lib/index.js.map +2 -2
- package/package.json +6 -5
- package/src/AbstractRepository.integration.spec.ts +155 -0
- package/src/AbstractRepository.ts +39 -52
- package/test.env +1 -0
- package/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
{
|
2
2
|
"name": "@squiz/db-lib",
|
3
|
-
"version": "1.12.
|
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": "
|
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.
|
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": "
|
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<
|
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
|
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
|
39
|
-
protected classRef: { new (data?: Record<string, unknown>):
|
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:
|
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
|
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(
|
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
|
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<
|
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<
|
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
|
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<
|
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):
|
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<
|
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<
|
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<
|
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<
|
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
|
216
|
+
sortBy: (keyof SHAPE)[] = [],
|
211
217
|
direction: SortDirection = 'asc',
|
212
218
|
pageSize: number | null = null,
|
213
|
-
item: Partial<
|
214
|
-
): Promise<PageResult<
|
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
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
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
|
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<
|
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
|
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
|