@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.
Files changed (97) hide show
  1. package/.babelrc +11 -0
  2. package/.eslintrc +23 -0
  3. package/README.md +584 -0
  4. package/index.cjs +2283 -0
  5. package/index.d.ts +5 -0
  6. package/index.js +2229 -0
  7. package/lib/error-codes.d.ts +17 -0
  8. package/lib/http/fetch-step.d.ts +53 -0
  9. package/lib/http/index.d.ts +2 -0
  10. package/lib/http/types.d.ts +21 -0
  11. package/lib/step/abstract-fragmentary-pipeline-step.d.ts +44 -0
  12. package/lib/step/async-step-sets.d.ts +5 -0
  13. package/lib/step/conditional-step-sets.d.ts +23 -0
  14. package/lib/step/delete-property-step.d.ts +11 -0
  15. package/lib/step/each-step-sets.d.ts +14 -0
  16. package/lib/step/get-property-step.d.ts +11 -0
  17. package/lib/step/index.d.ts +14 -0
  18. package/lib/step/ref-pipeline-step.d.ts +15 -0
  19. package/lib/step/ref-step-step.d.ts +14 -0
  20. package/lib/step/routes-step-sets.d.ts +30 -0
  21. package/lib/step/snippet-step.d.ts +19 -0
  22. package/lib/step/snowflake-step.d.ts +6 -0
  23. package/lib/step/step-sets.d.ts +19 -0
  24. package/lib/step/types.d.ts +13 -0
  25. package/lib/step/utils.d.ts +23 -0
  26. package/lib/typeorm/abstract-datasource.d.ts +22 -0
  27. package/lib/typeorm/better-sqlite3-datasource.d.ts +7 -0
  28. package/lib/typeorm/datasource-manager.d.ts +25 -0
  29. package/lib/typeorm/index.d.ts +7 -0
  30. package/lib/typeorm/mssql-datasource.d.ts +6 -0
  31. package/lib/typeorm/mysql-datasource.d.ts +6 -0
  32. package/lib/typeorm/oracle-datasource.d.ts +6 -0
  33. package/lib/typeorm/pgsql-datasource.d.ts +6 -0
  34. package/lib/typeorm-step/abstract-typeorm-by-sql-step.d.ts +62 -0
  35. package/lib/typeorm-step/abstract-typeorm-load-by-sql-step.d.ts +10 -0
  36. package/lib/typeorm-step/abstract-typeorm-step.d.ts +30 -0
  37. package/lib/typeorm-step/index.d.ts +12 -0
  38. package/lib/typeorm-step/type-orm-transactional-step-sets.d.ts +22 -0
  39. package/lib/typeorm-step/typeorm-bulk-save-by-sql-step.d.ts +9 -0
  40. package/lib/typeorm-step/typeorm-by-snippet-step.d.ts +21 -0
  41. package/lib/typeorm-step/typeorm-load-entity-by-id-step.d.ts +12 -0
  42. package/lib/typeorm-step/typeorm-load-many-by-sql-step.d.ts +6 -0
  43. package/lib/typeorm-step/typeorm-load-one-by-sql-step.d.ts +6 -0
  44. package/lib/typeorm-step/typeorm-save-by-sql-step.d.ts +9 -0
  45. package/lib/typeorm-step/typeorm-save-entity-step.d.ts +32 -0
  46. package/lib/typeorm-step/types.d.ts +22 -0
  47. package/package.json +74 -0
  48. package/rollup.config.base.js +33 -0
  49. package/rollup.config.ci.js +3 -0
  50. package/rollup.config.js +3 -0
  51. package/src/index.ts +7 -0
  52. package/src/lib/error-codes.ts +18 -0
  53. package/src/lib/http/fetch-step.ts +290 -0
  54. package/src/lib/http/index.ts +3 -0
  55. package/src/lib/http/types.ts +33 -0
  56. package/src/lib/step/abstract-fragmentary-pipeline-step.ts +367 -0
  57. package/src/lib/step/async-step-sets.ts +14 -0
  58. package/src/lib/step/conditional-step-sets.ts +115 -0
  59. package/src/lib/step/delete-property-step.ts +33 -0
  60. package/src/lib/step/each-step-sets.ts +80 -0
  61. package/src/lib/step/get-property-step.ts +34 -0
  62. package/src/lib/step/index.ts +18 -0
  63. package/src/lib/step/ref-pipeline-step.ts +65 -0
  64. package/src/lib/step/ref-step-step.ts +59 -0
  65. package/src/lib/step/routes-step-sets.ts +147 -0
  66. package/src/lib/step/snippet-step.ts +80 -0
  67. package/src/lib/step/snowflake-step.ts +16 -0
  68. package/src/lib/step/step-sets.ts +99 -0
  69. package/src/lib/step/types.ts +23 -0
  70. package/src/lib/step/utils.ts +109 -0
  71. package/src/lib/typeorm/abstract-datasource.ts +58 -0
  72. package/src/lib/typeorm/better-sqlite3-datasource.ts +29 -0
  73. package/src/lib/typeorm/datasource-manager.ts +104 -0
  74. package/src/lib/typeorm/index.ts +9 -0
  75. package/src/lib/typeorm/mssql-datasource.ts +131 -0
  76. package/src/lib/typeorm/mysql-datasource.ts +35 -0
  77. package/src/lib/typeorm/oracle-datasource.ts +27 -0
  78. package/src/lib/typeorm/pgsql-datasource.ts +25 -0
  79. package/src/lib/typeorm-step/abstract-typeorm-by-sql-step.ts +556 -0
  80. package/src/lib/typeorm-step/abstract-typeorm-load-by-sql-step.ts +31 -0
  81. package/src/lib/typeorm-step/abstract-typeorm-step.ts +241 -0
  82. package/src/lib/typeorm-step/index.ts +17 -0
  83. package/src/lib/typeorm-step/type-orm-transactional-step-sets.ts +129 -0
  84. package/src/lib/typeorm-step/typeorm-bulk-save-by-sql-step.ts +29 -0
  85. package/src/lib/typeorm-step/typeorm-by-snippet-step.ts +83 -0
  86. package/src/lib/typeorm-step/typeorm-load-entity-by-id-step.ts +35 -0
  87. package/src/lib/typeorm-step/typeorm-load-many-by-sql-step.ts +13 -0
  88. package/src/lib/typeorm-step/typeorm-load-one-by-sql-step.ts +13 -0
  89. package/src/lib/typeorm-step/typeorm-save-by-sql-step.ts +24 -0
  90. package/src/lib/typeorm-step/typeorm-save-entity-step.ts +109 -0
  91. package/src/lib/typeorm-step/types.ts +25 -0
  92. package/test/step/snippet-step.test.ts +21 -0
  93. package/test/step/snowflake-step.test.ts +22 -0
  94. package/test/step/typeorm-by-sql-autonomous.test.ts +117 -0
  95. package/test/step/typeorm-by-sql-transactional.test.ts +215 -0
  96. package/test/step/typeorm-entity.test.ts +50 -0
  97. 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
+ }