@takaro/db 0.0.9 → 0.0.11
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/dist/knex.d.ts +1 -0
- package/dist/knex.js +12 -10
- package/dist/knex.js.map +1 -1
- package/dist/migrations/sql/20240906125101-gameserver-enable.d.ts +3 -0
- package/dist/migrations/sql/20240906125101-gameserver-enable.js +11 -0
- package/dist/migrations/sql/20240906125101-gameserver-enable.js.map +1 -0
- package/dist/migrations/sql/20240908185212-domain-name-not-unique.d.ts +3 -0
- package/dist/migrations/sql/20240908185212-domain-name-not-unique.js +9 -0
- package/dist/migrations/sql/20240908185212-domain-name-not-unique.js.map +1 -0
- package/dist/queryBuilder.d.ts +13 -6
- package/dist/queryBuilder.js +53 -7
- package/dist/queryBuilder.js.map +1 -1
- package/package.json +1 -2
- package/src/__tests__/queryBuilder.integration.test.ts +3 -1
- package/src/knex.ts +12 -9
- package/src/migrations/sql/20240906125101-gameserver-enable.ts +14 -0
- package/src/migrations/sql/20240908185212-domain-name-not-unique.ts +12 -0
- package/src/queryBuilder.ts +65 -8
package/dist/knex.d.ts
CHANGED
|
@@ -15,5 +15,6 @@ export declare function getKnexOptions(extra?: Record<string, unknown>): {
|
|
|
15
15
|
};
|
|
16
16
|
};
|
|
17
17
|
export declare function getKnex(): Promise<KnexClient>;
|
|
18
|
+
export declare function isDbAvailable(): Promise<boolean>;
|
|
18
19
|
export declare function disconnectKnex(): Promise<void>;
|
|
19
20
|
export {};
|
package/dist/knex.js
CHANGED
|
@@ -25,18 +25,20 @@ export async function getKnex() {
|
|
|
25
25
|
log.debug('Missed knex cache, creating new client');
|
|
26
26
|
const knex = createKnex(getKnexOptions());
|
|
27
27
|
cachedKnex = knex;
|
|
28
|
-
health.registerHook('db',
|
|
29
|
-
try {
|
|
30
|
-
await knex.raw('SELECT 1');
|
|
31
|
-
return true;
|
|
32
|
-
}
|
|
33
|
-
catch (error) {
|
|
34
|
-
log.error(error);
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
});
|
|
28
|
+
health.registerHook('db', isDbAvailable);
|
|
38
29
|
return cachedKnex;
|
|
39
30
|
}
|
|
31
|
+
export async function isDbAvailable() {
|
|
32
|
+
try {
|
|
33
|
+
const knex = await getKnex();
|
|
34
|
+
await knex.raw('SELECT 1');
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
log.error(error);
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
40
42
|
export async function disconnectKnex() {
|
|
41
43
|
if (!cachedKnex)
|
|
42
44
|
return;
|
package/dist/knex.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"knex.js","sourceRoot":"","sources":["../src/knex.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,MAAM,CAAC;AAC3B,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAG9C,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;AAE1B,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC;AAIrC,MAAM,UAAU,cAAc,CAAC,QAAiC,EAAE;IAChE,MAAM,IAAI,GAAG;QACX,MAAM,EAAE,IAAI;QACZ,UAAU,EAAE;YACV,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,eAAe,CAAC;YACjC,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,eAAe,CAAC;YACjC,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,eAAe,CAAC;YACjC,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,mBAAmB,CAAC;YACzC,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,mBAAmB,CAAC;YACzC,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK;SAC5E;QACD,GAAG,KAAK;KACT,CAAC;IACF,OAAO,IAAI,CAAC;AACd,CAAC;AAED,IAAI,UAAU,GAAsB,IAAI,CAAC;AAEzC,MAAM,CAAC,KAAK,UAAU,OAAO;IAC3B,IAAI,UAAU;QAAE,OAAO,UAAU,CAAC;IAElC,GAAG,CAAC,KAAK,CAAC,wCAAwC,CAAC,CAAC;IACpD,MAAM,IAAI,GAAG,UAAU,CAAC,cAAc,EAAE,CAAC,CAAC;IAC1C,UAAU,GAAG,IAAI,CAAC;IAElB,MAAM,CAAC,YAAY,CAAC,IAAI,EAAE,KAAK,IAAI,
|
|
1
|
+
{"version":3,"file":"knex.js","sourceRoot":"","sources":["../src/knex.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,MAAM,CAAC;AAC3B,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAG9C,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;AAE1B,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC;AAIrC,MAAM,UAAU,cAAc,CAAC,QAAiC,EAAE;IAChE,MAAM,IAAI,GAAG;QACX,MAAM,EAAE,IAAI;QACZ,UAAU,EAAE;YACV,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,eAAe,CAAC;YACjC,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,eAAe,CAAC;YACjC,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,eAAe,CAAC;YACjC,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,mBAAmB,CAAC;YACzC,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,mBAAmB,CAAC;YACzC,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK;SAC5E;QACD,GAAG,KAAK;KACT,CAAC;IACF,OAAO,IAAI,CAAC;AACd,CAAC;AAED,IAAI,UAAU,GAAsB,IAAI,CAAC;AAEzC,MAAM,CAAC,KAAK,UAAU,OAAO;IAC3B,IAAI,UAAU;QAAE,OAAO,UAAU,CAAC;IAElC,GAAG,CAAC,KAAK,CAAC,wCAAwC,CAAC,CAAC;IACpD,MAAM,IAAI,GAAG,UAAU,CAAC,cAAc,EAAE,CAAC,CAAC;IAC1C,UAAU,GAAG,IAAI,CAAC;IAElB,MAAM,CAAC,YAAY,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC;IAEzC,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa;IACjC,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,OAAO,EAAE,CAAC;QAC7B,MAAM,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QAC3B,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACjB,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc;IAClC,IAAI,CAAC,UAAU;QAAE,OAAO;IACxB,MAAM,UAAU,CAAC,OAAO,EAAE,CAAC;IAC3B,UAAU,GAAG,IAAI,CAAC;IAClB,GAAG,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;AAChC,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export async function up(knex) {
|
|
2
|
+
await knex.schema.alterTable('gameservers', (table) => {
|
|
3
|
+
table.boolean('enabled').defaultTo(true);
|
|
4
|
+
});
|
|
5
|
+
}
|
|
6
|
+
export async function down(knex) {
|
|
7
|
+
await knex.schema.alterTable('gameservers', (table) => {
|
|
8
|
+
table.dropColumn('enabled');
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=20240906125101-gameserver-enable.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"20240906125101-gameserver-enable.js","sourceRoot":"","sources":["../../../src/migrations/sql/20240906125101-gameserver-enable.ts"],"names":[],"mappings":"AAGA,MAAM,CAAC,KAAK,UAAU,EAAE,CAAC,IAAU;IACjC,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,aAAa,EAAE,CAAC,KAAK,EAAE,EAAE;QACpD,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,IAAU;IACnC,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,aAAa,EAAE,CAAC,KAAK,EAAE,EAAE;QACpD,KAAK,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;IAC9B,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export async function up(knex) {
|
|
2
|
+
await knex.schema.alterTable('domains', (table) => {
|
|
3
|
+
table.dropUnique(['name'], 'domains_name_unique');
|
|
4
|
+
});
|
|
5
|
+
}
|
|
6
|
+
export async function down(knex) {
|
|
7
|
+
// None, this is a one-way migration
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=20240908185212-domain-name-not-unique.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"20240908185212-domain-name-not-unique.js","sourceRoot":"","sources":["../../../src/migrations/sql/20240908185212-domain-name-not-unique.ts"],"names":[],"mappings":"AAGA,MAAM,CAAC,KAAK,UAAU,EAAE,CAAC,IAAU;IACjC,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,SAAS,EAAE,CAAC,KAAK,EAAE,EAAE;QAChD,KAAK,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,EAAE,qBAAqB,CAAC,CAAC;IACpD,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,IAAU;IACnC,oCAAoC;AACtC,CAAC"}
|
package/dist/queryBuilder.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { QueryBuilder as ObjectionQueryBuilder, Model as ObjectionModel, Page } from 'objection';
|
|
2
2
|
export declare class ITakaroQuery<T> {
|
|
3
3
|
filters?: {
|
|
4
|
-
[key
|
|
4
|
+
[key: string]: unknown[] | string[] | boolean | null | undefined;
|
|
5
5
|
};
|
|
6
6
|
search?: {
|
|
7
|
-
[key
|
|
7
|
+
[key: string]: unknown[] | string[] | boolean | null | undefined;
|
|
8
8
|
};
|
|
9
9
|
greaterThan?: {
|
|
10
10
|
[key in keyof T]?: unknown;
|
|
@@ -14,7 +14,7 @@ export declare class ITakaroQuery<T> {
|
|
|
14
14
|
};
|
|
15
15
|
page?: number;
|
|
16
16
|
limit?: number;
|
|
17
|
-
sortBy?:
|
|
17
|
+
sortBy?: string;
|
|
18
18
|
sortDirection?: SortDirection;
|
|
19
19
|
extend?: string[];
|
|
20
20
|
}
|
|
@@ -22,10 +22,17 @@ export declare enum SortDirection {
|
|
|
22
22
|
asc = "asc",
|
|
23
23
|
desc = "desc"
|
|
24
24
|
}
|
|
25
|
+
export declare function populateModelColumns(): Promise<void>;
|
|
25
26
|
export declare class QueryBuilder<Model extends ObjectionModel, OutputDTO> {
|
|
26
|
-
private
|
|
27
|
-
constructor(
|
|
28
|
-
|
|
27
|
+
private query;
|
|
28
|
+
constructor(rawQuery?: ITakaroQuery<OutputDTO>);
|
|
29
|
+
/**
|
|
30
|
+
* Our custom query builder can only handle columns that exist in the table
|
|
31
|
+
* However, we might want to do complex/relational logic in higher layers
|
|
32
|
+
* To make sure we don't error out here, we filter out any columns that don't exist in the current table
|
|
33
|
+
*/
|
|
34
|
+
private cleanQueryObject;
|
|
35
|
+
build(queryBuilder: ObjectionQueryBuilder<Model, Model[]>): ObjectionQueryBuilder<Model, Page<Model>>;
|
|
29
36
|
private filters;
|
|
30
37
|
private greaterThan;
|
|
31
38
|
private lessThan;
|
package/dist/queryBuilder.js
CHANGED
|
@@ -8,6 +8,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
8
8
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
9
|
};
|
|
10
10
|
import { IsEnum, IsNumber, IsOptional, IsString } from 'class-validator';
|
|
11
|
+
import { getKnex, isDbAvailable } from './knex.js';
|
|
11
12
|
export class ITakaroQuery {
|
|
12
13
|
}
|
|
13
14
|
__decorate([
|
|
@@ -39,7 +40,7 @@ __decorate([
|
|
|
39
40
|
__decorate([
|
|
40
41
|
IsOptional(),
|
|
41
42
|
IsString(),
|
|
42
|
-
__metadata("design:type",
|
|
43
|
+
__metadata("design:type", String)
|
|
43
44
|
], ITakaroQuery.prototype, "sortBy", void 0);
|
|
44
45
|
__decorate([
|
|
45
46
|
IsOptional(),
|
|
@@ -57,15 +58,60 @@ export var SortDirection;
|
|
|
57
58
|
SortDirection["asc"] = "asc";
|
|
58
59
|
SortDirection["desc"] = "desc";
|
|
59
60
|
})(SortDirection || (SortDirection = {}));
|
|
61
|
+
const modelColumns = new Map();
|
|
62
|
+
export async function populateModelColumns() {
|
|
63
|
+
if (await isDbAvailable()) {
|
|
64
|
+
const knex = await getKnex();
|
|
65
|
+
// Find all tables
|
|
66
|
+
const tables = await knex.raw(
|
|
67
|
+
// eslint-disable-next-line
|
|
68
|
+
"SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE'");
|
|
69
|
+
// For each table, store the columns
|
|
70
|
+
for (const table of tables.rows) {
|
|
71
|
+
const tableName = table.table_name;
|
|
72
|
+
const columns = await knex(tableName).columnInfo();
|
|
73
|
+
modelColumns.set(tableName, Object.keys(columns));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
await populateModelColumns();
|
|
60
78
|
export class QueryBuilder {
|
|
61
|
-
constructor(
|
|
62
|
-
this.query =
|
|
79
|
+
constructor(rawQuery = new ITakaroQuery()) {
|
|
80
|
+
this.query = JSON.parse(JSON.stringify(rawQuery));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Our custom query builder can only handle columns that exist in the table
|
|
84
|
+
* However, we might want to do complex/relational logic in higher layers
|
|
85
|
+
* To make sure we don't error out here, we filter out any columns that don't exist in the current table
|
|
86
|
+
*/
|
|
87
|
+
cleanQueryObject(tableName) {
|
|
88
|
+
const columns = modelColumns.get(tableName) ?? [];
|
|
89
|
+
for (const key in this.query.filters) {
|
|
90
|
+
if (Object.prototype.hasOwnProperty.call(this.query.filters, key)) {
|
|
91
|
+
if (!columns.includes(key)) {
|
|
92
|
+
if (this.query.filters) {
|
|
93
|
+
this.query.filters[key] = [];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
for (const key in this.query.search) {
|
|
99
|
+
if (Object.prototype.hasOwnProperty.call(this.query.search, key)) {
|
|
100
|
+
if (!columns.includes(key)) {
|
|
101
|
+
if (this.query.search) {
|
|
102
|
+
this.query.search[key] = [];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return this.query;
|
|
63
108
|
}
|
|
64
|
-
build(
|
|
65
|
-
const tableName =
|
|
109
|
+
build(queryBuilder) {
|
|
110
|
+
const tableName = queryBuilder.modelClass().tableName;
|
|
111
|
+
this.cleanQueryObject(tableName);
|
|
66
112
|
const pagination = this.pagination();
|
|
67
113
|
const sorting = this.sorting();
|
|
68
|
-
let qry =
|
|
114
|
+
let qry = queryBuilder.page(pagination.page, pagination.limit).orderBy(sorting.sortBy, sorting.sortDirection);
|
|
69
115
|
qry = this.filters(tableName, qry);
|
|
70
116
|
qry = this.greaterThan(tableName, qry);
|
|
71
117
|
qry = this.lessThan(tableName, qry);
|
|
@@ -95,7 +141,7 @@ export class QueryBuilder {
|
|
|
95
141
|
if (Object.prototype.hasOwnProperty.call(this.query.filters, filter)) {
|
|
96
142
|
const searchVal = this.query.filters[filter];
|
|
97
143
|
if (searchVal && Array.isArray(searchVal)) {
|
|
98
|
-
if (searchVal.includes(
|
|
144
|
+
if (searchVal.includes('null')) {
|
|
99
145
|
query.whereNull(`${tableName}.${filter}`);
|
|
100
146
|
continue;
|
|
101
147
|
}
|
package/dist/queryBuilder.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"queryBuilder.js","sourceRoot":"","sources":["../src/queryBuilder.ts"],"names":[],"mappings":";;;;;;;;;AAAA,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"queryBuilder.js","sourceRoot":"","sources":["../src/queryBuilder.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@takaro/db",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.11",
|
|
4
4
|
"description": "An opinionated data layer",
|
|
5
5
|
"main": "dist/main.js",
|
|
6
6
|
"types": "dist/main.d.ts",
|
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
"license": "ISC",
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"convict": "^6.2.3",
|
|
21
|
-
"knex": "^3.0.0",
|
|
22
21
|
"objection": "^3.0.1",
|
|
23
22
|
"pg": "^8.8.0"
|
|
24
23
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { disconnectKnex, getKnex } from '../knex.js';
|
|
2
|
-
import { QueryBuilder, SortDirection } from '../queryBuilder.js';
|
|
2
|
+
import { populateModelColumns, QueryBuilder, SortDirection } from '../queryBuilder.js';
|
|
3
3
|
import { expect } from '@takaro/test';
|
|
4
4
|
import { TakaroModel } from '../TakaroModel.js';
|
|
5
5
|
import { Model } from 'objection';
|
|
@@ -60,6 +60,8 @@ describe('QueryBuilder', () => {
|
|
|
60
60
|
|
|
61
61
|
TestUserModel.knex(knex);
|
|
62
62
|
TestPostModel.knex(knex);
|
|
63
|
+
|
|
64
|
+
await populateModelColumns();
|
|
63
65
|
});
|
|
64
66
|
|
|
65
67
|
afterEach(async () => {
|
package/src/knex.ts
CHANGED
|
@@ -34,19 +34,22 @@ export async function getKnex(): Promise<KnexClient> {
|
|
|
34
34
|
const knex = createKnex(getKnexOptions());
|
|
35
35
|
cachedKnex = knex;
|
|
36
36
|
|
|
37
|
-
health.registerHook('db',
|
|
38
|
-
try {
|
|
39
|
-
await knex.raw('SELECT 1');
|
|
40
|
-
return true;
|
|
41
|
-
} catch (error) {
|
|
42
|
-
log.error(error);
|
|
43
|
-
return false;
|
|
44
|
-
}
|
|
45
|
-
});
|
|
37
|
+
health.registerHook('db', isDbAvailable);
|
|
46
38
|
|
|
47
39
|
return cachedKnex;
|
|
48
40
|
}
|
|
49
41
|
|
|
42
|
+
export async function isDbAvailable(): Promise<boolean> {
|
|
43
|
+
try {
|
|
44
|
+
const knex = await getKnex();
|
|
45
|
+
await knex.raw('SELECT 1');
|
|
46
|
+
return true;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
log.error(error);
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
50
53
|
export async function disconnectKnex(): Promise<void> {
|
|
51
54
|
if (!cachedKnex) return;
|
|
52
55
|
await cachedKnex.destroy();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import ms from 'ms';
|
|
2
|
+
import { Knex } from 'knex';
|
|
3
|
+
|
|
4
|
+
export async function up(knex: Knex): Promise<void> {
|
|
5
|
+
await knex.schema.alterTable('gameservers', (table) => {
|
|
6
|
+
table.boolean('enabled').defaultTo(true);
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function down(knex: Knex): Promise<void> {
|
|
11
|
+
await knex.schema.alterTable('gameservers', (table) => {
|
|
12
|
+
table.dropColumn('enabled');
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import ms from 'ms';
|
|
2
|
+
import { Knex } from 'knex';
|
|
3
|
+
|
|
4
|
+
export async function up(knex: Knex): Promise<void> {
|
|
5
|
+
await knex.schema.alterTable('domains', (table) => {
|
|
6
|
+
table.dropUnique(['name'], 'domains_name_unique');
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function down(knex: Knex): Promise<void> {
|
|
11
|
+
// None, this is a one-way migration
|
|
12
|
+
}
|
package/src/queryBuilder.ts
CHANGED
|
@@ -7,16 +7,17 @@ import {
|
|
|
7
7
|
Expression,
|
|
8
8
|
PrimitiveValue,
|
|
9
9
|
} from 'objection';
|
|
10
|
+
import { getKnex, isDbAvailable } from './knex.js';
|
|
10
11
|
|
|
11
12
|
export class ITakaroQuery<T> {
|
|
12
13
|
@IsOptional()
|
|
13
14
|
filters?: {
|
|
14
|
-
[key
|
|
15
|
+
[key: string]: unknown[] | string[] | boolean | null | undefined;
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
@IsOptional()
|
|
18
19
|
search?: {
|
|
19
|
-
[key
|
|
20
|
+
[key: string]: unknown[] | string[] | boolean | null | undefined;
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
@IsOptional()
|
|
@@ -39,7 +40,7 @@ export class ITakaroQuery<T> {
|
|
|
39
40
|
|
|
40
41
|
@IsOptional()
|
|
41
42
|
@IsString()
|
|
42
|
-
sortBy?:
|
|
43
|
+
sortBy?: string;
|
|
43
44
|
|
|
44
45
|
@IsOptional()
|
|
45
46
|
@IsString()
|
|
@@ -56,16 +57,72 @@ export enum SortDirection {
|
|
|
56
57
|
desc = 'desc',
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
const modelColumns = new Map<string, string[]>();
|
|
61
|
+
|
|
62
|
+
export async function populateModelColumns() {
|
|
63
|
+
if (await isDbAvailable()) {
|
|
64
|
+
const knex = await getKnex();
|
|
65
|
+
// Find all tables
|
|
66
|
+
const tables = await knex.raw(
|
|
67
|
+
// eslint-disable-next-line
|
|
68
|
+
"SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE'",
|
|
69
|
+
);
|
|
70
|
+
// For each table, store the columns
|
|
71
|
+
for (const table of tables.rows) {
|
|
72
|
+
const tableName = table.table_name;
|
|
73
|
+
const columns = await knex(tableName).columnInfo();
|
|
74
|
+
modelColumns.set(tableName, Object.keys(columns));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
await populateModelColumns();
|
|
80
|
+
|
|
59
81
|
export class QueryBuilder<Model extends ObjectionModel, OutputDTO> {
|
|
60
|
-
|
|
82
|
+
private query: ITakaroQuery<OutputDTO>;
|
|
83
|
+
constructor(rawQuery: ITakaroQuery<OutputDTO> = new ITakaroQuery()) {
|
|
84
|
+
this.query = JSON.parse(JSON.stringify(rawQuery));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Our custom query builder can only handle columns that exist in the table
|
|
89
|
+
* However, we might want to do complex/relational logic in higher layers
|
|
90
|
+
* To make sure we don't error out here, we filter out any columns that don't exist in the current table
|
|
91
|
+
*/
|
|
92
|
+
private cleanQueryObject(tableName: string) {
|
|
93
|
+
const columns = modelColumns.get(tableName) ?? [];
|
|
94
|
+
|
|
95
|
+
for (const key in this.query.filters) {
|
|
96
|
+
if (Object.prototype.hasOwnProperty.call(this.query.filters, key)) {
|
|
97
|
+
if (!columns.includes(key)) {
|
|
98
|
+
if (this.query.filters) {
|
|
99
|
+
this.query.filters[key] = [];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const key in this.query.search) {
|
|
106
|
+
if (Object.prototype.hasOwnProperty.call(this.query.search, key)) {
|
|
107
|
+
if (!columns.includes(key)) {
|
|
108
|
+
if (this.query.search) {
|
|
109
|
+
this.query.search[key] = [];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return this.query;
|
|
116
|
+
}
|
|
61
117
|
|
|
62
|
-
build(
|
|
63
|
-
const tableName =
|
|
118
|
+
build(queryBuilder: ObjectionQueryBuilder<Model, Model[]>): ObjectionQueryBuilder<Model, Page<Model>> {
|
|
119
|
+
const tableName = queryBuilder.modelClass().tableName;
|
|
120
|
+
this.cleanQueryObject(tableName);
|
|
64
121
|
|
|
65
122
|
const pagination = this.pagination();
|
|
66
123
|
const sorting = this.sorting();
|
|
67
124
|
|
|
68
|
-
let qry =
|
|
125
|
+
let qry = queryBuilder.page(pagination.page, pagination.limit).orderBy(sorting.sortBy, sorting.sortDirection);
|
|
69
126
|
|
|
70
127
|
qry = this.filters(tableName, qry);
|
|
71
128
|
qry = this.greaterThan(tableName, qry);
|
|
@@ -104,7 +161,7 @@ export class QueryBuilder<Model extends ObjectionModel, OutputDTO> {
|
|
|
104
161
|
const searchVal = this.query.filters[filter];
|
|
105
162
|
|
|
106
163
|
if (searchVal && Array.isArray(searchVal)) {
|
|
107
|
-
if (searchVal.includes(
|
|
164
|
+
if (searchVal.includes('null')) {
|
|
108
165
|
query.whereNull(`${tableName}.${filter}`);
|
|
109
166
|
continue;
|
|
110
167
|
}
|