@hahnpro/flow-sdk 9.6.4 → 2025.2.0-beta.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/CHANGELOG.md +904 -0
- package/jest.config.ts +10 -0
- package/package.json +7 -20
- package/project.json +41 -0
- package/src/index.ts +15 -0
- package/src/lib/ContextManager.ts +111 -0
- package/src/lib/FlowApplication.ts +659 -0
- package/src/lib/FlowElement.ts +220 -0
- package/src/lib/FlowEvent.ts +73 -0
- package/src/lib/FlowLogger.ts +131 -0
- package/src/lib/FlowModule.ts +18 -0
- package/src/lib/RpcClient.ts +99 -0
- package/src/lib/TestModule.ts +14 -0
- package/src/lib/__pycache__/rpc_server.cpython-310.pyc +0 -0
- package/src/lib/amqp.ts +32 -0
- package/src/lib/extra-validators.ts +62 -0
- package/src/lib/flow.interface.ts +56 -0
- package/{dist/index.d.ts → src/lib/index.ts} +3 -0
- package/src/lib/nats.ts +140 -0
- package/src/lib/unit-decorators.ts +156 -0
- package/src/lib/unit-utils.ts +163 -0
- package/src/lib/units.ts +587 -0
- package/src/lib/utils.ts +176 -0
- package/test/context-manager-purpose.spec.ts +248 -0
- package/test/context-manager.spec.ts +55 -0
- package/test/context.spec.ts +180 -0
- package/test/event.spec.ts +155 -0
- package/test/extra-validators.spec.ts +84 -0
- package/test/flow-logger.spec.ts +104 -0
- package/test/flow.spec.ts +508 -0
- package/test/input-stream.decorator.spec.ts +379 -0
- package/test/long-rpc.test.py +14 -0
- package/test/long-running-rpc.spec.ts +60 -0
- package/test/message.spec.ts +57 -0
- package/test/mocks/logger.mock.ts +7 -0
- package/test/mocks/nats-connection.mock.ts +135 -0
- package/test/mocks/nats-prepare.reals-nats.ts +15 -0
- package/test/rpc.spec.ts +198 -0
- package/test/rpc.test.py +45 -0
- package/test/rx.spec.ts +92 -0
- package/test/unit-decorator.spec.ts +57 -0
- package/test/utils.spec.ts +210 -0
- package/test/validation.spec.ts +174 -0
- package/tsconfig.json +13 -0
- package/tsconfig.lib.json +22 -0
- package/tsconfig.spec.json +8 -0
- package/LICENSE +0 -21
- package/dist/ContextManager.d.ts +0 -40
- package/dist/ContextManager.js +0 -77
- package/dist/FlowApplication.d.ts +0 -85
- package/dist/FlowApplication.js +0 -500
- package/dist/FlowElement.d.ts +0 -67
- package/dist/FlowElement.js +0 -163
- package/dist/FlowEvent.d.ts +0 -25
- package/dist/FlowEvent.js +0 -71
- package/dist/FlowLogger.d.ts +0 -44
- package/dist/FlowLogger.js +0 -94
- package/dist/FlowModule.d.ts +0 -7
- package/dist/FlowModule.js +0 -13
- package/dist/RpcClient.d.ts +0 -13
- package/dist/RpcClient.js +0 -84
- package/dist/TestModule.d.ts +0 -2
- package/dist/TestModule.js +0 -27
- package/dist/amqp.d.ts +0 -14
- package/dist/amqp.js +0 -12
- package/dist/extra-validators.d.ts +0 -1
- package/dist/extra-validators.js +0 -51
- package/dist/flow.interface.d.ts +0 -48
- package/dist/flow.interface.js +0 -9
- package/dist/index.js +0 -18
- package/dist/nats.d.ts +0 -12
- package/dist/nats.js +0 -109
- package/dist/unit-decorators.d.ts +0 -39
- package/dist/unit-decorators.js +0 -156
- package/dist/unit-utils.d.ts +0 -8
- package/dist/unit-utils.js +0 -143
- package/dist/units.d.ts +0 -31
- package/dist/units.js +0 -570
- package/dist/utils.d.ts +0 -51
- package/dist/utils.js +0 -137
- /package/{dist → src/lib}/rpc_server.py +0 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Owner } from '@hahnpro/hpc-api';
|
|
2
|
+
|
|
3
|
+
export interface FlowContext {
|
|
4
|
+
deploymentId?: string;
|
|
5
|
+
diagramId?: string;
|
|
6
|
+
flowId?: string;
|
|
7
|
+
owner?: Owner;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface FlowElementContext extends FlowContext {
|
|
11
|
+
id: string;
|
|
12
|
+
name?: string;
|
|
13
|
+
functionFqn?: string;
|
|
14
|
+
inputStreamId?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface DeploymentMessage extends Record<string, any> {
|
|
18
|
+
elementId?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface FlowElement {
|
|
22
|
+
id: string;
|
|
23
|
+
name?: string;
|
|
24
|
+
properties?: Record<string, any>;
|
|
25
|
+
module: string;
|
|
26
|
+
functionFqn: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface FlowConnection {
|
|
30
|
+
id: string;
|
|
31
|
+
name?: string;
|
|
32
|
+
source: string;
|
|
33
|
+
target: string;
|
|
34
|
+
sourceStream?: string;
|
|
35
|
+
targetStream?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface Flow {
|
|
39
|
+
elements: FlowElement[];
|
|
40
|
+
connections: FlowConnection[];
|
|
41
|
+
modules?: string[];
|
|
42
|
+
properties?: Record<string, any>;
|
|
43
|
+
context?: FlowContext;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface StreamOptions {
|
|
47
|
+
concurrent?: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type ClassType<T> = new (...args: any[]) => T;
|
|
51
|
+
|
|
52
|
+
export enum LifecycleEvent {
|
|
53
|
+
ACTIVATED = 'com.hahnpro.flow_function.activated',
|
|
54
|
+
COMPLETED = 'com.hahnpro.flow_function.completed',
|
|
55
|
+
TERMINATED = 'com.hahnpro.flow_function.terminated',
|
|
56
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export * from '@hahnpro/hpc-api';
|
|
2
|
+
|
|
2
3
|
export * from './flow.interface';
|
|
3
4
|
export * from './utils';
|
|
4
5
|
export * from './FlowApplication';
|
|
@@ -8,5 +9,7 @@ export * from './FlowLogger';
|
|
|
8
9
|
export { FlowModule } from './FlowModule';
|
|
9
10
|
export * from './TestModule';
|
|
10
11
|
export * from './unit-decorators';
|
|
12
|
+
|
|
11
13
|
export * from './ContextManager';
|
|
14
|
+
|
|
12
15
|
export { IncompatableWith } from './extra-validators';
|
package/src/lib/nats.ts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AckPolicy,
|
|
3
|
+
Consumer,
|
|
4
|
+
ConsumerConfig,
|
|
5
|
+
DeliverPolicy,
|
|
6
|
+
jetstream,
|
|
7
|
+
JetStreamApiError,
|
|
8
|
+
jetstreamManager,
|
|
9
|
+
PubAck,
|
|
10
|
+
ReplayPolicy,
|
|
11
|
+
} from '@nats-io/jetstream';
|
|
12
|
+
import { ConnectionOptions, NatsConnection } from '@nats-io/nats-core';
|
|
13
|
+
import { connect } from '@nats-io/transport-node';
|
|
14
|
+
import { CloudEvent } from 'cloudevents';
|
|
15
|
+
import { isEqual, omitBy } from 'lodash';
|
|
16
|
+
|
|
17
|
+
import { Logger } from './FlowLogger';
|
|
18
|
+
|
|
19
|
+
export type NatsEvent<T> = Pick<CloudEvent<T>, 'type' | 'source' | 'subject' | 'data' | 'datacontenttype' | 'time'>;
|
|
20
|
+
|
|
21
|
+
export const natsFlowsPrefixFlowDeployment = `fs.flowdeployment`;
|
|
22
|
+
|
|
23
|
+
// https://docs.nats.io/nats-concepts/jetstream/consumers#configuration
|
|
24
|
+
export const defaultConsumerConfig: ConsumerConfig = {
|
|
25
|
+
ack_policy: AckPolicy.Explicit,
|
|
26
|
+
ack_wait: 30_000_000_000, // 30 seconds
|
|
27
|
+
deliver_policy: DeliverPolicy.All,
|
|
28
|
+
max_ack_pending: 1000,
|
|
29
|
+
max_deliver: -1,
|
|
30
|
+
max_waiting: 512,
|
|
31
|
+
replay_policy: ReplayPolicy.Instant,
|
|
32
|
+
num_replicas: 0,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const FLOWS_STREAM_NAME = 'flows';
|
|
36
|
+
|
|
37
|
+
export async function getOrCreateConsumer(
|
|
38
|
+
logger: Logger,
|
|
39
|
+
natsConnection: NatsConnection,
|
|
40
|
+
streamName: string,
|
|
41
|
+
consumerName: string,
|
|
42
|
+
options: Partial<ConsumerConfig>,
|
|
43
|
+
): Promise<Consumer> {
|
|
44
|
+
if (!natsConnection || natsConnection.isClosed()) {
|
|
45
|
+
throw new Error('NATS connection is not available');
|
|
46
|
+
} else if (!streamName) {
|
|
47
|
+
throw new Error('Stream name is not available');
|
|
48
|
+
} else if (!consumerName) {
|
|
49
|
+
throw new Error('Consumer name is not available');
|
|
50
|
+
}
|
|
51
|
+
logger.debug(`Creating consumer ${consumerName} for stream ${streamName}`);
|
|
52
|
+
|
|
53
|
+
const jsm = await jetstreamManager(natsConnection);
|
|
54
|
+
const consumerInfo = await jsm.consumers.info(streamName, consumerName).catch((err: JetStreamApiError) => {
|
|
55
|
+
if (err.status !== 404) {
|
|
56
|
+
logger.error(`Could not get consumer info of stream ${streamName}`, err);
|
|
57
|
+
logger.error(err.message);
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const consumerConfig = { ...defaultConsumerConfig, ...options };
|
|
63
|
+
if (consumerInfo) {
|
|
64
|
+
const compared = omitBy(consumerConfig, (value, key) => isEqual(value, consumerInfo.config[key]));
|
|
65
|
+
if (Object.keys(compared).length !== 0) {
|
|
66
|
+
await jsm.consumers.update(streamName, consumerName, consumerConfig);
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
await jsm.consumers.add(streamName, { name: consumerName, ...consumerConfig });
|
|
70
|
+
}
|
|
71
|
+
return await jetstream(natsConnection).consumers.get(streamName, consumerName);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function natsEventListener(nc: NatsConnection, logger: Logger, reconnectHandler: () => void): Promise<void> {
|
|
75
|
+
const statusAsyncIterator = nc?.status();
|
|
76
|
+
if (!statusAsyncIterator) {
|
|
77
|
+
logger.error('NATS Status-AsyncIterator is not available, cannot listen for events to re-create consumers at reconnects');
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for await (const status of statusAsyncIterator) {
|
|
82
|
+
// Handle reconnect: event is triggered when the NATS client reconnected to the server
|
|
83
|
+
if (status.type === 'reconnect') {
|
|
84
|
+
reconnectHandler();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function publishNatsEvent<T>(logger: Logger, nc: NatsConnection, event: NatsEvent<T>, subject?: string): Promise<PubAck> {
|
|
90
|
+
if (!nc || nc.isClosed()) {
|
|
91
|
+
logger.error('NATS connection is not available, cannot publish event');
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const cloudEvent = new CloudEvent<T>({ datacontenttype: 'application/json', ...event });
|
|
95
|
+
cloudEvent.validate();
|
|
96
|
+
const js = jetstream(nc);
|
|
97
|
+
if (js) {
|
|
98
|
+
return js.publish(subject || `${cloudEvent.type}.${cloudEvent.subject}`, JSON.stringify(cloudEvent.toJSON()), {
|
|
99
|
+
msgID: cloudEvent.id,
|
|
100
|
+
});
|
|
101
|
+
} else {
|
|
102
|
+
logger.error(`Could not publish nats event, because jetstream is unavailable / undefined`);
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function createNatsConnection(config: ConnectionOptions): Promise<NatsConnection> {
|
|
108
|
+
const servers: string | string[] = config?.servers ?? process.env.NATS_SERVERS?.split(',') ?? [];
|
|
109
|
+
const reconnect: boolean = config?.reconnect ?? (process.env.NATS_RECONNECT ?? 'true') === 'true';
|
|
110
|
+
|
|
111
|
+
// Default maxReconnectAttempts is 10
|
|
112
|
+
let maxReconnectAttempts: number = config?.maxReconnectAttempts ?? parseInt(process.env.NATS_MAX_RECONNECT_ATTEMPTS ?? '-1', 10);
|
|
113
|
+
if (isNaN(maxReconnectAttempts)) {
|
|
114
|
+
maxReconnectAttempts = 10;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Default reconnectTimeWait is 2000ms
|
|
118
|
+
let reconnectTimeWait: number = config?.reconnectTimeWait ?? parseInt(process.env.NATS_RECONNECT_TIME_WAIT ?? '2000', 10);
|
|
119
|
+
if (isNaN(reconnectTimeWait)) {
|
|
120
|
+
reconnectTimeWait = 2000;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Default timeout is 2000ms
|
|
124
|
+
let timeout: number = config?.timeout ?? parseInt(process.env.NATS_TIMEOUT ?? '2000', 10);
|
|
125
|
+
if (isNaN(timeout)) {
|
|
126
|
+
timeout = 2000;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const options: ConnectionOptions = {
|
|
130
|
+
servers,
|
|
131
|
+
reconnect,
|
|
132
|
+
maxReconnectAttempts, // <-- maxReconnectAttempts: -1 means infinite reconnect attempts
|
|
133
|
+
reconnectTimeWait, // <-- reconnectTimeWait: -1 means no wait time between reconnect attempts
|
|
134
|
+
timeout,
|
|
135
|
+
user: config?.user ?? process.env.NATS_USER,
|
|
136
|
+
pass: config.pass ?? process.env.NATS_PASSWORD,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
return connect(options);
|
|
140
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { ValidationOptions } from 'class-validator';
|
|
2
|
+
|
|
3
|
+
import { makeUnitDecorator } from './unit-utils';
|
|
4
|
+
import { units } from './units';
|
|
5
|
+
|
|
6
|
+
export function IsTime(unit = units.time.baseUnit, validationOptions?: ValidationOptions) {
|
|
7
|
+
return makeUnitDecorator(unit, 'time', validationOptions);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function IsLength(unit = units.length.baseUnit, validationOptions?: ValidationOptions) {
|
|
11
|
+
return makeUnitDecorator(unit, 'length', validationOptions);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function IsMass(unit = units.mass.baseUnit, validationOptions?: ValidationOptions) {
|
|
15
|
+
return makeUnitDecorator(unit, 'mass', validationOptions);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function IsElectricCurrent(unit = units.electricCurrent.baseUnit, validationOptions?: ValidationOptions) {
|
|
19
|
+
return makeUnitDecorator(unit, 'electricCurrent', validationOptions);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function IsThermodynamicTemperature(unit = units.thermodynamicTemperature.baseUnit, validationOptions?: ValidationOptions) {
|
|
23
|
+
return makeUnitDecorator(unit, 'thermodynamicTemperature', validationOptions);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function IsAmountOfSubstance(unit = units.amountOfSubstance.baseUnit, validationOptions?: ValidationOptions) {
|
|
27
|
+
return makeUnitDecorator(unit, 'amountOfSubstance', validationOptions);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function IsLuminousIntensity(unit = units.luminousIntensity.baseUnit, validationOptions?: ValidationOptions) {
|
|
31
|
+
return makeUnitDecorator(unit, 'luminousIntensity', validationOptions);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function IsVoltage(unit = units.voltage.baseUnit, validationOptions?: ValidationOptions) {
|
|
35
|
+
return makeUnitDecorator(unit, 'voltage', validationOptions);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function IsForce(unit = units.force.baseUnit, validationOptions?: ValidationOptions) {
|
|
39
|
+
return makeUnitDecorator(unit, 'force', validationOptions);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function IsEnergy(unit = units.energy.baseUnit, validationOptions?: ValidationOptions) {
|
|
43
|
+
return makeUnitDecorator(unit, 'energy', validationOptions);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function IsPower(unit = units.power.baseUnit, validationOptions?: ValidationOptions) {
|
|
47
|
+
return makeUnitDecorator(unit, 'power', validationOptions);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function IsPressure(unit = units.pressure.baseUnit, validationOptions?: ValidationOptions) {
|
|
51
|
+
return makeUnitDecorator(unit, 'pressure', validationOptions);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function IsFrequency(unit = units.frequency.baseUnit, validationOptions?: ValidationOptions) {
|
|
55
|
+
return makeUnitDecorator(unit, 'frequency', validationOptions);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function IsArea(unit = units.area.baseUnit, validationOptions?: ValidationOptions) {
|
|
59
|
+
return makeUnitDecorator(unit, 'area', validationOptions);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function IsVolume(unit = units.volume.baseUnit, validationOptions?: ValidationOptions) {
|
|
63
|
+
return makeUnitDecorator(unit, 'volume', validationOptions);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function IsAngle(unit = units.angle.baseUnit, validationOptions?: ValidationOptions) {
|
|
67
|
+
return makeUnitDecorator(unit, 'angle', validationOptions);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function IsTranslationalAcceleration(unit = units.translationalAcceleration.baseUnit, validationOptions?: ValidationOptions) {
|
|
71
|
+
return makeUnitDecorator(unit, 'translationalAcceleration', validationOptions);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function IsTranslationalVelocity(unit = units.translationalVelocity.baseUnit, validationOptions?: ValidationOptions) {
|
|
75
|
+
return makeUnitDecorator(unit, 'translationalVelocity', validationOptions);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function IsTranslationalDisplacement(unit = units.translationalDisplacement.baseUnit, validationOptions?: ValidationOptions) {
|
|
79
|
+
return makeUnitDecorator(unit, 'translationalDisplacement', validationOptions);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function IsSpringConstant(unit = units.springConstant.baseUnit, validationOptions?: ValidationOptions) {
|
|
83
|
+
return makeUnitDecorator(unit, 'springConstant', validationOptions);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function IsRotationalAcceleration(unit = units.rotationalAcceleration.baseUnit, validationOptions?: ValidationOptions) {
|
|
87
|
+
return makeUnitDecorator(unit, 'rotationalAcceleration', validationOptions);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function IsRotationalVelocity(unit = units.rotationalVelocity.baseUnit, validationOptions?: ValidationOptions) {
|
|
91
|
+
return makeUnitDecorator(unit, 'rotationalVelocity', validationOptions);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function IsRotationalDisplacement(unit = units.rotationalDisplacement.baseUnit, validationOptions?: ValidationOptions) {
|
|
95
|
+
return makeUnitDecorator(unit, 'rotationalDisplacement', validationOptions);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function IsTorque(unit = units.torque.baseUnit, validationOptions?: ValidationOptions) {
|
|
99
|
+
return makeUnitDecorator(unit, 'torque', validationOptions);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function IsMomentOfInertia(unit = units.momentOfInertia.baseUnit, validationOptions?: ValidationOptions) {
|
|
103
|
+
return makeUnitDecorator(unit, 'momentOfInertia', validationOptions);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function IsRotatingUnbalance(unit = units.rotatingUnbalance.baseUnit, validationOptions?: ValidationOptions) {
|
|
107
|
+
return makeUnitDecorator(unit, 'rotatingUnbalance', validationOptions);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function IsMechanicalPower(unit = units.mechanicalPower.baseUnit, validationOptions?: ValidationOptions) {
|
|
111
|
+
return makeUnitDecorator(unit, 'mechanicalPower', validationOptions);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function IsMechanicalEnergy(unit = units.mechanicalEnergy.baseUnit, validationOptions?: ValidationOptions) {
|
|
115
|
+
return makeUnitDecorator(unit, 'mechanicalEnergy', validationOptions);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function IsDynamicViscosity(unit = units.dynamicViscosity.baseUnit, validationOptions?: ValidationOptions) {
|
|
119
|
+
return makeUnitDecorator(unit, 'dynamicViscosity', validationOptions);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function IsVolumeFlow(unit = units.volumeFlow.baseUnit, validationOptions?: ValidationOptions) {
|
|
123
|
+
return makeUnitDecorator(unit, 'volumeFlow', validationOptions);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function IsMassFlow(unit = units.massFlow.baseUnit, validationOptions?: ValidationOptions) {
|
|
127
|
+
return makeUnitDecorator(unit, 'massFlow', validationOptions);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function IsHeatFlux(unit = units.heatFlux.baseUnit, validationOptions?: ValidationOptions) {
|
|
131
|
+
return makeUnitDecorator(unit, 'heatFlux', validationOptions);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function IsThermalEnergy(unit = units.thermalEnergy.baseUnit, validationOptions?: ValidationOptions) {
|
|
135
|
+
return makeUnitDecorator(unit, 'thermalEnergy', validationOptions);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function IsSpecificHeatCapacity(unit = units.specificHeatCapacity.baseUnit, validationOptions?: ValidationOptions) {
|
|
139
|
+
return makeUnitDecorator(unit, 'specificHeatCapacity', validationOptions);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function IsThermalTransmittance(unit = units.thermalTransmittance.baseUnit, validationOptions?: ValidationOptions) {
|
|
143
|
+
return makeUnitDecorator(unit, 'thermalTransmittance', validationOptions);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function IsElectricalPower(unit = units.electricalPower.baseUnit, validationOptions?: ValidationOptions) {
|
|
147
|
+
return makeUnitDecorator(unit, 'electricalPower', validationOptions);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function IsElectricalEnergy(unit = units.electricalEnergy.baseUnit, validationOptions?: ValidationOptions) {
|
|
151
|
+
return makeUnitDecorator(unit, 'electricalEnergy', validationOptions);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function IsElectricalFrequency(unit = units.electricalFrequency.baseUnit, validationOptions?: ValidationOptions) {
|
|
155
|
+
return makeUnitDecorator(unit, 'electricalFrequency', validationOptions);
|
|
156
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isNumber,
|
|
3
|
+
registerDecorator,
|
|
4
|
+
ValidationArguments,
|
|
5
|
+
ValidationOptions,
|
|
6
|
+
ValidatorConstraint,
|
|
7
|
+
ValidatorConstraintInterface,
|
|
8
|
+
} from 'class-validator';
|
|
9
|
+
|
|
10
|
+
import { dimensionToUnitMap, units } from './units';
|
|
11
|
+
|
|
12
|
+
export function makeUnitDecorator(unit: string, metric: string, validationOptions?: ValidationOptions) {
|
|
13
|
+
const conversionFactor = verifyUnit(unit, metric);
|
|
14
|
+
if (conversionFactor < 0) {
|
|
15
|
+
throw new Error(`${unit} is not a valid ${metric}.`);
|
|
16
|
+
}
|
|
17
|
+
return (object: any, propertyName: string) => {
|
|
18
|
+
Reflect.defineMetadata(`conversionFactor:${propertyName}`, conversionFactor, object.constructor);
|
|
19
|
+
registerDecorator({
|
|
20
|
+
target: object.constructor,
|
|
21
|
+
propertyName,
|
|
22
|
+
options: validationOptions,
|
|
23
|
+
constraints: [metric, unit],
|
|
24
|
+
validator: UnitArgsValidator,
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@ValidatorConstraint({ async: false })
|
|
30
|
+
class UnitArgsValidator implements ValidatorConstraintInterface {
|
|
31
|
+
validate(value: any, validationArguments?: ValidationArguments): Promise<boolean> | boolean {
|
|
32
|
+
return isNumber(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
defaultMessage(args: ValidationArguments) {
|
|
36
|
+
return `${args.property} must be a number conforming to the specified constraints`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const prefixes: { prefix: string; convfactor: number }[] = [
|
|
41
|
+
{ prefix: 'Y', convfactor: 1e24 },
|
|
42
|
+
{ prefix: 'Z', convfactor: 1e21 },
|
|
43
|
+
{ prefix: 'E', convfactor: 1e18 },
|
|
44
|
+
{ prefix: 'P', convfactor: 1e15 },
|
|
45
|
+
{ prefix: 'T', convfactor: 1e12 },
|
|
46
|
+
{ prefix: 'G', convfactor: 1e9 },
|
|
47
|
+
{ prefix: 'M', convfactor: 1e6 },
|
|
48
|
+
{ prefix: 'k', convfactor: 1e3 },
|
|
49
|
+
{ prefix: 'h', convfactor: 1e2 },
|
|
50
|
+
{ prefix: 'da', convfactor: 1e1 },
|
|
51
|
+
{ prefix: 'd', convfactor: 1e-1 },
|
|
52
|
+
{ prefix: 'c', convfactor: 1e-2 },
|
|
53
|
+
{ prefix: 'm', convfactor: 1e-3 },
|
|
54
|
+
{ prefix: 'μ', convfactor: 1e-6 },
|
|
55
|
+
{ prefix: 'n', convfactor: 1e-9 },
|
|
56
|
+
{ prefix: 'p', convfactor: 1e-12 },
|
|
57
|
+
{ prefix: 'f', convfactor: 1e-15 },
|
|
58
|
+
{ prefix: 'a', convfactor: 1e-18 },
|
|
59
|
+
{ prefix: 'z', convfactor: 1e-21 },
|
|
60
|
+
{ prefix: 'y', convfactor: 1e-24 },
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
export function verifyUnit(unit: string, metric: string): number {
|
|
64
|
+
if (unit === '' || metric === '') return -1;
|
|
65
|
+
const definition = units[metric];
|
|
66
|
+
|
|
67
|
+
for (const dimension of definition.dimensions) {
|
|
68
|
+
const components = dimension.split('*');
|
|
69
|
+
const nominator = components.filter((v) => !v.includes('-'));
|
|
70
|
+
const denominator = components.filter((v) => v.includes('-'));
|
|
71
|
+
|
|
72
|
+
const nomIndices = computeIndices(unit.split('/')[0], nominator);
|
|
73
|
+
const denomIndices = computeIndices(unit.split('/')[1] || '', denominator);
|
|
74
|
+
|
|
75
|
+
const nomFactor = verifyIndices(nomIndices, unit.split('/')[0]);
|
|
76
|
+
const denomFactor = verifyIndices(denomIndices, unit.split('/')[1]);
|
|
77
|
+
|
|
78
|
+
if (nomFactor > 0 && denomFactor > 0) {
|
|
79
|
+
return nomFactor / denomFactor;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return -1;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function verifyIndices(indices: { index: number; length: number; convfactor: number }[][], unit: string): number {
|
|
86
|
+
if (indices.length === 0 || !unit) return 1;
|
|
87
|
+
|
|
88
|
+
const filtered = indices
|
|
89
|
+
.map((arr) => arr.filter((v) => v.index !== -1))
|
|
90
|
+
.reduce(
|
|
91
|
+
(previousValue, currentValue) => {
|
|
92
|
+
if (currentValue.length > 1) {
|
|
93
|
+
// copy existing arrays *length* times
|
|
94
|
+
for (let j = 0; j < currentValue.length - 1; j++) {
|
|
95
|
+
previousValue.forEach((value) => previousValue.push([...value]));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
let index = 0;
|
|
99
|
+
while (index < previousValue.length) {
|
|
100
|
+
previousValue[index].push(currentValue[index % currentValue.length]);
|
|
101
|
+
index++;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return previousValue;
|
|
105
|
+
},
|
|
106
|
+
[[]],
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
outer: for (const filteredElement of filtered) {
|
|
110
|
+
let index = 0;
|
|
111
|
+
let convfactor = 1;
|
|
112
|
+
while (index != unit.length) {
|
|
113
|
+
const find = filteredElement.find((obj) => obj?.index === index);
|
|
114
|
+
if (!find) continue outer;
|
|
115
|
+
index += find.length;
|
|
116
|
+
convfactor *= find.convfactor;
|
|
117
|
+
}
|
|
118
|
+
return convfactor;
|
|
119
|
+
}
|
|
120
|
+
return -1;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function computeIndices(unit: string, dimensions: string[]) {
|
|
124
|
+
return dimensions.map((dimension) => {
|
|
125
|
+
const options = computeUnitOptions(dimension);
|
|
126
|
+
const indices = options.map((preUnit) => unit.indexOf(preUnit.prefUnit));
|
|
127
|
+
return indices.map((ind, index) => ({
|
|
128
|
+
index: ind,
|
|
129
|
+
length: options[index].prefUnit.length,
|
|
130
|
+
convfactor: options[index].convfactor,
|
|
131
|
+
offset: options[index].offset,
|
|
132
|
+
}));
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function computeUnitOptions(dimension: string): { prefUnit: string; convfactor: number; offset: number }[] {
|
|
137
|
+
const definition = units[dimensionToUnitMap[dimension.substring(0, 1)]];
|
|
138
|
+
const exponent = dimension.length === 1 ? 1 : Number.parseInt(dimension.substring(dimension.length - 1), 10);
|
|
139
|
+
const withPrefixes = definition.units
|
|
140
|
+
.filter((unit) => unit.SIPrefixes)
|
|
141
|
+
.map((unit) =>
|
|
142
|
+
prefixes.map((prefix) => ({
|
|
143
|
+
prefUnit: prefix.prefix.concat(unit.unit),
|
|
144
|
+
convfactor: Math.pow(prefix.convfactor * unit.conversionFactor, exponent),
|
|
145
|
+
offset: unit.offset || 0,
|
|
146
|
+
})),
|
|
147
|
+
)
|
|
148
|
+
.reduce((prev, curr) => prev.concat(curr), []);
|
|
149
|
+
const allUnits = withPrefixes.concat(
|
|
150
|
+
definition.units.map((unit) => ({
|
|
151
|
+
prefUnit: unit.unit,
|
|
152
|
+
convfactor: Math.pow(unit.conversionFactor, exponent),
|
|
153
|
+
offset: unit.offset || 0,
|
|
154
|
+
})),
|
|
155
|
+
);
|
|
156
|
+
return dimension.length > 1 && !dimension.endsWith('-1')
|
|
157
|
+
? allUnits.map((unit) => ({
|
|
158
|
+
prefUnit: `${unit.prefUnit}^${dimension.substring(dimension.length - 1)}`,
|
|
159
|
+
convfactor: unit.convfactor,
|
|
160
|
+
offset: unit.offset,
|
|
161
|
+
}))
|
|
162
|
+
: allUnits;
|
|
163
|
+
}
|