@railway-ts/pipelines 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +811 -0
- package/dist/composition/index.cjs +72 -0
- package/dist/composition/index.cjs.map +1 -0
- package/dist/composition/index.d.cts +286 -0
- package/dist/composition/index.d.ts +286 -0
- package/dist/composition/index.mjs +65 -0
- package/dist/composition/index.mjs.map +1 -0
- package/dist/index-BdfKTZ7O.d.cts +799 -0
- package/dist/index-BdfKTZ7O.d.ts +799 -0
- package/dist/index.cjs +1074 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.mjs +969 -0
- package/dist/index.mjs.map +1 -0
- package/dist/option/index.cjs +111 -0
- package/dist/option/index.cjs.map +1 -0
- package/dist/option/index.d.cts +1 -0
- package/dist/option/index.d.ts +1 -0
- package/dist/option/index.mjs +93 -0
- package/dist/option/index.mjs.map +1 -0
- package/dist/result/index.cjs +178 -0
- package/dist/result/index.cjs.map +1 -0
- package/dist/result/index.d.cts +1 -0
- package/dist/result/index.d.ts +1 -0
- package/dist/result/index.mjs +152 -0
- package/dist/result/index.mjs.map +1 -0
- package/dist/schema/index.cjs +794 -0
- package/dist/schema/index.cjs.map +1 -0
- package/dist/schema/index.d.cts +1867 -0
- package/dist/schema/index.d.ts +1867 -0
- package/dist/schema/index.mjs +735 -0
- package/dist/schema/index.mjs.map +1 -0
- package/examples/complete-pipelines/async-launch.ts +128 -0
- package/examples/complete-pipelines/async.ts +119 -0
- package/examples/complete-pipelines/hill-clohessy-wiltshire.ts +218 -0
- package/examples/complete-pipelines/hohmann-transfer.ts +159 -0
- package/examples/composition/advanced-composition.ts +32 -0
- package/examples/composition/curry-basics.ts +24 -0
- package/examples/composition/tupled-basics.ts +26 -0
- package/examples/index.ts +47 -0
- package/examples/interop/interop-examples.ts +110 -0
- package/examples/option/option-examples.ts +63 -0
- package/examples/result/result-examples.ts +110 -0
- package/examples/schema/basic.ts +78 -0
- package/examples/schema/union.ts +301 -0
- package/package.json +100 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { pipe } from '@/composition';
|
|
2
|
+
import { ok, err, fromTry, match, flatMap, map, andThen, type Result } from '@/result';
|
|
3
|
+
|
|
4
|
+
// Example 1: Division by Zero
|
|
5
|
+
console.log('=== Division by Zero ===');
|
|
6
|
+
|
|
7
|
+
// Problem: Division can fail but functions don't show it in types
|
|
8
|
+
// function divide(a: number, b: number) {
|
|
9
|
+
// return a / b; // Returns Infinity for division by zero
|
|
10
|
+
// }
|
|
11
|
+
|
|
12
|
+
// Solution: Result makes failure explicit
|
|
13
|
+
function safeDivide(a: number, b: number) {
|
|
14
|
+
return b === 0 ? err('Division by zero') : ok(a / b);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const result1 = safeDivide(10, 2);
|
|
18
|
+
const result2 = safeDivide(10, 0);
|
|
19
|
+
|
|
20
|
+
match(result1, {
|
|
21
|
+
ok: (value) => console.log(`Result: ${value}`),
|
|
22
|
+
err: (error) => console.log(`Error: ${error}`),
|
|
23
|
+
}); // "Result: 5"
|
|
24
|
+
|
|
25
|
+
match(result2, {
|
|
26
|
+
ok: (value) => console.log(`Result: ${value}`),
|
|
27
|
+
err: (error) => console.log(`Error: ${error}`),
|
|
28
|
+
}); // "Error: Division by zero"
|
|
29
|
+
|
|
30
|
+
// Example 2: JSON Parsing
|
|
31
|
+
console.log('\n=== JSON Parsing ===');
|
|
32
|
+
|
|
33
|
+
// Problem: JSON.parse throws exceptions
|
|
34
|
+
// const data = JSON.parse('invalid json'); // Throws!
|
|
35
|
+
|
|
36
|
+
// Solution: fromTry captures exceptions as Results
|
|
37
|
+
const safeParseJson = (s: string) => fromTry(() => JSON.parse(s));
|
|
38
|
+
|
|
39
|
+
const valid = safeParseJson('{"name": "Alice"}');
|
|
40
|
+
const invalid = safeParseJson('invalid json');
|
|
41
|
+
|
|
42
|
+
match(valid, {
|
|
43
|
+
ok: (data) => console.log(`Parsed: ${data.name}`),
|
|
44
|
+
err: (error) => console.log(`Parse failed: ${error}`),
|
|
45
|
+
}); // "Parsed: Alice"
|
|
46
|
+
|
|
47
|
+
match(invalid, {
|
|
48
|
+
ok: (data) => console.log(`Parsed: ${data.name}`),
|
|
49
|
+
err: (error) => console.log(`Parse failed: ${error}`),
|
|
50
|
+
}); // "Parse failed: Unexpected token..."
|
|
51
|
+
|
|
52
|
+
// Example 3: Chaining Operations
|
|
53
|
+
console.log('\n=== Chaining Operations ===');
|
|
54
|
+
|
|
55
|
+
// Problem: Multiple operations that can fail require nested try/catch
|
|
56
|
+
// Solution: flatMapResult chains failing operations cleanly
|
|
57
|
+
|
|
58
|
+
const hasNumericValue = (u: unknown): u is { value: number } =>
|
|
59
|
+
typeof u === 'object' && u !== null && 'value' in u && typeof (u as { value: unknown }).value === 'number';
|
|
60
|
+
|
|
61
|
+
const toNumber = (data: unknown): Result<number, string> =>
|
|
62
|
+
hasNumericValue(data) ? ok(data.value) : err('Not a number');
|
|
63
|
+
|
|
64
|
+
const processNumber = (input: string) =>
|
|
65
|
+
pipe(
|
|
66
|
+
safeParseJson(input),
|
|
67
|
+
(result) => flatMap(result, toNumber),
|
|
68
|
+
(result) => flatMap(result, (num) => safeDivide(num, 2)),
|
|
69
|
+
(result) => map(result, (num) => Math.round(num)),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const success = processNumber('{"value": 42}');
|
|
73
|
+
const failure = processNumber('{"value": "not a number"}');
|
|
74
|
+
|
|
75
|
+
match(success, {
|
|
76
|
+
ok: (value) => console.log(`Final result: ${value}`),
|
|
77
|
+
err: (error) => console.log(`Failed: ${error}`),
|
|
78
|
+
}); // "Final result: 21"
|
|
79
|
+
|
|
80
|
+
match(failure, {
|
|
81
|
+
ok: (value) => console.log(`Final result: ${value}`),
|
|
82
|
+
err: (error) => console.log(`Failed: ${error}`),
|
|
83
|
+
}); // "Failed: Not a number"
|
|
84
|
+
|
|
85
|
+
// Example 4: Async Chaining with andThen
|
|
86
|
+
console.log('\n=== Async Chaining ===');
|
|
87
|
+
|
|
88
|
+
const safeDivideAsync = async (a: number, b: number): Promise<Result<number, string>> =>
|
|
89
|
+
b === 0 ? err('Division by zero') : ok(a / b);
|
|
90
|
+
|
|
91
|
+
const processNumberAsync = async (input: string) =>
|
|
92
|
+
await pipe(
|
|
93
|
+
safeParseJson(input),
|
|
94
|
+
(r) => andThen(r, toNumber),
|
|
95
|
+
(p) => andThen(p, (n) => safeDivideAsync(n, 2)),
|
|
96
|
+
(p) => andThen(p, (n) => ok(Math.round(n))),
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const successAsync = await processNumberAsync('{"value": 42}');
|
|
100
|
+
const failureAsync = await processNumberAsync('{"value": "not a number"}');
|
|
101
|
+
|
|
102
|
+
match(successAsync, {
|
|
103
|
+
ok: (value) => console.log(`Async final result: ${value}`),
|
|
104
|
+
err: (error) => console.log(`Async failed: ${error}`),
|
|
105
|
+
}); // "Async final result: 21"
|
|
106
|
+
|
|
107
|
+
match(failureAsync, {
|
|
108
|
+
ok: (value) => console.log(`Async final result: ${value}`),
|
|
109
|
+
err: (error) => console.log(`Async failed: ${error}`),
|
|
110
|
+
}); // "Async failed: Not a number"
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {
|
|
2
|
+
array,
|
|
3
|
+
chain,
|
|
4
|
+
email,
|
|
5
|
+
matches,
|
|
6
|
+
minLength,
|
|
7
|
+
object,
|
|
8
|
+
optional,
|
|
9
|
+
parseBool,
|
|
10
|
+
parseDate,
|
|
11
|
+
parseNumber,
|
|
12
|
+
pattern,
|
|
13
|
+
required,
|
|
14
|
+
string,
|
|
15
|
+
stringEnum,
|
|
16
|
+
validateAndFormatResult,
|
|
17
|
+
nonEmpty,
|
|
18
|
+
} from '@/schema';
|
|
19
|
+
|
|
20
|
+
const invalidInput = {
|
|
21
|
+
username: 'jo', // too short
|
|
22
|
+
email: 'not-an-email', // invalid format
|
|
23
|
+
password: 'password', // not complex enough
|
|
24
|
+
age: '25', // string that will be parsed to a number
|
|
25
|
+
birthdate: '1995-05-15', // string that will be parsed to a Date
|
|
26
|
+
termsAccepted: 'yes', // string that will be parsed to boolean true
|
|
27
|
+
address: {
|
|
28
|
+
street: '123 Main St',
|
|
29
|
+
city: 'A', // too short
|
|
30
|
+
zipCode: '1234', // invalid format
|
|
31
|
+
},
|
|
32
|
+
contacts: ['email', 'phone', 'fax'], // fax is not a valid option
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const validInput = {
|
|
36
|
+
username: 'john_doe',
|
|
37
|
+
email: 'valid@email.com',
|
|
38
|
+
password: 'ComplexPassword123!',
|
|
39
|
+
age: 30,
|
|
40
|
+
birthdate: new Date('1995-05-15'),
|
|
41
|
+
hasAcceptedTerms: true,
|
|
42
|
+
role: 'user',
|
|
43
|
+
address: {
|
|
44
|
+
street: '123 Main St',
|
|
45
|
+
city: 'New York',
|
|
46
|
+
zipCode: '10001',
|
|
47
|
+
},
|
|
48
|
+
contacts: ['email', 'phone'],
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const addressSchema = object({
|
|
52
|
+
street: optional(chain(string(), minLength(3))),
|
|
53
|
+
city: optional(chain(string(), minLength(2))),
|
|
54
|
+
zipCode: optional(chain(string(), pattern(/^\d{5}$/))),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export const userSchema = object({
|
|
58
|
+
username: required(chain(string(), nonEmpty(), minLength(3))),
|
|
59
|
+
email: required(chain(string(), nonEmpty(), email())),
|
|
60
|
+
password: required(chain(string(), nonEmpty(), minLength(8))),
|
|
61
|
+
age: required(parseNumber()),
|
|
62
|
+
birthdate: required(parseDate()),
|
|
63
|
+
hasAcceptedTerms: required(chain(parseBool(), matches(true, 'You must check this field'))),
|
|
64
|
+
role: required(stringEnum(['admin', 'user'])),
|
|
65
|
+
address: optional(addressSchema),
|
|
66
|
+
contacts: optional(array(stringEnum(['email', 'phone']))),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Using validateAndFormatResult - one-liner convenience function
|
|
70
|
+
const valid = validateAndFormatResult(validInput, userSchema);
|
|
71
|
+
|
|
72
|
+
console.log('--- Valid User ---\n');
|
|
73
|
+
console.log(valid);
|
|
74
|
+
|
|
75
|
+
const invalid = validateAndFormatResult(invalidInput, userSchema);
|
|
76
|
+
|
|
77
|
+
console.log('--- Invalid User ---\n');
|
|
78
|
+
console.log(invalid);
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { match } from '@/result';
|
|
2
|
+
import {
|
|
3
|
+
between,
|
|
4
|
+
boolean,
|
|
5
|
+
chain,
|
|
6
|
+
discriminatedUnion,
|
|
7
|
+
formatErrors,
|
|
8
|
+
literal,
|
|
9
|
+
object,
|
|
10
|
+
parseDate,
|
|
11
|
+
parseNumber,
|
|
12
|
+
pattern,
|
|
13
|
+
required,
|
|
14
|
+
string,
|
|
15
|
+
stringEnum,
|
|
16
|
+
validate,
|
|
17
|
+
type InferSchemaType,
|
|
18
|
+
type ValidationError,
|
|
19
|
+
type ValidationResult,
|
|
20
|
+
} from '@/schema';
|
|
21
|
+
|
|
22
|
+
// === Literal Constants ===
|
|
23
|
+
export const ALERT_TYPES = {
|
|
24
|
+
ENGINE: 'engine',
|
|
25
|
+
HYDRAULIC: 'hydraulic',
|
|
26
|
+
ELECTRICAL: 'electrical',
|
|
27
|
+
FUEL: 'fuel',
|
|
28
|
+
} as const;
|
|
29
|
+
|
|
30
|
+
export const SEVERITY_LEVELS = ['warning', 'caution', 'advisory'] as const;
|
|
31
|
+
export type SeverityLevel = (typeof SEVERITY_LEVELS)[number];
|
|
32
|
+
|
|
33
|
+
export const ENGINE_NUMBERS = ['1', '2', '3', '4'] as const;
|
|
34
|
+
export type EngineNumber = (typeof ENGINE_NUMBERS)[number];
|
|
35
|
+
|
|
36
|
+
export const ENGINE_PARAMETERS = ['temperature', 'pressure', 'vibration', 'oil'] as const;
|
|
37
|
+
export type EngineParameter = (typeof ENGINE_PARAMETERS)[number];
|
|
38
|
+
|
|
39
|
+
export const HYDRAULIC_SYSTEMS = ['primary', 'backup', 'emergency'] as const;
|
|
40
|
+
export type HydraulicSystem = (typeof HYDRAULIC_SYSTEMS)[number];
|
|
41
|
+
|
|
42
|
+
export const ELECTRICAL_BUSES = ['main', 'essential', 'battery'] as const;
|
|
43
|
+
export type ElectricalBus = (typeof ELECTRICAL_BUSES)[number];
|
|
44
|
+
|
|
45
|
+
export const POWER_SOURCES = ['generator', 'apu', 'battery', 'external'] as const;
|
|
46
|
+
export type PowerSource = (typeof POWER_SOURCES)[number];
|
|
47
|
+
|
|
48
|
+
export const FUEL_TANKS = ['left_main', 'right_main', 'center', 'auxiliary'] as const;
|
|
49
|
+
export type FuelTank = (typeof FUEL_TANKS)[number];
|
|
50
|
+
|
|
51
|
+
// === Helper function to create common field definitions ===
|
|
52
|
+
const createCommonFields = () => ({
|
|
53
|
+
timestamp: required(parseDate()),
|
|
54
|
+
aircraftId: required(chain(string(), pattern(/^[A-Z]-[A-Z0-9]{4,5}$/))), // e.g., N-12345, G-ABCD
|
|
55
|
+
flightNumber: required(chain(string(), pattern(/^[A-Z]{2,3}\d{1,4}$/))), // e.g., UA123, DLH4567
|
|
56
|
+
severity: required(stringEnum<SeverityLevel>([...SEVERITY_LEVELS], 'Invalid severity level')),
|
|
57
|
+
acknowledged: required(boolean()),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// === Engine Alert Schema ===
|
|
61
|
+
const engineAlertSchema = object({
|
|
62
|
+
...createCommonFields(),
|
|
63
|
+
type: required(literal(ALERT_TYPES.ENGINE)),
|
|
64
|
+
engineNumber: required(chain(stringEnum<EngineNumber>([...ENGINE_NUMBERS], 'Invalid engine number'), parseNumber())),
|
|
65
|
+
parameter: required(stringEnum<EngineParameter>([...ENGINE_PARAMETERS], 'Invalid engine parameter')),
|
|
66
|
+
value: required(chain(parseNumber(), between(0, 1000))),
|
|
67
|
+
threshold: required(chain(parseNumber(), between(0, 1000))),
|
|
68
|
+
exceedanceTime: required(chain(parseNumber(), between(0, 3600))),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// === Hydraulic System Alert Schema ===
|
|
72
|
+
const hydraulicAlertSchema = object({
|
|
73
|
+
...createCommonFields(),
|
|
74
|
+
type: required(literal(ALERT_TYPES.HYDRAULIC)),
|
|
75
|
+
system: required(stringEnum<HydraulicSystem>([...HYDRAULIC_SYSTEMS], 'Invalid hydraulic system')),
|
|
76
|
+
pressure: required(chain(parseNumber(), between(0, 5000))),
|
|
77
|
+
fluidLevel: required(chain(parseNumber(), between(0, 100))),
|
|
78
|
+
temperature: required(chain(parseNumber(), between(-50, 150))),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// === Electrical System Alert Schema ===
|
|
82
|
+
const electricalAlertSchema = object({
|
|
83
|
+
...createCommonFields(),
|
|
84
|
+
type: required(literal(ALERT_TYPES.ELECTRICAL)),
|
|
85
|
+
bus: required(stringEnum<ElectricalBus>([...ELECTRICAL_BUSES], 'Invalid electrical bus')),
|
|
86
|
+
voltage: required(chain(parseNumber(), between(0, 30))),
|
|
87
|
+
current: required(chain(parseNumber(), between(0, 200))),
|
|
88
|
+
source: required(stringEnum<PowerSource>([...POWER_SOURCES], 'Invalid power source')),
|
|
89
|
+
frequency: required(chain(parseNumber(), between(380, 420))),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// === Fuel System Alert Schema ===
|
|
93
|
+
const fuelAlertSchema = object({
|
|
94
|
+
...createCommonFields(),
|
|
95
|
+
type: required(literal(ALERT_TYPES.FUEL)),
|
|
96
|
+
tank: required(stringEnum<FuelTank>([...FUEL_TANKS], 'Invalid fuel tank')),
|
|
97
|
+
quantity: required(chain(parseNumber(), between(0, 50_000))),
|
|
98
|
+
imbalance: required(boolean()),
|
|
99
|
+
consumptionRate: required(chain(parseNumber(), between(0, 10_000))),
|
|
100
|
+
estimatedEndurance: required(chain(parseNumber(), between(0, 24))),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// === Type Definitions ===
|
|
104
|
+
type EngineAlert = InferSchemaType<typeof engineAlertSchema>;
|
|
105
|
+
type HydraulicAlert = InferSchemaType<typeof hydraulicAlertSchema>;
|
|
106
|
+
type ElectricalAlert = InferSchemaType<typeof electricalAlertSchema>;
|
|
107
|
+
type FuelAlert = InferSchemaType<typeof fuelAlertSchema>;
|
|
108
|
+
|
|
109
|
+
type AlertTypes = EngineAlert | HydraulicAlert | ElectricalAlert | FuelAlert;
|
|
110
|
+
|
|
111
|
+
// === Create Discriminated Union ===
|
|
112
|
+
const completeAlertSchema = discriminatedUnion<AlertTypes>('type', {
|
|
113
|
+
[ALERT_TYPES.ENGINE]: engineAlertSchema,
|
|
114
|
+
[ALERT_TYPES.HYDRAULIC]: hydraulicAlertSchema,
|
|
115
|
+
[ALERT_TYPES.ELECTRICAL]: electricalAlertSchema,
|
|
116
|
+
[ALERT_TYPES.FUEL]: fuelAlertSchema,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
type CompleteAlertSchema = InferSchemaType<typeof completeAlertSchema>;
|
|
120
|
+
|
|
121
|
+
// Result type used in this example that includes extra display fields
|
|
122
|
+
type AlertValidationDisplay = ValidationResult<CompleteAlertSchema> & {
|
|
123
|
+
summary?: string;
|
|
124
|
+
errorCount?: number;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// === Valid Data Examples ===
|
|
128
|
+
const validEngineAlert = {
|
|
129
|
+
// Common fields
|
|
130
|
+
timestamp: new Date('2025-01-17T14:30:00Z'),
|
|
131
|
+
aircraftId: 'N-12345',
|
|
132
|
+
flightNumber: 'UA456',
|
|
133
|
+
severity: 'warning',
|
|
134
|
+
acknowledged: false,
|
|
135
|
+
// Engine-specific fields
|
|
136
|
+
type: ALERT_TYPES.ENGINE,
|
|
137
|
+
engineNumber: '2',
|
|
138
|
+
parameter: 'temperature',
|
|
139
|
+
value: 850,
|
|
140
|
+
threshold: 800,
|
|
141
|
+
exceedanceTime: 45,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const validHydraulicAlert = {
|
|
145
|
+
// Common fields
|
|
146
|
+
timestamp: new Date('2025-01-17T14:35:00Z'),
|
|
147
|
+
aircraftId: 'G-ABCD',
|
|
148
|
+
flightNumber: 'BA789',
|
|
149
|
+
severity: 'caution',
|
|
150
|
+
acknowledged: true,
|
|
151
|
+
// Hydraulic-specific fields
|
|
152
|
+
type: ALERT_TYPES.HYDRAULIC,
|
|
153
|
+
system: 'primary',
|
|
154
|
+
pressure: 2950,
|
|
155
|
+
fluidLevel: 85,
|
|
156
|
+
temperature: 45,
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// === Invalid Data Examples ===
|
|
160
|
+
const invalidAircraftIdPattern = {
|
|
161
|
+
timestamp: new Date('2025-01-17T14:40:00Z'),
|
|
162
|
+
aircraftId: '12345', // Missing prefix letter and dash
|
|
163
|
+
flightNumber: 'DL123',
|
|
164
|
+
severity: 'advisory',
|
|
165
|
+
acknowledged: false,
|
|
166
|
+
type: ALERT_TYPES.ELECTRICAL,
|
|
167
|
+
bus: 'main',
|
|
168
|
+
voltage: 28,
|
|
169
|
+
current: 50,
|
|
170
|
+
source: 'generator',
|
|
171
|
+
frequency: 400,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const invalidEngineNumber = {
|
|
175
|
+
timestamp: new Date('2025-01-17T14:45:00Z'),
|
|
176
|
+
aircraftId: 'N-98765',
|
|
177
|
+
flightNumber: 'AA100',
|
|
178
|
+
severity: 'warning',
|
|
179
|
+
acknowledged: false,
|
|
180
|
+
type: ALERT_TYPES.ENGINE,
|
|
181
|
+
engineNumber: '5', // Invalid engine number (only 1-4 allowed)
|
|
182
|
+
parameter: 'pressure',
|
|
183
|
+
value: 450,
|
|
184
|
+
threshold: 400,
|
|
185
|
+
exceedanceTime: 120,
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const invalidFuelQuantity = {
|
|
189
|
+
timestamp: new Date('2025-01-17T14:50:00Z'),
|
|
190
|
+
aircraftId: 'C-FGHI',
|
|
191
|
+
flightNumber: 'AC202',
|
|
192
|
+
severity: 'caution',
|
|
193
|
+
acknowledged: true,
|
|
194
|
+
type: ALERT_TYPES.FUEL,
|
|
195
|
+
tank: 'center',
|
|
196
|
+
quantity: 75_000, // Exceeds maximum of 50_000 pounds
|
|
197
|
+
imbalance: true,
|
|
198
|
+
consumptionRate: 5000,
|
|
199
|
+
estimatedEndurance: 10,
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const invalidAlertType = {
|
|
203
|
+
timestamp: new Date('2025-01-17T14:55:00Z'),
|
|
204
|
+
aircraftId: 'D-JKLM',
|
|
205
|
+
flightNumber: 'LH404',
|
|
206
|
+
severity: 'warning',
|
|
207
|
+
acknowledged: false,
|
|
208
|
+
type: 'navigation', // Invalid alert type
|
|
209
|
+
latitude: 45.5,
|
|
210
|
+
longitude: -73.5,
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const missingCommonField = {
|
|
214
|
+
// Missing timestamp
|
|
215
|
+
aircraftId: 'F-NOPQ',
|
|
216
|
+
flightNumber: 'AF505',
|
|
217
|
+
severity: 'advisory',
|
|
218
|
+
acknowledged: false,
|
|
219
|
+
type: ALERT_TYPES.HYDRAULIC,
|
|
220
|
+
system: 'backup',
|
|
221
|
+
pressure: 3000,
|
|
222
|
+
fluidLevel: 90,
|
|
223
|
+
temperature: 25,
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const extraFieldsInjection = {
|
|
227
|
+
// Test that extra fields are rejected
|
|
228
|
+
timestamp: new Date('2025-01-17T15:00:00Z'),
|
|
229
|
+
aircraftId: 'N-54321',
|
|
230
|
+
flightNumber: 'UA999',
|
|
231
|
+
severity: 'warning',
|
|
232
|
+
acknowledged: false,
|
|
233
|
+
type: ALERT_TYPES.ENGINE,
|
|
234
|
+
engineNumber: '1',
|
|
235
|
+
parameter: 'temperature',
|
|
236
|
+
value: 750,
|
|
237
|
+
threshold: 700,
|
|
238
|
+
exceedanceTime: 30,
|
|
239
|
+
// Attempting to inject extra fields
|
|
240
|
+
maliciousField: 'should-be-rejected',
|
|
241
|
+
__proto__: { injected: true },
|
|
242
|
+
constructor: { name: 'hack' },
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// === Validation Function ===
|
|
246
|
+
const processValidation = (data: unknown) => {
|
|
247
|
+
const validationResult = validate(data, completeAlertSchema);
|
|
248
|
+
return match<CompleteAlertSchema, ValidationError[], AlertValidationDisplay>(validationResult, {
|
|
249
|
+
ok: (validData) => ({
|
|
250
|
+
valid: true,
|
|
251
|
+
data: validData,
|
|
252
|
+
summary: `Alert processed: ${validData.type} on ${validData.aircraftId} (${validData.severity})`,
|
|
253
|
+
}),
|
|
254
|
+
err: (errors) => ({
|
|
255
|
+
valid: false,
|
|
256
|
+
errors: formatErrors(errors),
|
|
257
|
+
errorCount: errors.length,
|
|
258
|
+
}),
|
|
259
|
+
});
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// === Process and Log Results ===
|
|
263
|
+
|
|
264
|
+
console.log('=== Aircraft System Alert Validation Examples ===\n');
|
|
265
|
+
|
|
266
|
+
console.log('--- Valid Engine Alert ---');
|
|
267
|
+
const validEngineResult = processValidation(validEngineAlert);
|
|
268
|
+
console.log(validEngineResult);
|
|
269
|
+
|
|
270
|
+
console.log('--- Valid Hydraulic Alert ---');
|
|
271
|
+
const validHydraulicResult = processValidation(validHydraulicAlert);
|
|
272
|
+
console.log(validHydraulicResult);
|
|
273
|
+
|
|
274
|
+
console.log('--- Invalid: Bad Aircraft ID Pattern ---');
|
|
275
|
+
const invalidPatternResult = processValidation(invalidAircraftIdPattern);
|
|
276
|
+
console.log(invalidPatternResult);
|
|
277
|
+
|
|
278
|
+
console.log('--- Invalid: Engine Number Out of Range ---');
|
|
279
|
+
const invalidEngineResult = processValidation(invalidEngineNumber);
|
|
280
|
+
console.log(invalidEngineResult);
|
|
281
|
+
|
|
282
|
+
console.log('--- Invalid: Fuel Quantity Exceeds Limit ---');
|
|
283
|
+
const invalidFuelResult = processValidation(invalidFuelQuantity);
|
|
284
|
+
console.log(invalidFuelResult);
|
|
285
|
+
|
|
286
|
+
console.log('--- Invalid: Unknown Alert Type ---');
|
|
287
|
+
const invalidTypeResult = processValidation(invalidAlertType);
|
|
288
|
+
console.log(invalidTypeResult);
|
|
289
|
+
|
|
290
|
+
console.log('--- Invalid: Missing Common Field (timestamp) ---');
|
|
291
|
+
const missingFieldResult = processValidation(missingCommonField);
|
|
292
|
+
console.log(missingFieldResult);
|
|
293
|
+
|
|
294
|
+
console.log('--- Security Test: Extra Fields Injection ---');
|
|
295
|
+
const injectionResult = processValidation(extraFieldsInjection);
|
|
296
|
+
// After your processValidation call
|
|
297
|
+
console.log('--- Prototype Pollution Check ---');
|
|
298
|
+
console.log("Does a fresh object have 'injected'? ->", ({} as any).injected); // undefined
|
|
299
|
+
console.log("Does Object.prototype have 'injected'? ->", (Object.prototype as any).injected); // undefined
|
|
300
|
+
console.log(injectionResult);
|
|
301
|
+
console.log('Note: Extra fields should be rejected in strict mode');
|
package/package.json
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@railway-ts/pipelines",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Functional programming abstractions for TypeScript: Build robust data pipelines with schema validation, Option, Result, and railway-oriented programming.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.mjs",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.mjs",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
},
|
|
15
|
+
"./option": {
|
|
16
|
+
"types": "./dist/option/index.d.ts",
|
|
17
|
+
"import": "./dist/option/index.mjs",
|
|
18
|
+
"require": "./dist/option/index.cjs"
|
|
19
|
+
},
|
|
20
|
+
"./result": {
|
|
21
|
+
"types": "./dist/result/index.d.ts",
|
|
22
|
+
"import": "./dist/result/index.mjs",
|
|
23
|
+
"require": "./dist/result/index.cjs"
|
|
24
|
+
},
|
|
25
|
+
"./composition": {
|
|
26
|
+
"types": "./dist/composition/index.d.ts",
|
|
27
|
+
"import": "./dist/composition/index.mjs",
|
|
28
|
+
"require": "./dist/composition/index.cjs"
|
|
29
|
+
},
|
|
30
|
+
"./schema": {
|
|
31
|
+
"types": "./dist/schema/index.d.ts",
|
|
32
|
+
"import": "./dist/schema/index.mjs",
|
|
33
|
+
"require": "./dist/schema/index.cjs"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"README.md",
|
|
39
|
+
"examples"
|
|
40
|
+
],
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "bunx tsup",
|
|
43
|
+
"build:watch": "bunx tsup --watch",
|
|
44
|
+
"dev": "bunx tsup --watch",
|
|
45
|
+
"test": "bun test",
|
|
46
|
+
"test:watch": "bun test --watch",
|
|
47
|
+
"test:coverage": "bun test --coverage",
|
|
48
|
+
"test:coverage:lcov": "mkdir -p coverage && bun test --coverage --coverage-reporter=lcov",
|
|
49
|
+
"test:bail": "bun test --bail",
|
|
50
|
+
"test:fast": "bun test --bail --timeout=1000",
|
|
51
|
+
"test:pattern": "bun test --test-name-pattern",
|
|
52
|
+
"test:only": "bun test --only",
|
|
53
|
+
"test:reporter": "bun test --reporter=junit --reporter-outfile=junit.xml",
|
|
54
|
+
"lint": "bunx eslint .",
|
|
55
|
+
"lint:fix": "bunx eslint . --fix",
|
|
56
|
+
"format": "bunx prettier --write .",
|
|
57
|
+
"format:check": "bunx prettier --check .",
|
|
58
|
+
"typecheck": "bunx tsc --noEmit",
|
|
59
|
+
"prepublishOnly": "bun run typecheck && bun run lint && bun run test && bun run build",
|
|
60
|
+
"check": "bun run typecheck && bun run lint && bun run test"
|
|
61
|
+
},
|
|
62
|
+
"keywords": [
|
|
63
|
+
"typescript",
|
|
64
|
+
"functional",
|
|
65
|
+
"option",
|
|
66
|
+
"result",
|
|
67
|
+
"railway",
|
|
68
|
+
"monads",
|
|
69
|
+
"schema",
|
|
70
|
+
"pipeline"
|
|
71
|
+
],
|
|
72
|
+
"repository": {
|
|
73
|
+
"type": "git",
|
|
74
|
+
"url": "git+https://github.com/sakobu/railway-ts-pipelines.git"
|
|
75
|
+
},
|
|
76
|
+
"homepage": "https://github.com/sakobu/railway-ts-pipelines#readme",
|
|
77
|
+
"bugs": {
|
|
78
|
+
"url": "https://github.com/sakobu/railway-ts-pipelines/issues"
|
|
79
|
+
},
|
|
80
|
+
"sideEffects": false,
|
|
81
|
+
"author": "Sarkis Melkonian",
|
|
82
|
+
"license": "MIT",
|
|
83
|
+
"devDependencies": {
|
|
84
|
+
"@eslint/js": "^9.36.0",
|
|
85
|
+
"@types/bun": "latest",
|
|
86
|
+
"eslint": "^9.36.0",
|
|
87
|
+
"eslint-import-resolver-typescript": "^4.4.4",
|
|
88
|
+
"eslint-plugin-import": "^2.32.0",
|
|
89
|
+
"eslint-plugin-prettier": "^5.5.4",
|
|
90
|
+
"eslint-plugin-security": "^3.0.1",
|
|
91
|
+
"eslint-plugin-unicorn": "^61.0.2",
|
|
92
|
+
"globals": "^16.4.0",
|
|
93
|
+
"prettier": "^3.6.2",
|
|
94
|
+
"tsup": "^8.5.0",
|
|
95
|
+
"typescript-eslint": "^8.44.1"
|
|
96
|
+
},
|
|
97
|
+
"peerDependencies": {
|
|
98
|
+
"typescript": "^5"
|
|
99
|
+
}
|
|
100
|
+
}
|