@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/.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
|