@rainbow-o23/n3 0.1.1
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/.babelrc +11 -0
- package/.eslintrc +23 -0
- package/README.md +584 -0
- package/index.cjs +2283 -0
- package/index.d.ts +5 -0
- package/index.js +2229 -0
- package/lib/error-codes.d.ts +17 -0
- package/lib/http/fetch-step.d.ts +53 -0
- package/lib/http/index.d.ts +2 -0
- package/lib/http/types.d.ts +21 -0
- package/lib/step/abstract-fragmentary-pipeline-step.d.ts +44 -0
- package/lib/step/async-step-sets.d.ts +5 -0
- package/lib/step/conditional-step-sets.d.ts +23 -0
- package/lib/step/delete-property-step.d.ts +11 -0
- package/lib/step/each-step-sets.d.ts +14 -0
- package/lib/step/get-property-step.d.ts +11 -0
- package/lib/step/index.d.ts +14 -0
- package/lib/step/ref-pipeline-step.d.ts +15 -0
- package/lib/step/ref-step-step.d.ts +14 -0
- package/lib/step/routes-step-sets.d.ts +30 -0
- package/lib/step/snippet-step.d.ts +19 -0
- package/lib/step/snowflake-step.d.ts +6 -0
- package/lib/step/step-sets.d.ts +19 -0
- package/lib/step/types.d.ts +13 -0
- package/lib/step/utils.d.ts +23 -0
- package/lib/typeorm/abstract-datasource.d.ts +22 -0
- package/lib/typeorm/better-sqlite3-datasource.d.ts +7 -0
- package/lib/typeorm/datasource-manager.d.ts +25 -0
- package/lib/typeorm/index.d.ts +7 -0
- package/lib/typeorm/mssql-datasource.d.ts +6 -0
- package/lib/typeorm/mysql-datasource.d.ts +6 -0
- package/lib/typeorm/oracle-datasource.d.ts +6 -0
- package/lib/typeorm/pgsql-datasource.d.ts +6 -0
- package/lib/typeorm-step/abstract-typeorm-by-sql-step.d.ts +62 -0
- package/lib/typeorm-step/abstract-typeorm-load-by-sql-step.d.ts +10 -0
- package/lib/typeorm-step/abstract-typeorm-step.d.ts +30 -0
- package/lib/typeorm-step/index.d.ts +12 -0
- package/lib/typeorm-step/type-orm-transactional-step-sets.d.ts +22 -0
- package/lib/typeorm-step/typeorm-bulk-save-by-sql-step.d.ts +9 -0
- package/lib/typeorm-step/typeorm-by-snippet-step.d.ts +21 -0
- package/lib/typeorm-step/typeorm-load-entity-by-id-step.d.ts +12 -0
- package/lib/typeorm-step/typeorm-load-many-by-sql-step.d.ts +6 -0
- package/lib/typeorm-step/typeorm-load-one-by-sql-step.d.ts +6 -0
- package/lib/typeorm-step/typeorm-save-by-sql-step.d.ts +9 -0
- package/lib/typeorm-step/typeorm-save-entity-step.d.ts +32 -0
- package/lib/typeorm-step/types.d.ts +22 -0
- package/package.json +74 -0
- package/rollup.config.base.js +33 -0
- package/rollup.config.ci.js +3 -0
- package/rollup.config.js +3 -0
- package/src/index.ts +7 -0
- package/src/lib/error-codes.ts +18 -0
- package/src/lib/http/fetch-step.ts +290 -0
- package/src/lib/http/index.ts +3 -0
- package/src/lib/http/types.ts +33 -0
- package/src/lib/step/abstract-fragmentary-pipeline-step.ts +367 -0
- package/src/lib/step/async-step-sets.ts +14 -0
- package/src/lib/step/conditional-step-sets.ts +115 -0
- package/src/lib/step/delete-property-step.ts +33 -0
- package/src/lib/step/each-step-sets.ts +80 -0
- package/src/lib/step/get-property-step.ts +34 -0
- package/src/lib/step/index.ts +18 -0
- package/src/lib/step/ref-pipeline-step.ts +65 -0
- package/src/lib/step/ref-step-step.ts +59 -0
- package/src/lib/step/routes-step-sets.ts +147 -0
- package/src/lib/step/snippet-step.ts +80 -0
- package/src/lib/step/snowflake-step.ts +16 -0
- package/src/lib/step/step-sets.ts +99 -0
- package/src/lib/step/types.ts +23 -0
- package/src/lib/step/utils.ts +109 -0
- package/src/lib/typeorm/abstract-datasource.ts +58 -0
- package/src/lib/typeorm/better-sqlite3-datasource.ts +29 -0
- package/src/lib/typeorm/datasource-manager.ts +104 -0
- package/src/lib/typeorm/index.ts +9 -0
- package/src/lib/typeorm/mssql-datasource.ts +131 -0
- package/src/lib/typeorm/mysql-datasource.ts +35 -0
- package/src/lib/typeorm/oracle-datasource.ts +27 -0
- package/src/lib/typeorm/pgsql-datasource.ts +25 -0
- package/src/lib/typeorm-step/abstract-typeorm-by-sql-step.ts +556 -0
- package/src/lib/typeorm-step/abstract-typeorm-load-by-sql-step.ts +31 -0
- package/src/lib/typeorm-step/abstract-typeorm-step.ts +241 -0
- package/src/lib/typeorm-step/index.ts +17 -0
- package/src/lib/typeorm-step/type-orm-transactional-step-sets.ts +129 -0
- package/src/lib/typeorm-step/typeorm-bulk-save-by-sql-step.ts +29 -0
- package/src/lib/typeorm-step/typeorm-by-snippet-step.ts +83 -0
- package/src/lib/typeorm-step/typeorm-load-entity-by-id-step.ts +35 -0
- package/src/lib/typeorm-step/typeorm-load-many-by-sql-step.ts +13 -0
- package/src/lib/typeorm-step/typeorm-load-one-by-sql-step.ts +13 -0
- package/src/lib/typeorm-step/typeorm-save-by-sql-step.ts +24 -0
- package/src/lib/typeorm-step/typeorm-save-entity-step.ts +109 -0
- package/src/lib/typeorm-step/types.ts +25 -0
- package/test/step/snippet-step.test.ts +21 -0
- package/test/step/snowflake-step.test.ts +22 -0
- package/test/step/typeorm-by-sql-autonomous.test.ts +117 -0
- package/test/step/typeorm-by-sql-transactional.test.ts +215 -0
- package/test/step/typeorm-entity.test.ts +50 -0
- package/tsconfig.json +36 -0
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
import {PipelineStepPayload, StepHelpersUtils, UncatchableError, Undefinable} from '@rainbow-o23/n1';
|
|
2
|
+
import {ERR_TYPEORM_SQL_NOT_EMPTY} from '../error-codes';
|
|
3
|
+
import {Utils} from '../step';
|
|
4
|
+
import {DataSourceType, SupportedDataSourceTypes} from '../typeorm';
|
|
5
|
+
import {AbstractTypeOrmPipelineStep, TypeOrmPipelineStepOptions} from './abstract-typeorm-step';
|
|
6
|
+
import {TypeOrmEntityToSave, TypeOrmEntityValue, TypeOrmSql} from './types';
|
|
7
|
+
|
|
8
|
+
export interface TypeOrmBasis {
|
|
9
|
+
sql?: TypeOrmSql;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface TypeOrmBySQLPipelineStepOptions<In = PipelineStepPayload, Out = PipelineStepPayload, InFragment = TypeOrmBasis, OutFragment = Out>
|
|
13
|
+
extends TypeOrmPipelineStepOptions<In, Out, InFragment, OutFragment> {
|
|
14
|
+
sql?: TypeOrmSql;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ParsedTypeOrmSql {
|
|
18
|
+
sql: TypeOrmSql;
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
|
+
params: Array<any>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export enum ParsedSqlSegmentType {
|
|
24
|
+
NONE = 'none',
|
|
25
|
+
SINGLE = 'single',
|
|
26
|
+
/** in */
|
|
27
|
+
ONE_OF = 'one-of',
|
|
28
|
+
/** %x% */
|
|
29
|
+
CONTAINS = 'contains',
|
|
30
|
+
/** %x */
|
|
31
|
+
ENDS_WITH = 'ends-with',
|
|
32
|
+
/** x% */
|
|
33
|
+
STARTS_WITH = 'starts-with'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ParsedSqlSegment {
|
|
37
|
+
statement: string;
|
|
38
|
+
type: ParsedSqlSegmentType;
|
|
39
|
+
/** effective when type is not none */
|
|
40
|
+
variable?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class TypeOrmParsedSQLCache {
|
|
44
|
+
[key: TypeOrmSql]: Array<ParsedSqlSegment>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface KindOfMySQLResult {
|
|
48
|
+
changedRows?: number;
|
|
49
|
+
affectedRows?: number;
|
|
50
|
+
insertId?: string | number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const PARSED_SQL_CACHE: TypeOrmParsedSQLCache = {};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* sql could be defined in step itself, or passed by in fragment.
|
|
57
|
+
* if sql is carried by in fragment, sql from options should be ignored.
|
|
58
|
+
*/
|
|
59
|
+
export abstract class AbstractTypeOrmBySQLPipelineStep<In = PipelineStepPayload, Out = PipelineStepPayload, InFragment = Undefinable<TypeOrmBasis>, OutFragment = Out>
|
|
60
|
+
extends AbstractTypeOrmPipelineStep<In, Out, InFragment, OutFragment> {
|
|
61
|
+
private readonly _timestampWriteFormat: string;
|
|
62
|
+
private readonly _timestampReadFormat: string;
|
|
63
|
+
private readonly _sql: TypeOrmSql;
|
|
64
|
+
private _parsedSqlSegments: Undefinable<Array<ParsedSqlSegment>> = (void 0);
|
|
65
|
+
|
|
66
|
+
// noinspection TypeScriptAbstractClassConstructorCanBeMadeProtected
|
|
67
|
+
public constructor(options: TypeOrmBySQLPipelineStepOptions<In, Out, InFragment, OutFragment>) {
|
|
68
|
+
super(options);
|
|
69
|
+
this._sql = options.sql;
|
|
70
|
+
const formats = this.getDefaultTimestampFormats();
|
|
71
|
+
this._timestampWriteFormat = this.getConfig().getString(`typeorm.${this.getDataSourceName()}.timestamp.format.write`, formats.write);
|
|
72
|
+
this._timestampReadFormat = this.getConfig().getString(`typeorm.${this.getDataSourceName()}.timestamp.format.read`, formats.read);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* read: dayjs pattern
|
|
77
|
+
* write: database dialect
|
|
78
|
+
*/
|
|
79
|
+
protected getDefaultTimestampFormats(): { write: string, read: string } {
|
|
80
|
+
switch (this.findDataSourceType()) {
|
|
81
|
+
case SupportedDataSourceTypes.ORACLE:
|
|
82
|
+
// TO_TIMESTAMP(X, WRITE)
|
|
83
|
+
return {read: 'YYYY-MM-DD HH:mm:ss', write: 'YYYY-MM-DD HH24:MI:SS'};
|
|
84
|
+
case SupportedDataSourceTypes.POSTGRES:
|
|
85
|
+
// TO_TIMESTAMP(X, WRITE)
|
|
86
|
+
return {read: 'YYYY-MM-DDTHH:mm:ss.SSSZ', write: 'YYYY-MM-DD HH24:MI:SS'};
|
|
87
|
+
case SupportedDataSourceTypes.MYSQL:
|
|
88
|
+
// STR_TO_DATE(X, WRITE)
|
|
89
|
+
return {read: 'YYYY-MM-DD HH:mm:ss', write: '%Y-%m-%d %H:%k:%s'};
|
|
90
|
+
case SupportedDataSourceTypes.MSSQL:
|
|
91
|
+
// FORMAT(X, WRITE)
|
|
92
|
+
return {read: 'YYYY-MM-DD HH:mm:ss', write: 'yyyy-MM-dd hh:mm:ss'};
|
|
93
|
+
case SupportedDataSourceTypes.BETTER_SQLITE3:
|
|
94
|
+
default:
|
|
95
|
+
return {read: 'YYYY-MM-DD HH:mm:ss', write: 'YYYY-MM-DD HH:MM:SS'};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
protected getTimestampWriteFormat(): string {
|
|
100
|
+
return this._timestampWriteFormat;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
protected getTimestampReadFormat(): string {
|
|
104
|
+
return this._timestampReadFormat;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
protected getParsedSqlSegments(): Undefinable<Array<ParsedSqlSegment>> {
|
|
108
|
+
return this._parsedSqlSegments;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
protected parseSql(sql: TypeOrmSql): void {
|
|
112
|
+
if (this.getConfig().getBoolean('typeorm.sql.cache.enabled', true)) {
|
|
113
|
+
// TODO TYPEORM SQL CACHE
|
|
114
|
+
const segments = PARSED_SQL_CACHE[sql];
|
|
115
|
+
if (segments != null && segments.length !== 0) {
|
|
116
|
+
this._parsedSqlSegments = segments;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let hasOneOf = false;
|
|
122
|
+
this._parsedSqlSegments = sql.match(/[^$]+|[$]/g).reduce((context, segment) => {
|
|
123
|
+
if (context.$) {
|
|
124
|
+
// split to variable name part and standard statement part
|
|
125
|
+
const isContainsOrEndsWith = segment.startsWith('%');
|
|
126
|
+
const s = isContainsOrEndsWith ? segment.substring(1) : segment;
|
|
127
|
+
const chars = [' ', '\n', '\r', '\t', ',', ')', '+', '-', '*', '/', '%'];
|
|
128
|
+
let firstPosition = -1;
|
|
129
|
+
for (const char of chars) {
|
|
130
|
+
const position = s.indexOf(char);
|
|
131
|
+
if (position !== -1 && (firstPosition === -1 || position < firstPosition)) {
|
|
132
|
+
if (char === '%') {
|
|
133
|
+
firstPosition = position + 1;
|
|
134
|
+
} else {
|
|
135
|
+
firstPosition = position;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (firstPosition === -1) {
|
|
140
|
+
// nothing found, segment is variable name
|
|
141
|
+
if (isContainsOrEndsWith) {
|
|
142
|
+
// ends with
|
|
143
|
+
context.segments.push({statement: segment, type: ParsedSqlSegmentType.ENDS_WITH, variable: s});
|
|
144
|
+
} else if (s.startsWith('...')) {
|
|
145
|
+
context.segments.push({
|
|
146
|
+
statement: segment,
|
|
147
|
+
type: ParsedSqlSegmentType.ONE_OF,
|
|
148
|
+
variable: s.substring(3)
|
|
149
|
+
});
|
|
150
|
+
hasOneOf = true;
|
|
151
|
+
} else {
|
|
152
|
+
context.segments.push({statement: segment, type: ParsedSqlSegmentType.SINGLE, variable: s});
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
const variable = s.substring(0, firstPosition);
|
|
156
|
+
const rest = s.substring(firstPosition);
|
|
157
|
+
if (variable.endsWith('%')) {
|
|
158
|
+
if (isContainsOrEndsWith) {
|
|
159
|
+
// contains
|
|
160
|
+
context.segments.push({
|
|
161
|
+
statement: variable,
|
|
162
|
+
type: ParsedSqlSegmentType.CONTAINS,
|
|
163
|
+
variable: variable.substring(0, firstPosition - 1)
|
|
164
|
+
});
|
|
165
|
+
} else {
|
|
166
|
+
// starts with
|
|
167
|
+
context.segments.push({
|
|
168
|
+
statement: variable,
|
|
169
|
+
type: ParsedSqlSegmentType.STARTS_WITH,
|
|
170
|
+
variable: variable.substring(0, firstPosition - 1)
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
if (isContainsOrEndsWith) {
|
|
175
|
+
// ends with
|
|
176
|
+
context.segments.push({
|
|
177
|
+
statement: variable,
|
|
178
|
+
type: ParsedSqlSegmentType.ENDS_WITH,
|
|
179
|
+
variable
|
|
180
|
+
});
|
|
181
|
+
} else if (variable.startsWith('...')) {
|
|
182
|
+
context.segments.push({
|
|
183
|
+
statement: variable,
|
|
184
|
+
type: ParsedSqlSegmentType.ONE_OF,
|
|
185
|
+
variable: variable.substring(3)
|
|
186
|
+
});
|
|
187
|
+
hasOneOf = true;
|
|
188
|
+
} else {
|
|
189
|
+
// starts with
|
|
190
|
+
context.segments.push({statement: variable, type: ParsedSqlSegmentType.SINGLE, variable});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
context.segments.push({statement: rest, type: ParsedSqlSegmentType.NONE});
|
|
194
|
+
}
|
|
195
|
+
context.$ = false;
|
|
196
|
+
} else if (segment === '$') {
|
|
197
|
+
context.$ = true;
|
|
198
|
+
} else {
|
|
199
|
+
context.segments.push({statement: segment, type: ParsedSqlSegmentType.NONE});
|
|
200
|
+
}
|
|
201
|
+
return context;
|
|
202
|
+
}, {$: false, segments: []} as { $: boolean, segments: Array<ParsedSqlSegment> }).segments;
|
|
203
|
+
if (!hasOneOf) {
|
|
204
|
+
PARSED_SQL_CACHE[sql] = this._parsedSqlSegments;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
protected replaceSqlPagination(sql: TypeOrmSql, type: DataSourceType): TypeOrmSql {
|
|
209
|
+
if (!/\$\.limit/gmi.test(sql)) {
|
|
210
|
+
// no pagination
|
|
211
|
+
return sql;
|
|
212
|
+
}
|
|
213
|
+
switch (type) {
|
|
214
|
+
case SupportedDataSourceTypes.POSTGRES:
|
|
215
|
+
return sql.replace(/^(.*)(\$\.limit\((.+),\s*(.+)\))(.*)$/gmi, '$1OFFSET $3 LIMIT $4$5');
|
|
216
|
+
case SupportedDataSourceTypes.MSSQL:
|
|
217
|
+
if (/order\s+by/i.test(sql)) {
|
|
218
|
+
return sql.replace(/^(.*)(\$\.limit\((.+),\s*(.+)\))(.*)$/gmi, '$1OFFSET $3 ROWS FETCH NEXT $4$5 ROWS ONLY');
|
|
219
|
+
} else {
|
|
220
|
+
// mssql always need an order by part, otherwise raise exception
|
|
221
|
+
return sql.replace(/^(.*)(\$\.limit\((.+),\s*(.+)\))(.*)$/gmi, '$1ORDER BY 1 OFFSET $3 ROWS FETCH NEXT $4$5 ROWS ONLY');
|
|
222
|
+
}
|
|
223
|
+
case SupportedDataSourceTypes.ORACLE:
|
|
224
|
+
// 12c and later
|
|
225
|
+
return sql.replace(/^(.*)(\$\.limit\((.+),\s*(.+)\))(.*)$/gmi, '$1OFFSET $3 ROWS FETCH NEXT $4$5 ROWS ONLY');
|
|
226
|
+
case SupportedDataSourceTypes.MYSQL:
|
|
227
|
+
case SupportedDataSourceTypes.BETTER_SQLITE3:
|
|
228
|
+
default:
|
|
229
|
+
return sql.replace(/^(.*)(\$\.limit\((.+),\s*(.+)\))(.*)$/gmi, '$1LIMIT $3, $4$5');
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
protected computeSqlPlaceholder(options: {
|
|
234
|
+
segment: ParsedSqlSegment; placeholderIndex: number; datasourceType: DataSourceType
|
|
235
|
+
}): string {
|
|
236
|
+
const {segment, placeholderIndex, datasourceType} = options;
|
|
237
|
+
switch (datasourceType) {
|
|
238
|
+
case SupportedDataSourceTypes.POSTGRES:
|
|
239
|
+
// psql starts from 1, using $1, $2, etc
|
|
240
|
+
return `$${placeholderIndex + 1}`;
|
|
241
|
+
case SupportedDataSourceTypes.MSSQL:
|
|
242
|
+
// mysql starts from 0, using @0, @1, etc.
|
|
243
|
+
return `@${placeholderIndex}`;
|
|
244
|
+
case SupportedDataSourceTypes.ORACLE:
|
|
245
|
+
// oracle starts from 0, using :0, :1, etc
|
|
246
|
+
if (segment.type === ParsedSqlSegmentType.SINGLE && segment.variable.endsWith('.@ts')) {
|
|
247
|
+
return `TO_TIMESTAMP(:${placeholderIndex}, '${this.getTimestampWriteFormat()}')`;
|
|
248
|
+
} else {
|
|
249
|
+
return `:${placeholderIndex}`;
|
|
250
|
+
}
|
|
251
|
+
case SupportedDataSourceTypes.MYSQL:
|
|
252
|
+
case SupportedDataSourceTypes.BETTER_SQLITE3:
|
|
253
|
+
default:
|
|
254
|
+
// using ?, index is not needed
|
|
255
|
+
return '?';
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
protected getBindingValue(options: {
|
|
260
|
+
params: TypeOrmEntityToSave; segment: ParsedSqlSegment; datasourceType: DataSourceType;
|
|
261
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
262
|
+
}): any {
|
|
263
|
+
const {params, segment} = options;
|
|
264
|
+
|
|
265
|
+
const json = segment.variable.endsWith('.@json');
|
|
266
|
+
const bool = segment.variable.endsWith('.@bool');
|
|
267
|
+
const timestamp = segment.variable.endsWith('.@ts');
|
|
268
|
+
let variable = segment.variable;
|
|
269
|
+
if (json) {
|
|
270
|
+
variable = variable.substring(0, variable.length - 6);
|
|
271
|
+
} else if (bool) {
|
|
272
|
+
variable = variable.substring(0, variable.length - 6);
|
|
273
|
+
} else if (timestamp) {
|
|
274
|
+
variable = variable.substring(0, variable.length - 4);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const value = Utils.getValue(params, variable);
|
|
278
|
+
if (value == null) {
|
|
279
|
+
return value;
|
|
280
|
+
} else if (json) {
|
|
281
|
+
if (typeof value === 'object') {
|
|
282
|
+
return JSON.stringify(value);
|
|
283
|
+
} else {
|
|
284
|
+
return value;
|
|
285
|
+
}
|
|
286
|
+
} else if (bool) {
|
|
287
|
+
return value === true || value === '1' ? 1 : 0;
|
|
288
|
+
} else if (timestamp) {
|
|
289
|
+
return value;
|
|
290
|
+
} else {
|
|
291
|
+
return value;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
protected getSql(basis: Undefinable<TypeOrmBasis>, params: Array<TypeOrmEntityValue> | TypeOrmEntityToSave): ParsedTypeOrmSql {
|
|
296
|
+
let sql: TypeOrmSql;
|
|
297
|
+
if (basis?.sql == null || basis.sql.trim().length === 0) {
|
|
298
|
+
if (this._sql == null || this._sql.trim().length === 0) {
|
|
299
|
+
throw new UncatchableError(ERR_TYPEORM_SQL_NOT_EMPTY, 'SQL cannot be empty.');
|
|
300
|
+
}
|
|
301
|
+
sql = this._sql;
|
|
302
|
+
} else {
|
|
303
|
+
sql = basis.sql;
|
|
304
|
+
}
|
|
305
|
+
const datasourceType = this.findDataSourceType();
|
|
306
|
+
sql = this.replaceSqlPagination(sql, datasourceType);
|
|
307
|
+
if (params == null || Array.isArray(params)) {
|
|
308
|
+
// no parameters or given parameters is an array,
|
|
309
|
+
// which means sql does not include any named variable, return directly
|
|
310
|
+
return {sql, params: (params ?? []) as Array<TypeOrmEntityValue>};
|
|
311
|
+
} else {
|
|
312
|
+
// parse given sql
|
|
313
|
+
this.parseSql(sql);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
let placeholderIndex = 0;
|
|
317
|
+
// build parsed sql, and params
|
|
318
|
+
return this.getParsedSqlSegments().reduce((parsed, segment) => {
|
|
319
|
+
switch (segment.type) {
|
|
320
|
+
case ParsedSqlSegmentType.SINGLE:
|
|
321
|
+
parsed.sql = parsed.sql + this.computeSqlPlaceholder({segment, placeholderIndex, datasourceType});
|
|
322
|
+
parsed.params.push(this.getBindingValue({params, segment, datasourceType}));
|
|
323
|
+
placeholderIndex++;
|
|
324
|
+
break;
|
|
325
|
+
case ParsedSqlSegmentType.STARTS_WITH:
|
|
326
|
+
switch (datasourceType) {
|
|
327
|
+
case SupportedDataSourceTypes.POSTGRES:
|
|
328
|
+
case SupportedDataSourceTypes.ORACLE:
|
|
329
|
+
parsed.sql = parsed.sql + `${this.computeSqlPlaceholder({
|
|
330
|
+
segment,
|
|
331
|
+
placeholderIndex,
|
|
332
|
+
datasourceType
|
|
333
|
+
})} || '%'`;
|
|
334
|
+
break;
|
|
335
|
+
case SupportedDataSourceTypes.MSSQL:
|
|
336
|
+
case SupportedDataSourceTypes.MYSQL:
|
|
337
|
+
case SupportedDataSourceTypes.BETTER_SQLITE3:
|
|
338
|
+
default:
|
|
339
|
+
parsed.sql = parsed.sql + `CONCAT(${this.computeSqlPlaceholder({
|
|
340
|
+
segment,
|
|
341
|
+
placeholderIndex,
|
|
342
|
+
datasourceType
|
|
343
|
+
})}, '%')`;
|
|
344
|
+
}
|
|
345
|
+
parsed.params.push(this.getBindingValue({params, segment, datasourceType}));
|
|
346
|
+
placeholderIndex++;
|
|
347
|
+
break;
|
|
348
|
+
case ParsedSqlSegmentType.CONTAINS:
|
|
349
|
+
switch (datasourceType) {
|
|
350
|
+
case SupportedDataSourceTypes.POSTGRES:
|
|
351
|
+
case SupportedDataSourceTypes.ORACLE:
|
|
352
|
+
parsed.sql = parsed.sql + `'%' || ${this.computeSqlPlaceholder({
|
|
353
|
+
segment,
|
|
354
|
+
placeholderIndex,
|
|
355
|
+
datasourceType
|
|
356
|
+
})} || '%'`;
|
|
357
|
+
break;
|
|
358
|
+
case SupportedDataSourceTypes.MSSQL:
|
|
359
|
+
case SupportedDataSourceTypes.MYSQL:
|
|
360
|
+
case SupportedDataSourceTypes.BETTER_SQLITE3:
|
|
361
|
+
default:
|
|
362
|
+
parsed.sql = parsed.sql + `CONCAT('%', ${this.computeSqlPlaceholder({
|
|
363
|
+
segment,
|
|
364
|
+
placeholderIndex,
|
|
365
|
+
datasourceType
|
|
366
|
+
})}, '%')`;
|
|
367
|
+
}
|
|
368
|
+
parsed.params.push(this.getBindingValue({params, segment, datasourceType}));
|
|
369
|
+
placeholderIndex++;
|
|
370
|
+
break;
|
|
371
|
+
case ParsedSqlSegmentType.ENDS_WITH:
|
|
372
|
+
switch (datasourceType) {
|
|
373
|
+
case SupportedDataSourceTypes.POSTGRES:
|
|
374
|
+
case SupportedDataSourceTypes.ORACLE:
|
|
375
|
+
parsed.sql = parsed.sql + `'%' || ${this.computeSqlPlaceholder({
|
|
376
|
+
segment,
|
|
377
|
+
placeholderIndex,
|
|
378
|
+
datasourceType
|
|
379
|
+
})}`;
|
|
380
|
+
break;
|
|
381
|
+
case SupportedDataSourceTypes.MSSQL:
|
|
382
|
+
case SupportedDataSourceTypes.MYSQL:
|
|
383
|
+
case SupportedDataSourceTypes.BETTER_SQLITE3:
|
|
384
|
+
default:
|
|
385
|
+
parsed.sql = parsed.sql + `CONCAT('%', ${this.computeSqlPlaceholder({
|
|
386
|
+
segment,
|
|
387
|
+
placeholderIndex,
|
|
388
|
+
datasourceType
|
|
389
|
+
})})`;
|
|
390
|
+
}
|
|
391
|
+
parsed.params.push(this.getBindingValue({params, segment, datasourceType}));
|
|
392
|
+
placeholderIndex++;
|
|
393
|
+
break;
|
|
394
|
+
case ParsedSqlSegmentType.ONE_OF: {
|
|
395
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
396
|
+
let values: Array<any> | any = this.getBindingValue({params, segment, datasourceType});
|
|
397
|
+
if (!Array.isArray(values)) {
|
|
398
|
+
values = [values];
|
|
399
|
+
}
|
|
400
|
+
if (values.length === 0) {
|
|
401
|
+
// in statement, at least one value
|
|
402
|
+
values.push(null);
|
|
403
|
+
}
|
|
404
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
405
|
+
parsed.sql = parsed.sql + values.map(_ => {
|
|
406
|
+
const placeholder = this.computeSqlPlaceholder({segment, placeholderIndex, datasourceType});
|
|
407
|
+
placeholderIndex++;
|
|
408
|
+
return placeholder;
|
|
409
|
+
}).join(', ');
|
|
410
|
+
parsed.params.push(...values);
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
case ParsedSqlSegmentType.NONE:
|
|
414
|
+
default:
|
|
415
|
+
parsed.sql = parsed.sql + segment.statement;
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
return parsed;
|
|
419
|
+
}, {sql: '', params: []} as ParsedTypeOrmSql);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
protected beautify<T>(options: { data: T; datasourceType: DataSourceType }): T {
|
|
423
|
+
const {data} = options;
|
|
424
|
+
|
|
425
|
+
if (data == null) {
|
|
426
|
+
return data;
|
|
427
|
+
} else {
|
|
428
|
+
const $helpers = this.getHelpers();
|
|
429
|
+
Object.keys(data)
|
|
430
|
+
.map(key => {
|
|
431
|
+
let json = false;
|
|
432
|
+
let bool = false;
|
|
433
|
+
let timestamp = false;
|
|
434
|
+
let newKey = key;
|
|
435
|
+
if (key.endsWith('.@json')) {
|
|
436
|
+
json = true;
|
|
437
|
+
newKey = key.substring(0, key.length - 6).trim();
|
|
438
|
+
} else if (key.startsWith('[') && key.endsWith('.@json]')) {
|
|
439
|
+
json = true;
|
|
440
|
+
newKey = key.substring(1, key.length - 7).trim();
|
|
441
|
+
} else if (key.endsWith('.@bool')) {
|
|
442
|
+
bool = true;
|
|
443
|
+
newKey = key.substring(0, key.length - 6).trim();
|
|
444
|
+
} else if (key.startsWith('[') && key.endsWith('.@bool]')) {
|
|
445
|
+
bool = true;
|
|
446
|
+
newKey = key.substring(1, key.length - 7).trim();
|
|
447
|
+
} else if (key.endsWith('.@ts')) {
|
|
448
|
+
timestamp = true;
|
|
449
|
+
newKey = key.substring(0, key.length - 4).trim();
|
|
450
|
+
} else if (key.startsWith('[') && key.endsWith('.@ts]')) {
|
|
451
|
+
timestamp = true;
|
|
452
|
+
newKey = key.substring(1, key.length - 5).trim();
|
|
453
|
+
}
|
|
454
|
+
return {json, bool, timestamp, key, newKey};
|
|
455
|
+
})
|
|
456
|
+
.filter(({json, bool, timestamp}) => json === true || bool === true || timestamp === true)
|
|
457
|
+
.forEach(({bool, timestamp, key, newKey}) => {
|
|
458
|
+
const value = data[key];
|
|
459
|
+
delete data[key];
|
|
460
|
+
if (StepHelpersUtils.isBlank(value)) {
|
|
461
|
+
if (bool === true) {
|
|
462
|
+
// default value of boolean is false
|
|
463
|
+
data[newKey] = false;
|
|
464
|
+
} else {
|
|
465
|
+
// json is null
|
|
466
|
+
data[newKey] = null;
|
|
467
|
+
}
|
|
468
|
+
} else if (value instanceof Date) {
|
|
469
|
+
data[newKey] = $helpers.$date.dayjs(value).format($helpers.$date.getDateTimeFormat());
|
|
470
|
+
} else if (typeof value === 'string') {
|
|
471
|
+
if (bool === true) {
|
|
472
|
+
data[newKey] = value === '1';
|
|
473
|
+
} else if (timestamp === true) {
|
|
474
|
+
data[newKey] = $helpers.$date.dayjs(value, this.getTimestampReadFormat()).format($helpers.$date.getDateTimeFormat());
|
|
475
|
+
} else {
|
|
476
|
+
// try to parse to json
|
|
477
|
+
data[newKey] = JSON.parse(value);
|
|
478
|
+
}
|
|
479
|
+
} else if (typeof value === 'number') {
|
|
480
|
+
if (bool === true) {
|
|
481
|
+
data[newKey] = value === 1;
|
|
482
|
+
} else {
|
|
483
|
+
// never occurs
|
|
484
|
+
data[newKey] = value;
|
|
485
|
+
}
|
|
486
|
+
} else {
|
|
487
|
+
data[newKey] = value;
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
return data;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* handle insert/update/delete.
|
|
496
|
+
* 1. for mssql, according to the "OUTPUT" clause, there might be returned an object or an object array.
|
|
497
|
+
* 2. for pgsql, the "RETURNING" clause is not handled yet.
|
|
498
|
+
* 3. for oracle, do nothing special.
|
|
499
|
+
* 4. for mysql, try to return the corresponding number of affected rows, or inserted id.
|
|
500
|
+
*/
|
|
501
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
502
|
+
protected parseResult(result: any) {
|
|
503
|
+
if (result == null) {
|
|
504
|
+
return result;
|
|
505
|
+
}
|
|
506
|
+
const datasourceType = this.findDataSourceType();
|
|
507
|
+
const resultType = typeof result;
|
|
508
|
+
switch (true) {
|
|
509
|
+
case ['number', 'bigint', 'string', 'boolean'].includes(resultType) :
|
|
510
|
+
return result;
|
|
511
|
+
case datasourceType === SupportedDataSourceTypes.MYSQL && resultType === 'object':
|
|
512
|
+
// eslint-disable-next-line no-case-declarations
|
|
513
|
+
const rst = result as KindOfMySQLResult;
|
|
514
|
+
if (rst.changedRows != null) {
|
|
515
|
+
// for update
|
|
516
|
+
return rst.changedRows;
|
|
517
|
+
} else if (rst.insertId != null) {
|
|
518
|
+
// for auto increment insert, which not recommended
|
|
519
|
+
// note if insertId exists, affectedRows will be ignored
|
|
520
|
+
return rst.insertId;
|
|
521
|
+
} else if (rst.affectedRows != null) {
|
|
522
|
+
// for insert and delete
|
|
523
|
+
return rst.affectedRows;
|
|
524
|
+
} else {
|
|
525
|
+
// other, such as SELECT
|
|
526
|
+
return rst;
|
|
527
|
+
}
|
|
528
|
+
case datasourceType === SupportedDataSourceTypes.POSTGRES && resultType === 'object':
|
|
529
|
+
switch (result.command) {
|
|
530
|
+
case 'DELETE':
|
|
531
|
+
case 'UPDATE':
|
|
532
|
+
// for UPDATE and DELETE query additionally return number of affected rows
|
|
533
|
+
// noinspection JSUnresolvedReference
|
|
534
|
+
return result.rowCount;
|
|
535
|
+
default:
|
|
536
|
+
// eslint-disable-next-line no-prototype-builtins
|
|
537
|
+
if (result.hasOwnProperty('rows')) {
|
|
538
|
+
// for SELECT
|
|
539
|
+
return result.rows;
|
|
540
|
+
} else {
|
|
541
|
+
// INSERT, an empty array
|
|
542
|
+
return result;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
case datasourceType === SupportedDataSourceTypes.MSSQL && resultType === 'object':
|
|
546
|
+
// MSSQL can return anything via OUTPUT clause
|
|
547
|
+
if (Array.isArray(result)) {
|
|
548
|
+
return result.map(item => this.beautify({data: item, datasourceType}));
|
|
549
|
+
} else {
|
|
550
|
+
return this.beautify({data: result, datasourceType});
|
|
551
|
+
}
|
|
552
|
+
default:
|
|
553
|
+
return result;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {PipelineStepData, PipelineStepPayload, Undefinable} from '@rainbow-o23/n1';
|
|
2
|
+
import {AbstractTypeOrmBySQLPipelineStep, TypeOrmBasis} from './abstract-typeorm-by-sql-step';
|
|
3
|
+
import {TypeOrmEntityToLoad, TypeOrmEntityToSave, TypeOrmEntityValue} from './types';
|
|
4
|
+
|
|
5
|
+
export interface TypeOrmLoadBasis extends TypeOrmBasis {
|
|
6
|
+
params?: Array<TypeOrmEntityValue> | TypeOrmEntityToSave;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export abstract class AbstractTypeOrmLoadBySQLPipelineStep<In = PipelineStepPayload, Out = PipelineStepPayload, OutFragment = Out>
|
|
10
|
+
extends AbstractTypeOrmBySQLPipelineStep<In, Out, Undefinable<TypeOrmLoadBasis>, OutFragment> {
|
|
11
|
+
protected abstract getDataFromResultSet(rst: Array<TypeOrmEntityToLoad>): Promise<OutFragment>;
|
|
12
|
+
|
|
13
|
+
protected async doPerform(basis: Undefinable<TypeOrmLoadBasis>, request: PipelineStepData<In>): Promise<Undefinable<OutFragment>> {
|
|
14
|
+
const {sql, params} = this.getSql(basis, basis?.params);
|
|
15
|
+
return await this.autoTrans<Undefinable<OutFragment>>(async (runner) => {
|
|
16
|
+
const rst = await runner.query(sql, params);
|
|
17
|
+
if (rst == null || rst.length === 0) {
|
|
18
|
+
return (void 0);
|
|
19
|
+
} else {
|
|
20
|
+
const datasourceType = this.findDataSourceType();
|
|
21
|
+
// typeorm already box row to object
|
|
22
|
+
const data = await this.getDataFromResultSet(rst);
|
|
23
|
+
if (Array.isArray(data)) {
|
|
24
|
+
return data.map(item => this.beautify({data: item, datasourceType})) as OutFragment;
|
|
25
|
+
} else {
|
|
26
|
+
return this.beautify({data, datasourceType}) as OutFragment;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}, request);
|
|
30
|
+
}
|
|
31
|
+
}
|