@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,109 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CatchableError,
|
|
3
|
+
O23ExternalErrorCode,
|
|
4
|
+
PipelineStepData,
|
|
5
|
+
PipelineStepPayload,
|
|
6
|
+
Undefinable
|
|
7
|
+
} from '@rainbow-o23/n1';
|
|
8
|
+
import {Snowflake} from '@theinternetfolks/snowflake';
|
|
9
|
+
import {DeepPartial, ObjectLiteral} from 'typeorm';
|
|
10
|
+
import {ScriptFuncOrBody, Utils} from '../step';
|
|
11
|
+
import {AbstractTypeOrmPipelineStep, TypeOrmPipelineStepOptions} from './abstract-typeorm-step';
|
|
12
|
+
import {TypeOrmEntityName} from './types';
|
|
13
|
+
|
|
14
|
+
export type EntityToSave = DeepPartial<ObjectLiteral>;
|
|
15
|
+
|
|
16
|
+
export type UniquenessCheckResult = { pass: true } | { pass: false, code: O23ExternalErrorCode, message: string };
|
|
17
|
+
/**
|
|
18
|
+
* parameter names could be change {@link TypeOrmSaveEntityPipelineStep#generateUniquenessCheckVariableNames}
|
|
19
|
+
*/
|
|
20
|
+
export type UniquenessCheckFunc<EntityToSave> = (given: EntityToSave, existing: EntityToSave) => UniquenessCheckResult;
|
|
21
|
+
|
|
22
|
+
export interface TypeOrmSaveEntityPipelineStepOptions<In = PipelineStepPayload, Out = PipelineStepPayload, InFragment = EntityToSave, OutFragment = EntityToSave>
|
|
23
|
+
extends TypeOrmPipelineStepOptions<In, Out, InFragment, OutFragment> {
|
|
24
|
+
entityName: TypeOrmEntityName;
|
|
25
|
+
fillIdBySnowflake?: boolean;
|
|
26
|
+
uniquenessCheckSnippet?: ScriptFuncOrBody<UniquenessCheckFunc<InFragment>>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* no transaction here
|
|
31
|
+
*/
|
|
32
|
+
export class TypeOrmSaveEntityPipelineStep<In = PipelineStepPayload, Out = PipelineStepPayload, InFragment = EntityToSave, OutFragment = EntityToSave>
|
|
33
|
+
extends AbstractTypeOrmPipelineStep<In, Out, InFragment, OutFragment> {
|
|
34
|
+
private readonly _entityName: TypeOrmEntityName;
|
|
35
|
+
private readonly _fillIdBySnowflake: boolean;
|
|
36
|
+
private readonly _uniquenessCheckSnippet?: ScriptFuncOrBody<UniquenessCheckFunc<InFragment>>;
|
|
37
|
+
private readonly _uniquenessCheckFunc?: UniquenessCheckFunc<InFragment>;
|
|
38
|
+
|
|
39
|
+
public constructor(options: TypeOrmSaveEntityPipelineStepOptions<In, Out, InFragment, OutFragment>) {
|
|
40
|
+
super(options);
|
|
41
|
+
this._entityName = options.entityName;
|
|
42
|
+
this._fillIdBySnowflake = options.fillIdBySnowflake ?? false;
|
|
43
|
+
this._uniquenessCheckSnippet = options.uniquenessCheckSnippet;
|
|
44
|
+
this._uniquenessCheckFunc = Utils.createSyncFunction(this.getUniquenessCheckSnippet(), {
|
|
45
|
+
createDefault: () => (void 0),
|
|
46
|
+
getVariableNames: () => this.generateUniquenessCheckVariableNames(),
|
|
47
|
+
error: (e: Error) => {
|
|
48
|
+
this.getLogger().error(`Failed on create function for uniqueness check, snippet is [${this.getUniquenessCheckSnippet()}].`);
|
|
49
|
+
throw e;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public getEntityName(): TypeOrmEntityName {
|
|
55
|
+
return this._entityName;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public isFillIdBySnowflake(): boolean {
|
|
59
|
+
return this._fillIdBySnowflake;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public getUniquenessCheckSnippet(): Undefinable<ScriptFuncOrBody<UniquenessCheckFunc<InFragment>>> {
|
|
63
|
+
return this._uniquenessCheckSnippet;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public needUniquenessCheck(): boolean {
|
|
67
|
+
return this._uniquenessCheckFunc != null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
protected generateUniquenessCheckVariableNames(): Array<string> {
|
|
71
|
+
return ['given', 'existing'];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 1. check uniqueness of given entity,
|
|
76
|
+
* - only when need it, see {@link needUniquenessCheck}
|
|
77
|
+
* - load entity by id, which get from given entity,
|
|
78
|
+
* - compare given and existing by uniqueness check snippet (see {@link _uniquenessCheckFunc}), only when existing loaded from database,
|
|
79
|
+
* - throw catchable error if failed on uniqueness check,
|
|
80
|
+
* 2. generate snowflake id and set into given entity,
|
|
81
|
+
* - only when need it, see {@link isFillIdBySnowflake}
|
|
82
|
+
* 3. save it.
|
|
83
|
+
*/
|
|
84
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
85
|
+
protected async doPerform(entity: InFragment, request: PipelineStepData<In>): Promise<OutFragment> {
|
|
86
|
+
const [, column, repository] = await this.findMetadata(this.getEntityName(), request);
|
|
87
|
+
if (this.needUniquenessCheck()) {
|
|
88
|
+
const id = entity[column.propertyName];
|
|
89
|
+
if (`${id ?? ''}`.trim().length !== 0) {
|
|
90
|
+
const existing = await repository.findOneBy({[column.propertyName]: id}) as InFragment;
|
|
91
|
+
if (existing != null) {
|
|
92
|
+
const checked = this._uniquenessCheckFunc(entity, existing);
|
|
93
|
+
if (checked.pass !== true) {
|
|
94
|
+
throw new CatchableError(checked.code, checked.message);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (this.isFillIdBySnowflake()) {
|
|
100
|
+
// fill id if not exists
|
|
101
|
+
const id = entity[column.propertyName];
|
|
102
|
+
if (id == null || `${id ?? ''}`.trim().length === 0) {
|
|
103
|
+
entity[column.propertyName] = Snowflake.generate() as unknown as number;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return await repository.save(entity) as OutFragment;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {DeepPartial, ObjectLiteral, QueryRunner} from 'typeorm';
|
|
2
|
+
import {PipelineStepSetsContext} from '../step';
|
|
3
|
+
import {TypeOrmDataSource} from '../typeorm';
|
|
4
|
+
|
|
5
|
+
export type TypeOrmIdType = string | number | bigint;
|
|
6
|
+
export type TypeOrmDataSourceName = string;
|
|
7
|
+
export type TypeOrmTransactionName = string;
|
|
8
|
+
export type TypeOrmTransactionKey = `${TypeOrmDataSourceName}.${TypeOrmTransactionName}`;
|
|
9
|
+
export type TypeOrmEntityName = string;
|
|
10
|
+
export type TypeOrmEntityValue = string | number | bigint | boolean | Date | null | undefined;
|
|
11
|
+
export type TypeOrmEntityToLoad = DeepPartial<ObjectLiteral>;
|
|
12
|
+
export type TypeOrmEntityToSave = DeepPartial<ObjectLiteral>;
|
|
13
|
+
export type TypeOrmSql = string;
|
|
14
|
+
export type TypeOrmIdOfInserted = TypeOrmIdType;
|
|
15
|
+
export type TypeOrmIdsOfInserted = Array<TypeOrmIdOfInserted>;
|
|
16
|
+
export type TypeOrmCountOfAffected = number;
|
|
17
|
+
export type TypeOrmCountsOfAffected = Array<TypeOrmCountOfAffected>;
|
|
18
|
+
export type TypeOrmWrittenResult = TypeOrmIdOfInserted | TypeOrmCountOfAffected;
|
|
19
|
+
export type TypeOrmBulkWrittenResult = TypeOrmIdsOfInserted | TypeOrmCountsOfAffected;
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_TRANSACTION_NAME: TypeOrmTransactionName = '$default-transaction';
|
|
22
|
+
|
|
23
|
+
export interface TypeOrmTransactionalContext extends PipelineStepSetsContext {
|
|
24
|
+
$trans: Record<TypeOrmTransactionKey, [TypeOrmDataSource, QueryRunner]>;
|
|
25
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {createConfig, createLogger} from '@rainbow-o23/n1';
|
|
2
|
+
import {SnippetPipelineStep} from '../../src';
|
|
3
|
+
|
|
4
|
+
const logger = createLogger();
|
|
5
|
+
const config = createConfig(logger);
|
|
6
|
+
|
|
7
|
+
test('Snippet Pipeline Step Test #1, + 100', async () => {
|
|
8
|
+
const snippet = 'return $factor.base + 100;';
|
|
9
|
+
const step = new SnippetPipelineStep({config, logger, snippet});
|
|
10
|
+
const request = {content: {base: 1}};
|
|
11
|
+
const response = await step.perform(request);
|
|
12
|
+
expect(response.content).toEqual(101);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('Snippet Pipeline Step Test #2, async + 100', async () => {
|
|
16
|
+
const snippet = 'return await new Promise(resolve => resolve($factor.base + 100));';
|
|
17
|
+
const step = new SnippetPipelineStep({config, logger, snippet});
|
|
18
|
+
const request = {content: {base: 1}};
|
|
19
|
+
const response = await step.perform(request);
|
|
20
|
+
expect(response.content).toEqual(101);
|
|
21
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {createConfig, createLogger} from '@rainbow-o23/n1';
|
|
2
|
+
import {SnowflakePipelineStep} from '../../src';
|
|
3
|
+
|
|
4
|
+
const logger = createLogger();
|
|
5
|
+
const config = createConfig(logger);
|
|
6
|
+
|
|
7
|
+
test('Snowflake Pipeline Step Test #1, replace content', async () => {
|
|
8
|
+
const step = new SnowflakePipelineStep({config, logger, mergeRequest: '$id'});
|
|
9
|
+
const request = {content: (void 0)};
|
|
10
|
+
const response = await step.perform(request);
|
|
11
|
+
expect(response.content).not.toBeNull();
|
|
12
|
+
expect(response.content.$id).not.toBeNull();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('Snowflake Pipeline Step Test #2, merge content', async () => {
|
|
16
|
+
const step = new SnowflakePipelineStep({config, logger, mergeRequest: '$id'});
|
|
17
|
+
const request = {content: {base: 1}};
|
|
18
|
+
const response = await step.perform(request);
|
|
19
|
+
expect(response.content).not.toBeNull();
|
|
20
|
+
expect(response.content.base).toEqual(1);
|
|
21
|
+
expect(response.content.$id).not.toBeNull();
|
|
22
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import {createConfig, createLogger} from '@rainbow-o23/n1';
|
|
2
|
+
import {Column, Entity, PrimaryColumn} from 'typeorm';
|
|
3
|
+
import {
|
|
4
|
+
TypeOrmBulkSaveBySQLPipelineStep,
|
|
5
|
+
TypeOrmDataSourceHelper,
|
|
6
|
+
TypeOrmDataSourceManager,
|
|
7
|
+
TypeOrmLoadManyBySQLPipelineStep,
|
|
8
|
+
TypeOrmLoadOneBySQLPipelineStep,
|
|
9
|
+
TypeOrmSaveBySQLPipelineStep
|
|
10
|
+
} from '../../src';
|
|
11
|
+
|
|
12
|
+
const logger = createLogger();
|
|
13
|
+
const config = createConfig(logger);
|
|
14
|
+
|
|
15
|
+
// decorators are leading syntax errors in ide, since the test folder is not included in tsconfig.json
|
|
16
|
+
// but the tricky thing is, once test folder included into tsconfig.json, d.ts files will be created in src folder
|
|
17
|
+
// which cause currently use ts-ignore to avoid this syntax errors
|
|
18
|
+
// @ts-ignore
|
|
19
|
+
@Entity({name: 'T_TEST_TABLE'})
|
|
20
|
+
export class TestTable {
|
|
21
|
+
// @ts-ignore
|
|
22
|
+
@PrimaryColumn('bigint', {name: 'ID'})
|
|
23
|
+
id: number;
|
|
24
|
+
// @ts-ignore
|
|
25
|
+
@Column('varchar', {name: 'CONTENT'})
|
|
26
|
+
content: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('TypeORM SQL Autonomous Suite', () => {
|
|
30
|
+
beforeAll(async () => {
|
|
31
|
+
process.env.CFG_TYPEORM_TEST_TYPE = 'better-sqlite3';
|
|
32
|
+
process.env.CFG_TYPEORM_TEST_SYNCHRONIZE = 'true';
|
|
33
|
+
// in memory, kept in global cache, to make sure always use same one.
|
|
34
|
+
process.env.CFG_TYPEORM_TEST_KEPT_ON_GLOBAL = 'true';
|
|
35
|
+
// process.env.CFG_TYPEORM_TEST_LOGGING = 'true';
|
|
36
|
+
await new TypeOrmDataSourceHelper(config).create({
|
|
37
|
+
'TEST': [TestTable]
|
|
38
|
+
});
|
|
39
|
+
const repo = (await TypeOrmDataSourceManager.findDataSource('TEST')).getDataSource().getRepository(TestTable);
|
|
40
|
+
await repo.insert({id: 1, content: 'hello world!'});
|
|
41
|
+
await repo.insert({id: 2, content: 'good-bye world!'});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('TypeORM load one by sql Pipeline Step Test #1', async () => {
|
|
45
|
+
// noinspection SqlResolve
|
|
46
|
+
const step = new TypeOrmLoadOneBySQLPipelineStep({
|
|
47
|
+
config, logger, dataSourceName: 'TEST', sql: 'SELECT ID id, CONTENT content FROM T_TEST_TABLE WHERE ID = ?',
|
|
48
|
+
autonomous: true
|
|
49
|
+
});
|
|
50
|
+
const request = {content: {params: [1]}};
|
|
51
|
+
const response = await step.perform(request);
|
|
52
|
+
expect(response.content).not.toBeNull();
|
|
53
|
+
expect(response.content.id).toBe(1);
|
|
54
|
+
expect(response.content.content).toBe('hello world!');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('TypeORM insert one by sql Pipeline Step Test #1', async () => {
|
|
58
|
+
// noinspection SqlResolve
|
|
59
|
+
const step = new TypeOrmSaveBySQLPipelineStep({
|
|
60
|
+
config, logger, dataSourceName: 'TEST', sql: 'INSERT INTO T_TEST_TABLE(ID, CONTENT) VALUES (?, ?)',
|
|
61
|
+
autonomous: true, mergeRequest: 'id'
|
|
62
|
+
});
|
|
63
|
+
const request = {content: {values: [3, 'another world!']}};
|
|
64
|
+
const response = await step.perform(request);
|
|
65
|
+
expect(response.content).not.toBeNull();
|
|
66
|
+
expect(response.content.id).toBe(3);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('TypeORM update one by sql Pipeline Step Test #1', async () => {
|
|
70
|
+
// noinspection SqlResolve
|
|
71
|
+
const step = new TypeOrmSaveBySQLPipelineStep({
|
|
72
|
+
config, logger, dataSourceName: 'TEST', sql: 'UPDATE T_TEST_TABLE SET CONTENT = ? WHERE ID = ?',
|
|
73
|
+
autonomous: true, mergeRequest: 'count'
|
|
74
|
+
});
|
|
75
|
+
const request = {content: {values: ['world #3!', 3]}};
|
|
76
|
+
const response = await step.perform(request);
|
|
77
|
+
expect(response.content).not.toBeNull();
|
|
78
|
+
// DON'T KNOW WHY THIS IS 3, SEEMS SHOULD BE 1 ACCORDING TO BETTER-SQLITE3 DOCUMENT
|
|
79
|
+
// BUT CURRENTLY IT RETURNS COUNT OF THIS TABLE, NOT IMPACTED ROW COUNT
|
|
80
|
+
expect(response.content.count).toBe(3);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('TypeORM insert many by sql Pipeline Step Test #1', async () => {
|
|
84
|
+
// noinspection SqlResolve
|
|
85
|
+
const step = new TypeOrmBulkSaveBySQLPipelineStep({
|
|
86
|
+
config, logger, dataSourceName: 'TEST', sql: 'INSERT INTO T_TEST_TABLE(ID, CONTENT) VALUES (?, ?)',
|
|
87
|
+
autonomous: true, mergeRequest: 'ids'
|
|
88
|
+
});
|
|
89
|
+
const request = {
|
|
90
|
+
content: {
|
|
91
|
+
items: [[4, 'world #4!'], [5, 'world #5!']]
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
const response = await step.perform(request);
|
|
95
|
+
expect(response.content).not.toBeNull();
|
|
96
|
+
expect(response.content.ids.length).toBe(2);
|
|
97
|
+
expect(response.content.ids[0]).toBe(4);
|
|
98
|
+
expect(response.content.ids[1]).toBe(5);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('TypeORM load many by sql Pipeline Step Test #1', async () => {
|
|
102
|
+
// noinspection SqlResolve
|
|
103
|
+
const step = new TypeOrmLoadManyBySQLPipelineStep<any, Array<TestTable>>({
|
|
104
|
+
config, logger, dataSourceName: 'TEST', sql: 'SELECT ID id, CONTENT content FROM T_TEST_TABLE',
|
|
105
|
+
autonomous: true
|
|
106
|
+
});
|
|
107
|
+
const request = {content: (void 0)};
|
|
108
|
+
const response = await step.perform(request);
|
|
109
|
+
expect(response.content).not.toBeNull();
|
|
110
|
+
expect(Array.isArray(response.content)).toBeTruthy();
|
|
111
|
+
expect(response.content[2].content).toBe('world #3!');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
afterAll((done) => {
|
|
115
|
+
done();
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AbstractStaticPipeline,
|
|
3
|
+
createConfig,
|
|
4
|
+
createLogger,
|
|
5
|
+
PipelineCode,
|
|
6
|
+
PipelineStepData,
|
|
7
|
+
PipelineStepType,
|
|
8
|
+
Undefinable
|
|
9
|
+
} from '@rainbow-o23/n1';
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
import {Column, Entity, PrimaryColumn} from 'typeorm';
|
|
13
|
+
import {
|
|
14
|
+
TypeOrmBasis,
|
|
15
|
+
TypeOrmBulkSaveBySQLPipelineStep,
|
|
16
|
+
TypeOrmCountOfAffected,
|
|
17
|
+
TypeOrmDataSourceHelper,
|
|
18
|
+
TypeOrmDataSourceManager,
|
|
19
|
+
TypeOrmIdOfInserted,
|
|
20
|
+
TypeOrmIdsOfInserted,
|
|
21
|
+
TypeOrmLoadManyBySQLPipelineStep,
|
|
22
|
+
TypeOrmLoadOneBySQLPipelineStep,
|
|
23
|
+
TypeOrmSaveBySQLPipelineStep,
|
|
24
|
+
TypeOrmTransactionalPipelineStepSets,
|
|
25
|
+
TypeOrmTransactionalPipelineStepSetsOptions
|
|
26
|
+
} from '../../src';
|
|
27
|
+
|
|
28
|
+
const logger = createLogger();
|
|
29
|
+
const config = createConfig(logger);
|
|
30
|
+
|
|
31
|
+
// decorators are leading syntax errors in ide, since the test folder is not included in tsconfig.json
|
|
32
|
+
// but the tricky thing is, once test folder included into tsconfig.json, d.ts files will be created in src folder
|
|
33
|
+
// which cause currently use ts-ignore to avoid this syntax errors
|
|
34
|
+
// @ts-ignore
|
|
35
|
+
@Entity({name: 'T_TEST_TABLE'})
|
|
36
|
+
export class TestTable {
|
|
37
|
+
// @ts-ignore
|
|
38
|
+
@PrimaryColumn('bigint', {name: 'ID'})
|
|
39
|
+
id: number;
|
|
40
|
+
// @ts-ignore
|
|
41
|
+
@Column('varchar', {name: 'CONTENT'})
|
|
42
|
+
content: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface TransactionalRequest {
|
|
46
|
+
idToLoad: number,
|
|
47
|
+
item3: TestTable;
|
|
48
|
+
item3ChangeTo: TestTable;
|
|
49
|
+
item4: TestTable;
|
|
50
|
+
item5: TestTable;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface TransactionalResponse extends TransactionalRequest {
|
|
54
|
+
loadedById: TestTable;
|
|
55
|
+
insertedId: TypeOrmIdOfInserted;
|
|
56
|
+
updatedCount: TypeOrmCountOfAffected;
|
|
57
|
+
insertedIds: TypeOrmIdsOfInserted;
|
|
58
|
+
all: Array<TestTable>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
class TransactionalTestStepSets extends TypeOrmTransactionalPipelineStepSets {
|
|
62
|
+
constructor(options: Omit<TypeOrmTransactionalPipelineStepSetsOptions, 'dataSourceName'>) {
|
|
63
|
+
// noinspection SqlResolve
|
|
64
|
+
super({
|
|
65
|
+
...options, dataSourceName: 'TEST',
|
|
66
|
+
steps: [
|
|
67
|
+
{
|
|
68
|
+
create: async ({config, logger}) => new TypeOrmLoadOneBySQLPipelineStep({
|
|
69
|
+
config,
|
|
70
|
+
logger,
|
|
71
|
+
dataSourceName: 'TEST',
|
|
72
|
+
sql: 'SELECT ID id, CONTENT content FROM T_TEST_TABLE WHERE ID = ?',
|
|
73
|
+
fromRequest: ($factor: TransactionalRequest, _$request: PipelineStepData<TransactionalRequest>) => {
|
|
74
|
+
return {params: [$factor.idToLoad]} as TypeOrmBasis;
|
|
75
|
+
},
|
|
76
|
+
toResponse: ($result: TestTable, _$request: PipelineStepData) => {
|
|
77
|
+
expect($result.id).toBe(1);
|
|
78
|
+
expect($result.content).toBe('hello world!');
|
|
79
|
+
return {loadedById: $result};
|
|
80
|
+
},
|
|
81
|
+
mergeRequest: true
|
|
82
|
+
})
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
create: async ({config, logger}) => new TypeOrmSaveBySQLPipelineStep({
|
|
86
|
+
config,
|
|
87
|
+
logger,
|
|
88
|
+
dataSourceName: 'TEST',
|
|
89
|
+
sql: 'INSERT INTO T_TEST_TABLE(ID, CONTENT) VALUES (?, ?)',
|
|
90
|
+
fromRequest: ($factor: TransactionalRequest, _$request: PipelineStepData<TransactionalRequest>) => {
|
|
91
|
+
return {values: [$factor.item3.id, $factor.item3.content]};
|
|
92
|
+
},
|
|
93
|
+
toResponse: ($result: TypeOrmIdOfInserted, _$request: PipelineStepData) => {
|
|
94
|
+
expect($result).toBe(3);
|
|
95
|
+
return {insertedId: $result};
|
|
96
|
+
},
|
|
97
|
+
mergeRequest: true
|
|
98
|
+
})
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
create: async ({config, logger}) => new TypeOrmLoadOneBySQLPipelineStep({
|
|
102
|
+
config,
|
|
103
|
+
logger,
|
|
104
|
+
dataSourceName: 'TEST',
|
|
105
|
+
autonomous: true,
|
|
106
|
+
sql: 'SELECT ID id, CONTENT content FROM T_TEST_TABLE WHERE ID = ?',
|
|
107
|
+
fromRequest: ($factor: TransactionalRequest, _$request: PipelineStepData<TransactionalRequest>) => {
|
|
108
|
+
return {params: [$factor.item3.id]};
|
|
109
|
+
},
|
|
110
|
+
toResponse: ($result: Undefinable<TestTable>, $request: PipelineStepData) => {
|
|
111
|
+
expect($result).toBeUndefined();
|
|
112
|
+
return $request.content;
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
create: async ({config, logger}) => new TypeOrmSaveBySQLPipelineStep({
|
|
118
|
+
config, logger, dataSourceName: 'TEST', sql: 'UPDATE T_TEST_TABLE SET CONTENT = ? WHERE ID = ?',
|
|
119
|
+
fromRequest: ($factor: TransactionalRequest, _$request: PipelineStepData<TransactionalRequest>) => {
|
|
120
|
+
return {values: [$factor.item3ChangeTo.content, $factor.item3ChangeTo.id]};
|
|
121
|
+
},
|
|
122
|
+
toResponse: ($result: TypeOrmCountOfAffected, _$request: PipelineStepData) => {
|
|
123
|
+
// DON'T KNOW WHY THIS IS 3, SEEMS SHOULD BE 1 ACCORDING TO BETTER-SQLITE3 DOCUMENT
|
|
124
|
+
// BUT CURRENTLY IT RETURNS COUNT OF THIS TABLE, NOT IMPACTED ROW COUNT
|
|
125
|
+
expect($result).toBe(3);
|
|
126
|
+
return {updatedCount: $result};
|
|
127
|
+
},
|
|
128
|
+
mergeRequest: true
|
|
129
|
+
})
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
create: async ({config, logger}) => new TypeOrmBulkSaveBySQLPipelineStep({
|
|
133
|
+
config,
|
|
134
|
+
logger,
|
|
135
|
+
dataSourceName: 'TEST',
|
|
136
|
+
sql: 'INSERT INTO T_TEST_TABLE(ID, CONTENT) VALUES (?, ?)',
|
|
137
|
+
fromRequest: ($factor: TransactionalRequest, _$request: PipelineStepData<TransactionalRequest>) => {
|
|
138
|
+
return {
|
|
139
|
+
items: [
|
|
140
|
+
[$factor.item4.id, $factor.item4.content], [$factor.item5.id, $factor.item5.content]
|
|
141
|
+
]
|
|
142
|
+
};
|
|
143
|
+
},
|
|
144
|
+
toResponse: ($result: TypeOrmIdsOfInserted, _$request: PipelineStepData) => {
|
|
145
|
+
return {insertedIds: $result};
|
|
146
|
+
},
|
|
147
|
+
mergeRequest: true
|
|
148
|
+
})
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
create: async ({config, logger}) => new TypeOrmLoadManyBySQLPipelineStep({
|
|
152
|
+
config, logger, dataSourceName: 'TEST', sql: 'SELECT ID id, CONTENT content FROM T_TEST_TABLE',
|
|
153
|
+
toResponse: ($result: Array<TestTable>, _$request: PipelineStepData) => {
|
|
154
|
+
return {all: $result};
|
|
155
|
+
},
|
|
156
|
+
mergeRequest: true
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
]
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
class TransactionalPipeline extends AbstractStaticPipeline<TransactionalRequest, TransactionalResponse> {
|
|
165
|
+
public getCode(): PipelineCode {
|
|
166
|
+
return 'TransactionalPipeline';
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
protected getStepTypes(): Array<PipelineStepType> {
|
|
170
|
+
return [TransactionalTestStepSets];
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const filename = 'TypeORM SQL Transactional.db';
|
|
175
|
+
describe('TypeORM SQL Transactional Suite', () => {
|
|
176
|
+
beforeAll(async () => {
|
|
177
|
+
if (fs.existsSync(path.resolve(filename))) {
|
|
178
|
+
fs.rmSync(path.resolve(filename));
|
|
179
|
+
}
|
|
180
|
+
process.env.CFG_TYPEORM_TEST_TYPE = 'better-sqlite3';
|
|
181
|
+
process.env.CFG_TYPEORM_TEST_SYNCHRONIZE = 'true';
|
|
182
|
+
// process.env.CFG_TYPEORM_TEST_LOGGING = 'true';
|
|
183
|
+
// cannot use in memory, since when cache it, only one connection exists,
|
|
184
|
+
// when don't cache it, doesn't share the same memory.
|
|
185
|
+
// therefore use file
|
|
186
|
+
process.env.CFG_TYPEORM_TEST_DATABASE = filename;
|
|
187
|
+
await new TypeOrmDataSourceHelper(config).create({
|
|
188
|
+
'TEST': [TestTable]
|
|
189
|
+
});
|
|
190
|
+
const repo = (await TypeOrmDataSourceManager.findDataSource('TEST')).getDataSource().getRepository(TestTable);
|
|
191
|
+
await repo.insert({id: 1, content: 'hello world!'});
|
|
192
|
+
await repo.insert({id: 2, content: 'good-bye world!'});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test('TypeORM transactional by sql Pipeline Step Test #1', async () => {
|
|
196
|
+
const pipeline = new TransactionalPipeline({config, logger});
|
|
197
|
+
const response = await pipeline.perform({
|
|
198
|
+
payload: {
|
|
199
|
+
idToLoad: 1,
|
|
200
|
+
item3: {id: 3, content: 'another world!'},
|
|
201
|
+
item3ChangeTo: {id: 3, content: 'world #3!'},
|
|
202
|
+
item4: {id: 4, content: 'world #4'},
|
|
203
|
+
item5: {id: 5, content: 'world #5'}
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
expect(response).not.toBeNull();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
afterAll((done) => {
|
|
210
|
+
if (fs.existsSync(path.resolve(filename))) {
|
|
211
|
+
fs.rmSync(path.resolve(filename));
|
|
212
|
+
}
|
|
213
|
+
done();
|
|
214
|
+
});
|
|
215
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {createConfig, createLogger} from '@rainbow-o23/n1';
|
|
2
|
+
import {Column, Entity, PrimaryColumn} from 'typeorm';
|
|
3
|
+
import {TypeOrmDataSourceHelper, TypeOrmDataSourceManager, TypeOrmLoadEntityByIdPipelineStep} from '../../src';
|
|
4
|
+
|
|
5
|
+
const logger = createLogger();
|
|
6
|
+
const config = createConfig(logger);
|
|
7
|
+
|
|
8
|
+
// decorators are leading syntax errors in ide, since the test folder is not included in tsconfig.json
|
|
9
|
+
// but the tricky thing is, once test folder included into tsconfig.json, d.ts files will be created in src folder
|
|
10
|
+
// which cause currently use ts-ignore to avoid this syntax errors
|
|
11
|
+
// @ts-ignore
|
|
12
|
+
@Entity({name: 'T_TEST_TABLE'})
|
|
13
|
+
export class TestTable {
|
|
14
|
+
// @ts-ignore
|
|
15
|
+
@PrimaryColumn('bigint', {name: 'ID'})
|
|
16
|
+
id: number;
|
|
17
|
+
// @ts-ignore
|
|
18
|
+
@Column('varchar', {name: 'CONTENT'})
|
|
19
|
+
content: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('TypeORM Entity Suite', () => {
|
|
23
|
+
beforeAll(async () => {
|
|
24
|
+
process.env.CFG_TYPEORM_TEST_TYPE = 'better-sqlite3';
|
|
25
|
+
process.env.CFG_TYPEORM_TEST_SYNCHRONIZE = 'true';
|
|
26
|
+
// in memory, kept in global cache, to make sure always use same one.
|
|
27
|
+
process.env.CFG_TYPEORM_TEST_KEPT_ON_GLOBAL = 'true';
|
|
28
|
+
await new TypeOrmDataSourceHelper(config).create({
|
|
29
|
+
'TEST': [TestTable]
|
|
30
|
+
});
|
|
31
|
+
const repo = (await TypeOrmDataSourceManager.findDataSource('TEST')).getDataSource().getRepository(TestTable);
|
|
32
|
+
await repo.insert({id: 1, content: 'hello world!'});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('TypeORM load entity by id Pipeline Step Test #1', async () => {
|
|
36
|
+
const step = new TypeOrmLoadEntityByIdPipelineStep({
|
|
37
|
+
config, logger, dataSourceName: 'TEST', entityName: 'TestTable',
|
|
38
|
+
autonomous: true
|
|
39
|
+
});
|
|
40
|
+
const request = {content: 1};
|
|
41
|
+
const response = await step.perform(request);
|
|
42
|
+
expect(response.content).not.toBeNull();
|
|
43
|
+
expect(response.content.id).toBe(1);
|
|
44
|
+
expect(response.content.content).toBe('hello world!');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterAll((done) => {
|
|
48
|
+
done();
|
|
49
|
+
});
|
|
50
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"baseUrl": ".",
|
|
4
|
+
"outDir": "./dist",
|
|
5
|
+
"declarationDir": "./lib",
|
|
6
|
+
"module": "ES2020",
|
|
7
|
+
"target": "ESNext",
|
|
8
|
+
"moduleResolution": "node",
|
|
9
|
+
"sourceMap": true,
|
|
10
|
+
// create d.ts file
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"removeComments": true,
|
|
13
|
+
"noImplicitAny": false,
|
|
14
|
+
"allowSyntheticDefaultImports": true,
|
|
15
|
+
"allowUnreachableCode": true,
|
|
16
|
+
"allowUnusedLabels": false,
|
|
17
|
+
"alwaysStrict": true,
|
|
18
|
+
"allowJs": true,
|
|
19
|
+
"experimentalDecorators": true,
|
|
20
|
+
"lib": [
|
|
21
|
+
"es5",
|
|
22
|
+
"es2015",
|
|
23
|
+
"es2016",
|
|
24
|
+
"es2017",
|
|
25
|
+
"es2018",
|
|
26
|
+
"dom"
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
"include": [
|
|
30
|
+
"src/**/*"
|
|
31
|
+
],
|
|
32
|
+
"exclude": [
|
|
33
|
+
"node_modules",
|
|
34
|
+
"dist"
|
|
35
|
+
]
|
|
36
|
+
}
|