@travetto/model-sql 2.2.6 → 3.0.0-rc.2
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/README.md +9 -89
- package/package.json +10 -14
- package/src/internal/util.ts +4 -4
- package/test-support/query.ts +102 -0
- package/src/dialect/mysql/connection.ts +0 -85
- package/src/dialect/mysql/dialect.ts +0 -100
- package/src/dialect/postgresql/connection.ts +0 -74
- package/src/dialect/postgresql/dialect.ts +0 -69
- package/src/dialect/sqlite/connection.ts +0 -98
- package/src/dialect/sqlite/dialect.ts +0 -80
- package/support/service.mysql.ts +0 -16
- package/support/service.postgresql.ts +0 -17
package/README.md
CHANGED
|
@@ -10,109 +10,29 @@ npm install @travetto/model-sql
|
|
|
10
10
|
|
|
11
11
|
**Install: Specific SQL Client: mysql**
|
|
12
12
|
```bash
|
|
13
|
-
npm install mysql
|
|
13
|
+
npm install @travetto/model-mysql
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
or
|
|
17
17
|
|
|
18
|
-
**Install: Specific SQL Client:
|
|
18
|
+
**Install: Specific SQL Client: mysql**
|
|
19
19
|
```bash
|
|
20
|
-
npm install
|
|
20
|
+
npm install @travetto/model-postgres
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
or
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
**Install: Specific SQL Client: mysql**
|
|
26
|
+
```bash
|
|
27
|
+
npm install @travetto/model-sqlite
|
|
28
|
+
```
|
|
27
29
|
|
|
28
30
|
The current SQL client support stands at:
|
|
29
31
|
|
|
30
32
|
* [MySQL](https://www.mysql.com/) - 5.6 and 5.7
|
|
31
33
|
* [Postgres](https://postgresql.org) - 11+
|
|
34
|
+
* [SQLite](https://www.sqlite.org/) - (bettersqlite 7.6+)
|
|
32
35
|
* `SQL Server` - Currently unsupported
|
|
33
36
|
* `Oracle` - Currently unsupported
|
|
34
37
|
|
|
35
38
|
**Note**: Wider client support will roll out as usage increases.
|
|
36
|
-
|
|
37
|
-
Supported features:
|
|
38
|
-
|
|
39
|
-
* [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/service/crud.ts#L11)
|
|
40
|
-
* [Bulk](https://github.com/travetto/travetto/tree/main/module/model/src/service/bulk.ts#L23)
|
|
41
|
-
* [Query Crud](https://github.com/travetto/travetto/tree/main/module/model-query/src/service/crud.ts#L11)
|
|
42
|
-
* [Facet](https://github.com/travetto/travetto/tree/main/module/model-query/src/service/facet.ts#L12)
|
|
43
|
-
* [Query](https://github.com/travetto/travetto/tree/main/module/model-query/src/service/query.ts#L10)
|
|
44
|
-
* [Suggest](https://github.com/travetto/travetto/tree/main/module/model-query/src/service/suggest.ts#L12)
|
|
45
|
-
|
|
46
|
-
Out of the box, by installing the module, everything should be wired up by default.If you need to customize any aspect of the source
|
|
47
|
-
or config, you can override and register it with the [Dependency Injection](https://github.com/travetto/travetto/tree/main/module/di#readme "Dependency registration/management and injection support.") module.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
**Code: Wiring up a custom Model Source**
|
|
51
|
-
```typescript
|
|
52
|
-
import { AsyncContext } from '@travetto/context';
|
|
53
|
-
import { InjectableFactory } from '@travetto/di';
|
|
54
|
-
|
|
55
|
-
import { SQLModelService, SQLModelConfig } from '@travetto/model-sql';
|
|
56
|
-
import { MySQLDialect } from '@travetto/model-sql/src/dialect/mysql/dialect';
|
|
57
|
-
|
|
58
|
-
export class Init {
|
|
59
|
-
@InjectableFactory({ primary: true })
|
|
60
|
-
static getModelService(ctx: AsyncContext, conf: SQLModelConfig) {
|
|
61
|
-
return new SQLModelService(ctx, conf, new MySQLDialect(ctx, conf));
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
where the [SQLModelConfig](https://github.com/travetto/travetto/tree/main/module/model-sql/src/config.ts#L7) is defined by:
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
**Code: Structure of SQLModelConfig**
|
|
70
|
-
```typescript
|
|
71
|
-
import { Config } from '@travetto/config';
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* SQL Model Config
|
|
75
|
-
*/
|
|
76
|
-
@Config('model.sql')
|
|
77
|
-
export class SQLModelConfig {
|
|
78
|
-
/**
|
|
79
|
-
* Host to connect to
|
|
80
|
-
*/
|
|
81
|
-
host = '127.0.0.1';
|
|
82
|
-
/**
|
|
83
|
-
* Default port
|
|
84
|
-
*/
|
|
85
|
-
port = 0;
|
|
86
|
-
/**
|
|
87
|
-
* Username
|
|
88
|
-
*/
|
|
89
|
-
user = '';
|
|
90
|
-
/**
|
|
91
|
-
* Password
|
|
92
|
-
*/
|
|
93
|
-
password = '';
|
|
94
|
-
/**
|
|
95
|
-
* Table prefix
|
|
96
|
-
*/
|
|
97
|
-
namespace = '';
|
|
98
|
-
/**
|
|
99
|
-
* Database name
|
|
100
|
-
*/
|
|
101
|
-
database = 'app';
|
|
102
|
-
/**
|
|
103
|
-
* Auto schema creation
|
|
104
|
-
*/
|
|
105
|
-
autoCreate?: boolean;
|
|
106
|
-
/**
|
|
107
|
-
* Db version
|
|
108
|
-
*/
|
|
109
|
-
version = '';
|
|
110
|
-
/**
|
|
111
|
-
* Raw client options
|
|
112
|
-
*/
|
|
113
|
-
options = {};
|
|
114
|
-
}
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
Additionally, you can see that the class is registered with the [@Config](https://github.com/travetto/travetto/tree/main/module/config/src/decorator.ts#L9) annotation, and so these values can be overridden using the
|
|
118
|
-
standard [Configuration](https://github.com/travetto/travetto/tree/main/module/config#readme "Environment-aware config management using yaml files")resolution paths.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/model-sql",
|
|
3
3
|
"displayName": "SQL Model Service",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "3.0.0-rc.2",
|
|
5
5
|
"description": "SQL backing for the travetto model module, with real-time modeling support for SQL schemas.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"sql",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"files": [
|
|
21
21
|
"index.ts",
|
|
22
22
|
"src",
|
|
23
|
-
"support"
|
|
23
|
+
"support",
|
|
24
|
+
"test-support"
|
|
24
25
|
],
|
|
25
26
|
"main": "index.ts",
|
|
26
27
|
"repository": {
|
|
@@ -28,21 +29,16 @@
|
|
|
28
29
|
"directory": "module/model-sql"
|
|
29
30
|
},
|
|
30
31
|
"dependencies": {
|
|
31
|
-
"@travetto/config": "^
|
|
32
|
-
"@travetto/context": "^
|
|
33
|
-
"@travetto/model": "^
|
|
34
|
-
"@travetto/model-query": "
|
|
32
|
+
"@travetto/config": "^3.0.0-rc.2",
|
|
33
|
+
"@travetto/context": "^3.0.0-rc.2",
|
|
34
|
+
"@travetto/model": "^3.0.0-rc.2",
|
|
35
|
+
"@travetto/model-query": "3.0.0-rc.2"
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|
|
37
|
-
"@travetto/app": "^
|
|
38
|
+
"@travetto/app": "^3.0.0-rc.2"
|
|
38
39
|
},
|
|
39
|
-
"
|
|
40
|
-
"@
|
|
41
|
-
"@types/pg": "^8.6.5",
|
|
42
|
-
"mysql": "^2.18.1",
|
|
43
|
-
"pg": "^8.7.3",
|
|
44
|
-
"better-sqlite3": "^7.6.2",
|
|
45
|
-
"@types/better-sqlite3": "^7.6.0"
|
|
40
|
+
"docDependencies": {
|
|
41
|
+
"@travetto/model-mysql": 1
|
|
46
42
|
},
|
|
47
43
|
"publishConfig": {
|
|
48
44
|
"access": "public"
|
package/src/internal/util.ts
CHANGED
|
@@ -230,15 +230,15 @@ export class SQLUtil {
|
|
|
230
230
|
|
|
231
231
|
for (const [k, v] of Object.entries(select)) {
|
|
232
232
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
233
|
-
const
|
|
234
|
-
if (!Util.isPlainObject(select[
|
|
233
|
+
const sk = k as string;
|
|
234
|
+
if (!Util.isPlainObject(select[k]) && localMap[sk]) {
|
|
235
235
|
if (!v) {
|
|
236
236
|
if (toGet.size === 0) {
|
|
237
237
|
toGet = new Set(SchemaRegistry.get(cls).views[AllViewⲐ].fields);
|
|
238
238
|
}
|
|
239
|
-
toGet.delete(
|
|
239
|
+
toGet.delete(sk);
|
|
240
240
|
} else {
|
|
241
|
-
toGet.add(
|
|
241
|
+
toGet.add(sk);
|
|
242
242
|
}
|
|
243
243
|
}
|
|
244
244
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import * as assert from 'assert';
|
|
2
|
+
|
|
3
|
+
import { Schema, FieldConfig } from '@travetto/schema';
|
|
4
|
+
import { Suite, Test } from '@travetto/test';
|
|
5
|
+
import { BaseModelSuite } from '@travetto/model/test-support/base';
|
|
6
|
+
|
|
7
|
+
import { VisitStack } from '../src/internal/util';
|
|
8
|
+
import { SQLModelService } from '../src/service';
|
|
9
|
+
|
|
10
|
+
@Schema()
|
|
11
|
+
class User {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@Schema()
|
|
17
|
+
class WhereTypeAB {
|
|
18
|
+
c: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@Schema()
|
|
22
|
+
class WhereTypeA {
|
|
23
|
+
d: number;
|
|
24
|
+
b: WhereTypeAB;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@Schema()
|
|
28
|
+
class WhereTypeD {
|
|
29
|
+
e: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@Schema()
|
|
33
|
+
class WhereTypeG {
|
|
34
|
+
z: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@Schema()
|
|
38
|
+
class WhereType {
|
|
39
|
+
a: WhereTypeA[];
|
|
40
|
+
d: WhereTypeD;
|
|
41
|
+
g: WhereTypeG;
|
|
42
|
+
name: number;
|
|
43
|
+
age: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@Suite()
|
|
47
|
+
export abstract class BaseSQLTest extends BaseModelSuite<SQLModelService> {
|
|
48
|
+
|
|
49
|
+
get dialect() {
|
|
50
|
+
return this.service.then(s => s.client);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@Test()
|
|
54
|
+
async validateQuery() {
|
|
55
|
+
const qry = {
|
|
56
|
+
$and: [
|
|
57
|
+
{ a: { b: { c: 5 } } },
|
|
58
|
+
{ d: { e: true } },
|
|
59
|
+
{
|
|
60
|
+
$or: [{ name: 5 }, { age: 10 }]
|
|
61
|
+
},
|
|
62
|
+
{ g: { z: { $in: ['a', 'b', 'c'] } } },
|
|
63
|
+
{ a: { d: { $gt: 20 } } }
|
|
64
|
+
]
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const dct = await this.dialect;
|
|
68
|
+
dct.resolveName = (stack: VisitStack[]) => {
|
|
69
|
+
const field = stack[stack.length - 1] as FieldConfig;
|
|
70
|
+
const parent = stack[stack.length - 2] as FieldConfig;
|
|
71
|
+
return `${field.owner ? field.owner.name : parent.name}.${field.name}`;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const qryStr = dct.getWhereGroupingSQL(WhereType, qry);
|
|
75
|
+
assert(qryStr === "(WhereTypeAB.c = 5 AND WhereTypeD.e = TRUE AND (WhereType.name = 5 OR WhereType.age = 10) AND z.z IN ('a','b','c') AND WhereTypeA.d > 20)");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@Test()
|
|
79
|
+
async testRegEx() {
|
|
80
|
+
const dct = await this.dialect;
|
|
81
|
+
dct.resolveName = (stack: VisitStack[]) => {
|
|
82
|
+
const field = stack[stack.length - 1] as FieldConfig;
|
|
83
|
+
return `${field.owner?.name}.${field.name}`;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const out = dct.getWhereGroupingSQL(User, {
|
|
87
|
+
name: {
|
|
88
|
+
$regex: /google.$/
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
assert(out === `User.name ${dct.SQL_OPS.$regex} 'google.$'`);
|
|
93
|
+
|
|
94
|
+
const outBoundary = dct.getWhereGroupingSQL(User, {
|
|
95
|
+
name: {
|
|
96
|
+
$regex: /\bgoogle\b/
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
assert(outBoundary === `User.name ${dct.SQL_OPS.$regex} '${dct.regexWordBoundary}google${dct.regexWordBoundary}'`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
// @file-if mysql
|
|
2
|
-
import * as mysql from 'mysql';
|
|
3
|
-
|
|
4
|
-
import { ShutdownManager } from '@travetto/base';
|
|
5
|
-
import { AsyncContext } from '@travetto/context';
|
|
6
|
-
import { ExistsError } from '@travetto/model/src/error/exists';
|
|
7
|
-
|
|
8
|
-
import { Connection } from '../../connection/base';
|
|
9
|
-
import { SQLModelConfig } from '../../config';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Connection support for mysql
|
|
13
|
-
*/
|
|
14
|
-
export class MySQLConnection extends Connection<mysql.PoolConnection> {
|
|
15
|
-
|
|
16
|
-
#pool: mysql.Pool;
|
|
17
|
-
#config: SQLModelConfig;
|
|
18
|
-
|
|
19
|
-
constructor(
|
|
20
|
-
context: AsyncContext,
|
|
21
|
-
config: SQLModelConfig
|
|
22
|
-
) {
|
|
23
|
-
super(context);
|
|
24
|
-
this.#config = config;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async init(): Promise<void> {
|
|
28
|
-
this.#pool = mysql.createPool({
|
|
29
|
-
user: this.#config.user,
|
|
30
|
-
password: this.#config.password,
|
|
31
|
-
database: this.#config.database,
|
|
32
|
-
host: this.#config.host,
|
|
33
|
-
port: this.#config.port,
|
|
34
|
-
timezone: 'utc',
|
|
35
|
-
typeCast: this.typeCast.bind(this),
|
|
36
|
-
...(this.#config.options || {})
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
// Close mysql
|
|
40
|
-
ShutdownManager.onShutdown(this.constructor.ᚕid, () => new Promise(r => this.#pool.end(r)));
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Support some basic type support for JSON data
|
|
45
|
-
*/
|
|
46
|
-
typeCast(field: Parameters<Exclude<mysql.TypeCast, boolean>>[0], next: () => unknown): unknown {
|
|
47
|
-
const res = next();
|
|
48
|
-
if (typeof res === 'string' && (field.type === 'JSON' || field.type === 'BLOB')) {
|
|
49
|
-
if (res.charAt(0) === '{' && res.charAt(res.length - 1) === '}') {
|
|
50
|
-
try {
|
|
51
|
-
return JSON.parse(res);
|
|
52
|
-
} catch { }
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return res;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async execute<T = unknown>(conn: mysql.Connection, query: string): Promise<{ count: number, records: T[] }> {
|
|
59
|
-
return new Promise<{ count: number, records: T[] }>((res, rej) => {
|
|
60
|
-
console.debug('Executing Query', { query });
|
|
61
|
-
conn.query(query, (err, results, fields) => {
|
|
62
|
-
if (err) {
|
|
63
|
-
console.debug('Failed query', { error: err, query });
|
|
64
|
-
if (err.message.startsWith('ER_DUP_ENTRY')) {
|
|
65
|
-
rej(new ExistsError('query', query));
|
|
66
|
-
} else {
|
|
67
|
-
rej(err);
|
|
68
|
-
}
|
|
69
|
-
} else {
|
|
70
|
-
const records: T[] = Array.isArray(results) ? [...results].map(v => ({ ...v })) : [{ ...results }];
|
|
71
|
-
res({ records, count: results.affectedRows });
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
acquire(): Promise<mysql.PoolConnection> {
|
|
78
|
-
return new Promise<mysql.PoolConnection>((res, rej) =>
|
|
79
|
-
this.#pool.getConnection((err, conn) => err ? rej(err) : res(conn)));
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
release(conn: mysql.PoolConnection): void {
|
|
83
|
-
conn.release();
|
|
84
|
-
}
|
|
85
|
-
}
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
// @file-if mysql
|
|
2
|
-
import { FieldConfig } from '@travetto/schema';
|
|
3
|
-
import { Injectable } from '@travetto/di';
|
|
4
|
-
import { AsyncContext } from '@travetto/context';
|
|
5
|
-
import { WhereClause } from '@travetto/model-query';
|
|
6
|
-
import { Class } from '@travetto/base';
|
|
7
|
-
import { ModelType } from '@travetto/model';
|
|
8
|
-
|
|
9
|
-
import { SQLModelConfig } from '../../config';
|
|
10
|
-
import { SQLDialect } from '../base';
|
|
11
|
-
import { VisitStack } from '../../internal/util';
|
|
12
|
-
import { MySQLConnection } from './connection';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* MYSQL Dialect for the SQL Model Source
|
|
16
|
-
*/
|
|
17
|
-
@Injectable()
|
|
18
|
-
export class MySQLDialect extends SQLDialect {
|
|
19
|
-
|
|
20
|
-
conn: MySQLConnection;
|
|
21
|
-
tablePostfix = "COLLATE='utf8mb4_unicode_ci' ENGINE=InnoDB";
|
|
22
|
-
|
|
23
|
-
constructor(context: AsyncContext, public config: SQLModelConfig) {
|
|
24
|
-
super(config.namespace);
|
|
25
|
-
this.conn = new MySQLConnection(context, config);
|
|
26
|
-
|
|
27
|
-
// Customer operators
|
|
28
|
-
Object.assign(this.SQL_OPS, {
|
|
29
|
-
$regex: 'REGEXP BINARY',
|
|
30
|
-
$iregex: 'REGEXP'
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
// Custom types
|
|
34
|
-
Object.assign(this.COLUMN_TYPES, {
|
|
35
|
-
TIMESTAMP: 'DATETIME(3)',
|
|
36
|
-
JSON: 'TEXT'
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
// Word boundary
|
|
40
|
-
this.regexWordBoundary = '([[:<:]]|[[:>:]])';
|
|
41
|
-
// Field maxlength
|
|
42
|
-
this.idField.minlength = this.idField.maxlength = { n: this.KEY_LEN };
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Set string length limit based on version
|
|
46
|
-
*/
|
|
47
|
-
if (/^5[.][56]/.test(this.config.version)) {
|
|
48
|
-
this.DEFAULT_STRING_LEN = 191; // Mysql limitation with utf8 and keys
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Compute hash
|
|
54
|
-
*/
|
|
55
|
-
hash(value: string): string {
|
|
56
|
-
return `SHA2('${value}', ${this.KEY_LEN * 4})`;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Build identifier
|
|
61
|
-
*/
|
|
62
|
-
ident(field: FieldConfig | string): string {
|
|
63
|
-
return `\`${typeof field === 'string' ? field : field.name}\``;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Create table, adding in specific engine options
|
|
68
|
-
*/
|
|
69
|
-
override getCreateTableSQL(stack: VisitStack[]): string {
|
|
70
|
-
return super.getCreateTableSQL(stack).replace(/;$/, ` ${this.tablePostfix};`);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Define column modification
|
|
75
|
-
*/
|
|
76
|
-
getModifyColumnSQL(stack: VisitStack[]): string {
|
|
77
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
78
|
-
const field = stack[stack.length - 1] as FieldConfig;
|
|
79
|
-
return `ALTER TABLE ${this.parentTable(stack)} MODIFY COLUMN ${this.getColumnDefinition(field)};`;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Add root alias to delete clause
|
|
84
|
-
*/
|
|
85
|
-
override getDeleteSQL(stack: VisitStack[], where?: WhereClause<unknown>): string {
|
|
86
|
-
const sql = super.getDeleteSQL(stack, where);
|
|
87
|
-
return sql.replace(/\bDELETE\b/g, `DELETE ${this.rootAlias}`);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Suppress foreign key checks
|
|
92
|
-
*/
|
|
93
|
-
override getTruncateAllTablesSQL<T extends ModelType>(cls: Class<T>): string[] {
|
|
94
|
-
return [
|
|
95
|
-
'SET FOREIGN_KEY_CHECKS = 0;',
|
|
96
|
-
...super.getTruncateAllTablesSQL(cls),
|
|
97
|
-
'SET FOREIGN_KEY_CHECKS = 1;'
|
|
98
|
-
];
|
|
99
|
-
}
|
|
100
|
-
}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
// @file-if pg
|
|
2
|
-
import * as pg from 'pg';
|
|
3
|
-
|
|
4
|
-
import { ShutdownManager } from '@travetto/base';
|
|
5
|
-
import { AsyncContext, WithAsyncContext } from '@travetto/context';
|
|
6
|
-
import { ExistsError } from '@travetto/model';
|
|
7
|
-
|
|
8
|
-
import { Connection } from '../../connection/base';
|
|
9
|
-
import { SQLModelConfig } from '../../config';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Connection support for postgresql
|
|
13
|
-
*/
|
|
14
|
-
export class PostgreSQLConnection extends Connection<pg.PoolClient> {
|
|
15
|
-
|
|
16
|
-
#pool: pg.Pool;
|
|
17
|
-
#config: SQLModelConfig;
|
|
18
|
-
|
|
19
|
-
constructor(
|
|
20
|
-
context: AsyncContext,
|
|
21
|
-
config: SQLModelConfig
|
|
22
|
-
) {
|
|
23
|
-
super(context);
|
|
24
|
-
this.#config = config;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Initializes connection and establishes crypto extension for use with hashing
|
|
29
|
-
*/
|
|
30
|
-
@WithAsyncContext()
|
|
31
|
-
async init(): Promise<void> {
|
|
32
|
-
this.#pool = new pg.Pool({
|
|
33
|
-
user: this.#config.user,
|
|
34
|
-
password: this.#config.password,
|
|
35
|
-
database: this.#config.database,
|
|
36
|
-
host: this.#config.host,
|
|
37
|
-
port: this.#config.port,
|
|
38
|
-
parseInputDatesAsUTC: true,
|
|
39
|
-
...(this.#config.options || {})
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
await this.runWithActive(() =>
|
|
43
|
-
this.runWithTransaction('required', () =>
|
|
44
|
-
this.execute(this.active, 'CREATE EXTENSION IF NOT EXISTS pgcrypto;')
|
|
45
|
-
)
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
// Close postgres
|
|
49
|
-
ShutdownManager.onShutdown(this.constructor.ᚕid, () => this.#pool.end());
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async execute<T = unknown>(conn: pg.PoolClient, query: string): Promise<{ count: number, records: T[] }> {
|
|
53
|
-
console.debug('Executing query', { query });
|
|
54
|
-
try {
|
|
55
|
-
const out = await conn.query(query);
|
|
56
|
-
const records: T[] = [...out.rows].map(v => ({ ...v }));
|
|
57
|
-
return { count: out.rowCount, records };
|
|
58
|
-
} catch (err) {
|
|
59
|
-
if (err instanceof Error && err.message.includes('duplicate key value')) {
|
|
60
|
-
throw new ExistsError('query', query);
|
|
61
|
-
} else {
|
|
62
|
-
throw err;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
acquire(): Promise<pg.PoolClient> {
|
|
68
|
-
return this.#pool.connect();
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
release(conn: pg.PoolClient): void {
|
|
72
|
-
conn.release();
|
|
73
|
-
}
|
|
74
|
-
}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
// @file-if pg
|
|
2
|
-
import { FieldConfig } from '@travetto/schema';
|
|
3
|
-
import { Injectable } from '@travetto/di';
|
|
4
|
-
import { AsyncContext } from '@travetto/context';
|
|
5
|
-
import { ModelType } from '@travetto/model';
|
|
6
|
-
import { Class } from '@travetto/base';
|
|
7
|
-
|
|
8
|
-
import { SQLModelConfig } from '../../config';
|
|
9
|
-
import { SQLDialect } from '../base';
|
|
10
|
-
import { SQLUtil, VisitStack } from '../../internal/util';
|
|
11
|
-
import { PostgreSQLConnection } from './connection';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Postgresql Dialect for the SQL Model Source
|
|
15
|
-
*/
|
|
16
|
-
@Injectable()
|
|
17
|
-
export class PostgreSQLDialect extends SQLDialect {
|
|
18
|
-
|
|
19
|
-
conn: PostgreSQLConnection;
|
|
20
|
-
|
|
21
|
-
constructor(context: AsyncContext, public config: SQLModelConfig) {
|
|
22
|
-
super(config.namespace);
|
|
23
|
-
this.conn = new PostgreSQLConnection(context, config);
|
|
24
|
-
|
|
25
|
-
// Special operators
|
|
26
|
-
Object.assign(this.SQL_OPS, {
|
|
27
|
-
$regex: '~',
|
|
28
|
-
$iregex: '~*'
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
// Special types
|
|
32
|
-
Object.assign(this.COLUMN_TYPES, {
|
|
33
|
-
JSON: 'json',
|
|
34
|
-
TIMESTAMP: 'TIMESTAMP(6) WITH TIME ZONE'
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
// Word boundary
|
|
38
|
-
this.regexWordBoundary = '\\y';
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* How to hash
|
|
43
|
-
*/
|
|
44
|
-
hash(value: string): string {
|
|
45
|
-
return `encode(digest('${value}', 'sha1'), 'hex')`;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
ident(field: FieldConfig | string): string {
|
|
49
|
-
return `"${typeof field === 'string' ? field : field.name}"`;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Define column modification
|
|
54
|
-
*/
|
|
55
|
-
getModifyColumnSQL(stack: VisitStack[]): string {
|
|
56
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
57
|
-
const field = stack[stack.length - 1] as FieldConfig;
|
|
58
|
-
const type = this.getColumnType(field);
|
|
59
|
-
const ident = this.ident(field.name);
|
|
60
|
-
return `ALTER TABLE ${this.parentTable(stack)} ALTER COLUMN ${ident} TYPE ${type} USING (${ident}::${type});`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Suppress foreign key checks
|
|
65
|
-
*/
|
|
66
|
-
override getTruncateAllTablesSQL<T extends ModelType>(cls: Class<T>): string[] {
|
|
67
|
-
return [`TRUNCATE ${this.table(SQLUtil.classToStack(cls))} CASCADE;`];
|
|
68
|
-
}
|
|
69
|
-
}
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
// @file-if better-sqlite3
|
|
2
|
-
import * as sqlite3 from 'better-sqlite3';
|
|
3
|
-
import Db = require('better-sqlite3');
|
|
4
|
-
import * as pool from 'generic-pool';
|
|
5
|
-
|
|
6
|
-
import { ShutdownManager, Util } from '@travetto/base';
|
|
7
|
-
import { AsyncContext, WithAsyncContext } from '@travetto/context';
|
|
8
|
-
import { ExistsError } from '@travetto/model';
|
|
9
|
-
import { AppCache } from '@travetto/boot';
|
|
10
|
-
|
|
11
|
-
import { Connection } from '../../connection/base';
|
|
12
|
-
import { SQLModelConfig } from '../../config';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Connection support for Sqlite
|
|
16
|
-
*/
|
|
17
|
-
export class SqliteConnection extends Connection<sqlite3.Database> {
|
|
18
|
-
|
|
19
|
-
isolatedTransactions = false;
|
|
20
|
-
|
|
21
|
-
#config: SQLModelConfig;
|
|
22
|
-
#pool: pool.Pool<sqlite3.Database>;
|
|
23
|
-
|
|
24
|
-
constructor(
|
|
25
|
-
context: AsyncContext,
|
|
26
|
-
config: SQLModelConfig
|
|
27
|
-
) {
|
|
28
|
-
super(context);
|
|
29
|
-
this.#config = config;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async #withRetries<T>(op: () => Promise<T>, retries = 10, delay = 250): Promise<T> {
|
|
33
|
-
for (; ;) {
|
|
34
|
-
try {
|
|
35
|
-
return await op();
|
|
36
|
-
} catch (err) {
|
|
37
|
-
if (err instanceof Error && retries > 1 && err.message.includes('database is locked')) {
|
|
38
|
-
console.error('Failed, and waiting', retries);
|
|
39
|
-
await Util.wait(delay);
|
|
40
|
-
retries -= 1;
|
|
41
|
-
} else {
|
|
42
|
-
throw err;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Initializes connection and establishes crypto extension for use with hashing
|
|
50
|
-
*/
|
|
51
|
-
@WithAsyncContext()
|
|
52
|
-
override async init(): Promise<void> {
|
|
53
|
-
this.#pool = pool.createPool({
|
|
54
|
-
create: () => this.#withRetries(async () => {
|
|
55
|
-
const db = Db(AppCache.toEntryName('sqlite_db'),
|
|
56
|
-
this.#config.options
|
|
57
|
-
);
|
|
58
|
-
await db.pragma('foreign_keys = ON');
|
|
59
|
-
await db.pragma('journal_mode = WAL');
|
|
60
|
-
db.function('regexp', (a, b) => new RegExp(a).test(b) ? 1 : 0);
|
|
61
|
-
return db;
|
|
62
|
-
}),
|
|
63
|
-
destroy: async db => { db.close(); }
|
|
64
|
-
}, { max: 1 });
|
|
65
|
-
|
|
66
|
-
// Close postgres
|
|
67
|
-
ShutdownManager.onShutdown(this.constructor.ᚕid, () => this.#pool.clear());
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async execute<T = unknown>(conn: sqlite3.Database, query: string): Promise<{ count: number, records: T[] }> {
|
|
71
|
-
return this.#withRetries(async () => {
|
|
72
|
-
console.debug('Executing query', { query });
|
|
73
|
-
try {
|
|
74
|
-
const out = await conn.prepare(query)[query.trim().startsWith('SELECT') ? 'all' : 'run']();
|
|
75
|
-
if (Array.isArray(out)) {
|
|
76
|
-
const records: T[] = [...out].map(v => ({ ...v }));
|
|
77
|
-
return { count: out.length, records };
|
|
78
|
-
} else {
|
|
79
|
-
return { count: out.changes, records: [] };
|
|
80
|
-
}
|
|
81
|
-
} catch (err) {
|
|
82
|
-
if (err instanceof Error && err.message.includes('UNIQUE constraint failed')) {
|
|
83
|
-
throw new ExistsError('query', query);
|
|
84
|
-
} else {
|
|
85
|
-
throw err;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async acquire(): Promise<Db.Database> {
|
|
92
|
-
return this.#pool.acquire();
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async release(db: sqlite3.Database): Promise<void> {
|
|
96
|
-
return this.#pool.release(db);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
// @file-if better-sqlite3
|
|
2
|
-
import { FieldConfig } from '@travetto/schema';
|
|
3
|
-
import { Injectable } from '@travetto/di';
|
|
4
|
-
import { AsyncContext } from '@travetto/context';
|
|
5
|
-
import { WhereClause } from '@travetto/model-query';
|
|
6
|
-
|
|
7
|
-
import { SQLModelConfig } from '../../config';
|
|
8
|
-
import { SQLDialect } from '../base';
|
|
9
|
-
import { VisitStack } from '../../internal/util';
|
|
10
|
-
import { SqliteConnection } from './connection';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Sqlite Dialect for the SQL Model Source
|
|
14
|
-
*/
|
|
15
|
-
@Injectable()
|
|
16
|
-
export class SqliteDialect extends SQLDialect {
|
|
17
|
-
|
|
18
|
-
conn: SqliteConnection;
|
|
19
|
-
|
|
20
|
-
constructor(context: AsyncContext, public config: SQLModelConfig) {
|
|
21
|
-
super(config.namespace);
|
|
22
|
-
this.conn = new SqliteConnection(context, config);
|
|
23
|
-
|
|
24
|
-
// Special operators
|
|
25
|
-
Object.assign(this.SQL_OPS, {
|
|
26
|
-
$regex: 'REGEXP',
|
|
27
|
-
$ilike: undefined
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
// Special types
|
|
31
|
-
Object.assign(this.COLUMN_TYPES, {
|
|
32
|
-
JSON: 'TEXT',
|
|
33
|
-
TIMESTAMP: 'INTEGER'
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
override resolveDateValue(value: Date): string {
|
|
38
|
-
return `${value.getTime()}`;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* How to hash
|
|
43
|
-
*/
|
|
44
|
-
hash(value: string): string {
|
|
45
|
-
return `hex('${value}')`;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Build identifier
|
|
50
|
-
*/
|
|
51
|
-
ident(field: FieldConfig | string): string {
|
|
52
|
-
return `\`${typeof field === 'string' ? field : field.name}\``;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Define column modification
|
|
57
|
-
*/
|
|
58
|
-
getModifyColumnSQL(stack: VisitStack[]): string {
|
|
59
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
60
|
-
const field = stack[stack.length - 1] as FieldConfig;
|
|
61
|
-
const type = this.getColumnType(field);
|
|
62
|
-
const ident = this.ident(field.name);
|
|
63
|
-
return `ALTER TABLE ${this.parentTable(stack)} ALTER COLUMN ${ident} TYPE ${type} USING (${ident}::${type});`;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Generate truncate SQL
|
|
68
|
-
*/
|
|
69
|
-
override getTruncateTableSQL(stack: VisitStack[]): string {
|
|
70
|
-
return `DELETE FROM ${this.table(stack)};`;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
override getDeleteSQL(stack: VisitStack[], where?: WhereClause<unknown>): string {
|
|
74
|
-
return super.getDeleteSQL(stack, where).replace(/_ROOT[.]?/g, '');
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
override getUpdateSQL(stack: VisitStack[], data: Record<string, unknown>, where?: WhereClause<unknown>): string {
|
|
78
|
-
return super.getUpdateSQL(stack, data, where).replace(/_ROOT[.]?/g, '');
|
|
79
|
-
}
|
|
80
|
-
}
|
package/support/service.mysql.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
// @file-if mysql
|
|
2
|
-
import { EnvUtil } from '@travetto/boot';
|
|
3
|
-
import type { Service } from '@travetto/command/bin/lib/service';
|
|
4
|
-
|
|
5
|
-
const version = EnvUtil.get('TRV_SERVICE_MYSQL', '5.6');
|
|
6
|
-
|
|
7
|
-
export const service: Service = {
|
|
8
|
-
name: 'mysql',
|
|
9
|
-
version,
|
|
10
|
-
image: `mysql:${version}`,
|
|
11
|
-
port: 3306,
|
|
12
|
-
env: {
|
|
13
|
-
MYSQL_ROOT_PASSWORD: 'password',
|
|
14
|
-
MYSQL_DATABASE: 'app'
|
|
15
|
-
},
|
|
16
|
-
};
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
// @file-if pg
|
|
2
|
-
import { EnvUtil } from '@travetto/boot';
|
|
3
|
-
import type { Service } from '@travetto/command/bin/lib/service';
|
|
4
|
-
|
|
5
|
-
const version = EnvUtil.get('TRV_SERVICE_POSTGRESQL', '12.2');
|
|
6
|
-
|
|
7
|
-
export const service: Service = {
|
|
8
|
-
name: 'postgresql',
|
|
9
|
-
version,
|
|
10
|
-
port: 5432,
|
|
11
|
-
image: `postgres:${version}-alpine`,
|
|
12
|
-
env: {
|
|
13
|
-
POSTGRES_USER: 'root',
|
|
14
|
-
POSTGRES_PASSWORD: 'password',
|
|
15
|
-
POSTGRES_DB: 'app'
|
|
16
|
-
}
|
|
17
|
-
};
|