@pgflow/dsl 0.0.5 → 0.0.6
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/package.json +4 -1
- package/CHANGELOG.md +0 -7
- package/__tests__/runtime/flow.test.ts +0 -121
- package/__tests__/runtime/steps.test.ts +0 -183
- package/__tests__/runtime/utils.test.ts +0 -149
- package/__tests__/types/dsl-types.test-d.ts +0 -103
- package/__tests__/types/example-flow.test-d.ts +0 -76
- package/__tests__/types/extract-flow-input.test-d.ts +0 -71
- package/__tests__/types/extract-flow-steps.test-d.ts +0 -74
- package/__tests__/types/getStepDefinition.test-d.ts +0 -65
- package/__tests__/types/step-input.test-d.ts +0 -212
- package/__tests__/types/step-output.test-d.ts +0 -55
- package/brainstorming/condition/condition-alternatives.md +0 -219
- package/brainstorming/condition/condition-with-flexibility.md +0 -303
- package/brainstorming/condition/condition.md +0 -139
- package/brainstorming/condition/implementation-plan.md +0 -372
- package/brainstorming/dsl/cli-json-schema.md +0 -225
- package/brainstorming/dsl/cli.md +0 -179
- package/brainstorming/dsl/create-compilator.md +0 -25
- package/brainstorming/dsl/dsl-analysis-2.md +0 -166
- package/brainstorming/dsl/dsl-analysis.md +0 -512
- package/brainstorming/dsl/dsl-critique.md +0 -41
- package/brainstorming/fanouts/fanout-subflows-flattened-vs-subruns.md +0 -213
- package/brainstorming/fanouts/fanouts-task-index.md +0 -150
- package/brainstorming/fanouts/fanouts-with-conditions-and-subflows.md +0 -239
- package/brainstorming/subflows/branching.ts.md +0 -38
- package/brainstorming/subflows/subflows-callbacks.ts.md +0 -124
- package/brainstorming/subflows/subflows-classes.ts.md +0 -83
- package/brainstorming/subflows/subflows-flattening-versioned.md +0 -119
- package/brainstorming/subflows/subflows-flattening.md +0 -138
- package/brainstorming/subflows/subflows.md +0 -118
- package/brainstorming/subflows/subruns-table.md +0 -282
- package/brainstorming/subflows/subruns.md +0 -315
- package/brainstorming/versioning/breaking-and-non-breaking-flow-changes.md +0 -259
- package/docs/refactor-edge-worker.md +0 -146
- package/docs/versioning.md +0 -19
- package/eslint.config.cjs +0 -22
- package/out-tsc/vitest/__tests__/runtime/flow.test.d.ts +0 -2
- package/out-tsc/vitest/__tests__/runtime/flow.test.d.ts.map +0 -1
- package/out-tsc/vitest/__tests__/runtime/steps.test.d.ts +0 -2
- package/out-tsc/vitest/__tests__/runtime/steps.test.d.ts.map +0 -1
- package/out-tsc/vitest/__tests__/runtime/utils.test.d.ts +0 -2
- package/out-tsc/vitest/__tests__/runtime/utils.test.d.ts.map +0 -1
- package/out-tsc/vitest/__tests__/types/dsl-types.test-d.d.ts +0 -2
- package/out-tsc/vitest/__tests__/types/dsl-types.test-d.d.ts.map +0 -1
- package/out-tsc/vitest/__tests__/types/example-flow.test-d.d.ts +0 -2
- package/out-tsc/vitest/__tests__/types/example-flow.test-d.d.ts.map +0 -1
- package/out-tsc/vitest/__tests__/types/extract-flow-input.test-d.d.ts +0 -2
- package/out-tsc/vitest/__tests__/types/extract-flow-input.test-d.d.ts.map +0 -1
- package/out-tsc/vitest/__tests__/types/extract-flow-steps.test-d.d.ts +0 -2
- package/out-tsc/vitest/__tests__/types/extract-flow-steps.test-d.d.ts.map +0 -1
- package/out-tsc/vitest/__tests__/types/getStepDefinition.test-d.d.ts +0 -2
- package/out-tsc/vitest/__tests__/types/getStepDefinition.test-d.d.ts.map +0 -1
- package/out-tsc/vitest/__tests__/types/step-input.test-d.d.ts +0 -2
- package/out-tsc/vitest/__tests__/types/step-input.test-d.d.ts.map +0 -1
- package/out-tsc/vitest/__tests__/types/step-output.test-d.d.ts +0 -2
- package/out-tsc/vitest/__tests__/types/step-output.test-d.d.ts.map +0 -1
- package/out-tsc/vitest/tsconfig.spec.tsbuildinfo +0 -1
- package/out-tsc/vitest/vite.config.d.ts +0 -3
- package/out-tsc/vitest/vite.config.d.ts.map +0 -1
- package/project.json +0 -28
- package/prompts/edge-worker-refactor.md +0 -105
- package/src/dsl.ts +0 -318
- package/src/example-flow.ts +0 -67
- package/src/index.ts +0 -1
- package/src/utils.ts +0 -84
- package/tsconfig.json +0 -13
- package/tsconfig.lib.json +0 -26
- package/tsconfig.spec.json +0 -35
- package/typecheck.log +0 -120
- package/vite.config.ts +0 -57
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { AnyFlow, ExtractFlowInput, Flow } from '../../src/index.ts';
|
|
2
|
-
import { describe, it, expectTypeOf } from 'vitest';
|
|
3
|
-
|
|
4
|
-
describe('ExtractFlowInput utility type', () => {
|
|
5
|
-
it('should correctly extract the input type from a flow with defined input', () => {
|
|
6
|
-
const flow = new Flow<{ userId: number; query: string }>({
|
|
7
|
-
slug: 'user_search_flow',
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
type FlowInput = ExtractFlowInput<typeof flow>;
|
|
11
|
-
|
|
12
|
-
expectTypeOf<FlowInput>().toMatchTypeOf<{
|
|
13
|
-
userId: number;
|
|
14
|
-
query: string;
|
|
15
|
-
}>();
|
|
16
|
-
|
|
17
|
-
// ensure it doesn't extract non-existent fields
|
|
18
|
-
expectTypeOf<FlowInput>().not.toMatchTypeOf<{
|
|
19
|
-
nonExistentField: number;
|
|
20
|
-
}>();
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('should work with AnyFlow', () => {
|
|
24
|
-
const anyFlow: AnyFlow = new Flow<{ data: string }>({ slug: 'any_flow' });
|
|
25
|
-
|
|
26
|
-
type ExtractedInput = ExtractFlowInput<typeof anyFlow>;
|
|
27
|
-
|
|
28
|
-
expectTypeOf<ExtractedInput>().toMatchTypeOf<any>();
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('should extract complex nested input types', () => {
|
|
32
|
-
const complexFlow = new Flow<{
|
|
33
|
-
user: {
|
|
34
|
-
id: number;
|
|
35
|
-
profile: {
|
|
36
|
-
name: string;
|
|
37
|
-
preferences: string[];
|
|
38
|
-
};
|
|
39
|
-
};
|
|
40
|
-
options: {
|
|
41
|
-
includeMeta: boolean;
|
|
42
|
-
};
|
|
43
|
-
}>({ slug: 'complex_input_flow' });
|
|
44
|
-
|
|
45
|
-
type ComplexInput = ExtractFlowInput<typeof complexFlow>;
|
|
46
|
-
|
|
47
|
-
expectTypeOf<ComplexInput>().toMatchTypeOf<{
|
|
48
|
-
user: {
|
|
49
|
-
id: number;
|
|
50
|
-
profile: {
|
|
51
|
-
name: string;
|
|
52
|
-
preferences: string[];
|
|
53
|
-
};
|
|
54
|
-
};
|
|
55
|
-
options: {
|
|
56
|
-
includeMeta: boolean;
|
|
57
|
-
};
|
|
58
|
-
}>();
|
|
59
|
-
|
|
60
|
-
// ensure it doesn't extract non-existent fields
|
|
61
|
-
expectTypeOf<ComplexInput>().not.toMatchTypeOf<{
|
|
62
|
-
user: {
|
|
63
|
-
nonExistentField: number;
|
|
64
|
-
};
|
|
65
|
-
options: {
|
|
66
|
-
nonExistentOption: string;
|
|
67
|
-
};
|
|
68
|
-
nonExistentInput: string;
|
|
69
|
-
}>();
|
|
70
|
-
});
|
|
71
|
-
});
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { Flow, type ExtractFlowSteps } from '../../src/index.ts';
|
|
2
|
-
import { describe, it, expectTypeOf } from 'vitest';
|
|
3
|
-
|
|
4
|
-
describe('ExtractFlowSteps utility type', () => {
|
|
5
|
-
it('should correctly extract steps from a flow with defined input', () => {
|
|
6
|
-
const flow = new Flow<{ userId: number }>({ slug: 'user_flow' })
|
|
7
|
-
.step({ slug: 'fetchUser' }, () => ({ name: 'John', age: 30 }))
|
|
8
|
-
.step({ slug: 'fetchPosts', dependsOn: ['fetchUser'] }, () => [
|
|
9
|
-
{ id: 1, title: 'Hello World' },
|
|
10
|
-
{ id: 2, title: 'TypeScript is Fun' },
|
|
11
|
-
]);
|
|
12
|
-
|
|
13
|
-
type Steps = ExtractFlowSteps<typeof flow>;
|
|
14
|
-
|
|
15
|
-
expectTypeOf<Steps>().toMatchTypeOf<{
|
|
16
|
-
fetchUser: { name: string; age: number };
|
|
17
|
-
fetchPosts: Array<{ id: number; title: string }>;
|
|
18
|
-
}>();
|
|
19
|
-
|
|
20
|
-
// ensure it doesn't extract non-existent fields
|
|
21
|
-
expectTypeOf<Steps>().not.toMatchTypeOf<{
|
|
22
|
-
nonExistentStep: number;
|
|
23
|
-
}>();
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('should work with AnyFlow to extract steps from a generic flow', () => {
|
|
27
|
-
const anyFlow = new Flow({ slug: 'any_flow' })
|
|
28
|
-
.step({ slug: 'step1' }, () => 42)
|
|
29
|
-
.step({ slug: 'step2' }, () => 'string value')
|
|
30
|
-
.step({ slug: 'step3' }, () => ({ complex: { nested: true } }));
|
|
31
|
-
|
|
32
|
-
type Steps = ExtractFlowSteps<typeof anyFlow>;
|
|
33
|
-
|
|
34
|
-
expectTypeOf<Steps>().toMatchTypeOf<{
|
|
35
|
-
step1: number;
|
|
36
|
-
step2: string;
|
|
37
|
-
step3: { complex: { nested: boolean } };
|
|
38
|
-
}>();
|
|
39
|
-
|
|
40
|
-
// ensure it doesn't extract non-existent fields
|
|
41
|
-
expectTypeOf<Steps>().not.toMatchTypeOf<{
|
|
42
|
-
nonExistentStep: number;
|
|
43
|
-
}>();
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('should handle empty steps correctly', () => {
|
|
47
|
-
const emptyFlow = new Flow({ slug: 'empty_flow' });
|
|
48
|
-
type Steps = ExtractFlowSteps<typeof emptyFlow>;
|
|
49
|
-
|
|
50
|
-
expectTypeOf<Steps>().toEqualTypeOf<Record<never, never>>();
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('should extract steps with primitive return types', () => {
|
|
54
|
-
const primitiveFlow = new Flow({ slug: 'primitive_flow' })
|
|
55
|
-
.step({ slug: 'numberStep' }, () => 123)
|
|
56
|
-
.step({ slug: 'stringStep' }, () => 'text')
|
|
57
|
-
.step({ slug: 'booleanStep' }, () => true)
|
|
58
|
-
.step({ slug: 'nullStep' }, () => null);
|
|
59
|
-
|
|
60
|
-
type Steps = ExtractFlowSteps<typeof primitiveFlow>;
|
|
61
|
-
|
|
62
|
-
expectTypeOf<Steps>().toMatchTypeOf<{
|
|
63
|
-
numberStep: number;
|
|
64
|
-
stringStep: string;
|
|
65
|
-
booleanStep: boolean;
|
|
66
|
-
nullStep: null;
|
|
67
|
-
}>();
|
|
68
|
-
|
|
69
|
-
// ensure it doesn't extract non-existent fields
|
|
70
|
-
expectTypeOf<Steps>().not.toMatchTypeOf<{
|
|
71
|
-
nonExistentStep: number;
|
|
72
|
-
}>();
|
|
73
|
-
});
|
|
74
|
-
});
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { Flow } from '../../src/index.ts';
|
|
2
|
-
import { it, expectTypeOf, expect } from 'vitest';
|
|
3
|
-
|
|
4
|
-
it('should correctly type step handlers when using getStepDefinition', () => {
|
|
5
|
-
const TestFlow = new Flow<{ url: string }>({ slug: 'test_flo' })
|
|
6
|
-
.step({ slug: 'root_a' }, (input) =>
|
|
7
|
-
[input.run.url, input.run.url].join(' ')
|
|
8
|
-
)
|
|
9
|
-
.step({ slug: 'root_b' }, (input) => input.run.url.length)
|
|
10
|
-
.step({ slug: 'merge', dependsOn: ['root_a', 'root_b'] }, (input) => {
|
|
11
|
-
return {
|
|
12
|
-
a_val: input.root_a,
|
|
13
|
-
b_val: input.root_b,
|
|
14
|
-
};
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
const root_a = TestFlow.getStepDefinition('root_a');
|
|
18
|
-
|
|
19
|
-
// Test root_a handler type
|
|
20
|
-
expectTypeOf(root_a.handler).toBeFunction();
|
|
21
|
-
expectTypeOf(root_a.handler).parameters.toMatchTypeOf<
|
|
22
|
-
[{ run: { url: string } }]
|
|
23
|
-
>();
|
|
24
|
-
expectTypeOf(root_a.handler).returns.toMatchTypeOf<
|
|
25
|
-
string | Promise<string>
|
|
26
|
-
>();
|
|
27
|
-
|
|
28
|
-
// Test root_b handler type
|
|
29
|
-
const root_b = TestFlow.getStepDefinition('root_b');
|
|
30
|
-
expectTypeOf(root_b.handler).toBeFunction();
|
|
31
|
-
expectTypeOf(root_b.handler).parameters.toMatchTypeOf<
|
|
32
|
-
[{ run: { url: string } }]
|
|
33
|
-
>();
|
|
34
|
-
expectTypeOf(root_b.handler).returns.toMatchTypeOf<
|
|
35
|
-
number | Promise<number>
|
|
36
|
-
>();
|
|
37
|
-
|
|
38
|
-
// Test merge handler type
|
|
39
|
-
const merge = TestFlow.getStepDefinition('merge');
|
|
40
|
-
expectTypeOf(merge.handler).toBeFunction();
|
|
41
|
-
expectTypeOf(merge.handler).parameters.toMatchTypeOf<
|
|
42
|
-
[
|
|
43
|
-
{
|
|
44
|
-
run: { url: string };
|
|
45
|
-
root_a: string;
|
|
46
|
-
root_b: number;
|
|
47
|
-
}
|
|
48
|
-
]
|
|
49
|
-
>();
|
|
50
|
-
expectTypeOf(merge.handler).returns.toMatchTypeOf<
|
|
51
|
-
| {
|
|
52
|
-
a_val: string;
|
|
53
|
-
b_val: number;
|
|
54
|
-
}
|
|
55
|
-
| Promise<{
|
|
56
|
-
a_val: string;
|
|
57
|
-
b_val: number;
|
|
58
|
-
}>
|
|
59
|
-
>();
|
|
60
|
-
|
|
61
|
-
// Test that dependencies are correctly set
|
|
62
|
-
expect(root_a.dependencies).toEqual([]);
|
|
63
|
-
expect(root_b.dependencies).toEqual([]);
|
|
64
|
-
expect(merge.dependencies).toEqual(['root_a', 'root_b']);
|
|
65
|
-
});
|
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
import { Flow, type StepInput } from '../../src/index.ts';
|
|
2
|
-
import { describe, it, expectTypeOf } from 'vitest';
|
|
3
|
-
|
|
4
|
-
describe('StepInput utility type', () => {
|
|
5
|
-
describe('for a flow without steps', () => {
|
|
6
|
-
const emptyFlow = new Flow<{ userId: number }>({ slug: 'empty_flow' });
|
|
7
|
-
type NonExistentInput = StepInput<typeof emptyFlow, 'nonExistentStep'>;
|
|
8
|
-
|
|
9
|
-
it('should contain only run input for non-existent steps', () => {
|
|
10
|
-
expectTypeOf<NonExistentInput>().toMatchTypeOf<{
|
|
11
|
-
run: { userId: number };
|
|
12
|
-
}>();
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it('should not allow extraneous keys', () => {
|
|
16
|
-
expectTypeOf<NonExistentInput>().not.toMatchTypeOf<{
|
|
17
|
-
nonExistentStep: any;
|
|
18
|
-
}>();
|
|
19
|
-
});
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
describe('for a flow with two root steps', () => {
|
|
23
|
-
const twoRootFlow = new Flow<{ baseInput: string }>({
|
|
24
|
-
slug: 'two_root_flow',
|
|
25
|
-
})
|
|
26
|
-
.step({ slug: 'step1' }, () => ({ result1: 42 }))
|
|
27
|
-
.step({ slug: 'step2' }, () => ({ result2: 'test' }));
|
|
28
|
-
|
|
29
|
-
describe('step1 input', () => {
|
|
30
|
-
type Step1Input = StepInput<typeof twoRootFlow, 'step1'>;
|
|
31
|
-
|
|
32
|
-
it('should only contain run input and not the other root step', () => {
|
|
33
|
-
expectTypeOf<Step1Input>().toMatchTypeOf<{
|
|
34
|
-
run: { baseInput: string };
|
|
35
|
-
}>();
|
|
36
|
-
expectTypeOf<Step1Input>().not.toMatchTypeOf<{
|
|
37
|
-
step2: { result2: string };
|
|
38
|
-
}>();
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('should not allow extraneous keys', () => {
|
|
42
|
-
expectTypeOf<Step1Input>().not.toMatchTypeOf<{
|
|
43
|
-
extraProperty: unknown;
|
|
44
|
-
}>();
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
describe('step2 input', () => {
|
|
49
|
-
type Step2Input = StepInput<typeof twoRootFlow, 'step2'>;
|
|
50
|
-
|
|
51
|
-
it('should only contain run input and not contain the other root step', () => {
|
|
52
|
-
expectTypeOf<Step2Input>().toMatchTypeOf<{
|
|
53
|
-
run: { baseInput: string };
|
|
54
|
-
}>();
|
|
55
|
-
expectTypeOf<Step2Input>().not.toMatchTypeOf<{
|
|
56
|
-
step1: { result1: number };
|
|
57
|
-
}>();
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('should not allow extraneous keys', () => {
|
|
61
|
-
expectTypeOf<Step2Input>().not.toMatchTypeOf<{
|
|
62
|
-
extraProperty: unknown;
|
|
63
|
-
}>();
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('should contain only run input if used for non-existent steps', () => {
|
|
68
|
-
type NonExistentInput = StepInput<typeof twoRootFlow, 'nonExistentStep'>;
|
|
69
|
-
expectTypeOf<NonExistentInput>().toMatchTypeOf<{
|
|
70
|
-
run: { baseInput: string };
|
|
71
|
-
}>();
|
|
72
|
-
expectTypeOf<NonExistentInput>().not.toMatchTypeOf<{
|
|
73
|
-
step1: any;
|
|
74
|
-
}>();
|
|
75
|
-
expectTypeOf<NonExistentInput>().not.toMatchTypeOf<{
|
|
76
|
-
step2: any;
|
|
77
|
-
}>();
|
|
78
|
-
expectTypeOf<NonExistentInput>().not.toMatchTypeOf<{
|
|
79
|
-
extraProperty: unknown;
|
|
80
|
-
}>();
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
describe('for a flow with root step and dependent steps', () => {
|
|
85
|
-
const dependentFlow = new Flow<{ userId: string }>({
|
|
86
|
-
slug: 'dependent_flow',
|
|
87
|
-
})
|
|
88
|
-
.step({ slug: 'rootStep' }, () => ({ data: 'root' }))
|
|
89
|
-
.step({ slug: 'dependent1', dependsOn: ['rootStep'] }, () => ({
|
|
90
|
-
child: 1,
|
|
91
|
-
}))
|
|
92
|
-
.step({ slug: 'dependent2', dependsOn: ['rootStep'] }, () => ({
|
|
93
|
-
child: 2,
|
|
94
|
-
}));
|
|
95
|
-
|
|
96
|
-
describe('rootStep input', () => {
|
|
97
|
-
type RootStepInput = StepInput<typeof dependentFlow, 'rootStep'>;
|
|
98
|
-
|
|
99
|
-
it('should only contain run input', () => {
|
|
100
|
-
expectTypeOf<RootStepInput>().toMatchTypeOf<{
|
|
101
|
-
run: { userId: string };
|
|
102
|
-
}>();
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('should not contain dependent1 step', () => {
|
|
106
|
-
expectTypeOf<RootStepInput>().not.toMatchTypeOf<{
|
|
107
|
-
dependent1: { child: number };
|
|
108
|
-
}>();
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('should not contain dependent2 step', () => {
|
|
112
|
-
expectTypeOf<RootStepInput>().not.toMatchTypeOf<{
|
|
113
|
-
dependent2: { child: number };
|
|
114
|
-
}>();
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('should not allow extraneous keys', () => {
|
|
118
|
-
expectTypeOf<RootStepInput>().not.toMatchTypeOf<{
|
|
119
|
-
extraProperty: unknown;
|
|
120
|
-
}>();
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
describe('dependent1 input', () => {
|
|
125
|
-
type Dependent1Input = StepInput<typeof dependentFlow, 'dependent1'>;
|
|
126
|
-
|
|
127
|
-
it('should contain run input and rootStep', () => {
|
|
128
|
-
expectTypeOf<Dependent1Input>().toMatchTypeOf<{
|
|
129
|
-
run: { userId: string };
|
|
130
|
-
rootStep: { data: string };
|
|
131
|
-
}>();
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('should not contain dependent2 step', () => {
|
|
135
|
-
expectTypeOf<Dependent1Input>().not.toMatchTypeOf<{
|
|
136
|
-
dependent2: { child: number };
|
|
137
|
-
}>();
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('should not allow extraneous keys', () => {
|
|
141
|
-
expectTypeOf<Dependent1Input>().not.toMatchTypeOf<{
|
|
142
|
-
extraProperty: unknown;
|
|
143
|
-
}>();
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
describe('dependent2 input', () => {
|
|
148
|
-
type Dependent2Input = StepInput<typeof dependentFlow, 'dependent2'>;
|
|
149
|
-
|
|
150
|
-
it('should contain run input and rootStep', () => {
|
|
151
|
-
expectTypeOf<Dependent2Input>().toMatchTypeOf<{
|
|
152
|
-
run: { userId: string };
|
|
153
|
-
rootStep: { data: string };
|
|
154
|
-
}>();
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('should not contain dependent1 step', () => {
|
|
158
|
-
expectTypeOf<Dependent2Input>().not.toMatchTypeOf<{
|
|
159
|
-
dependent1: { child: number };
|
|
160
|
-
}>();
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it('should not allow extraneous keys', () => {
|
|
164
|
-
expectTypeOf<Dependent2Input>().not.toMatchTypeOf<{
|
|
165
|
-
extraProperty: unknown;
|
|
166
|
-
}>();
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
describe('for a flow with multiple dependencies', () => {
|
|
172
|
-
const complexFlow = new Flow<{ initial: boolean }>({
|
|
173
|
-
slug: 'complex_flow',
|
|
174
|
-
})
|
|
175
|
-
.step({ slug: 'step1' }, () => ({ val1: 'a' }))
|
|
176
|
-
.step({ slug: 'step2' }, () => ({ val2: 'b' }))
|
|
177
|
-
.step({ slug: 'step3', dependsOn: ['step1', 'step2'] }, () => ({
|
|
178
|
-
val3: 'c',
|
|
179
|
-
}));
|
|
180
|
-
|
|
181
|
-
describe('step3 input with multiple dependencies', () => {
|
|
182
|
-
type Step3Input = StepInput<typeof complexFlow, 'step3'>;
|
|
183
|
-
|
|
184
|
-
it('should contain run input and both dependencies', () => {
|
|
185
|
-
expectTypeOf<Step3Input>().toMatchTypeOf<{
|
|
186
|
-
run: { initial: boolean };
|
|
187
|
-
step1: { val1: string };
|
|
188
|
-
step2: { val2: string };
|
|
189
|
-
}>();
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it('should not allow extraneous keys', () => {
|
|
193
|
-
expectTypeOf<Step3Input>().not.toMatchTypeOf<{
|
|
194
|
-
extraProperty: unknown;
|
|
195
|
-
}>();
|
|
196
|
-
});
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it('should contain only run input for non-existent steps', () => {
|
|
200
|
-
type NonExistentInput = StepInput<typeof complexFlow, 'nonExistentStep'>;
|
|
201
|
-
expectTypeOf<NonExistentInput>().toMatchTypeOf<{
|
|
202
|
-
run: { initial: boolean };
|
|
203
|
-
}>();
|
|
204
|
-
expectTypeOf<NonExistentInput>().not.toMatchTypeOf<{
|
|
205
|
-
step1: { val1: string };
|
|
206
|
-
}>();
|
|
207
|
-
expectTypeOf<NonExistentInput>().not.toMatchTypeOf<{
|
|
208
|
-
step2: { val2: string };
|
|
209
|
-
}>();
|
|
210
|
-
});
|
|
211
|
-
});
|
|
212
|
-
});
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { Flow, type StepOutput } from '../../src/index.ts';
|
|
2
|
-
import { describe, it, expectTypeOf } from 'vitest';
|
|
3
|
-
|
|
4
|
-
describe('StepOutput utility type', () => {
|
|
5
|
-
it('should correctly extract the output type of a step', () => {
|
|
6
|
-
const flow = new Flow<{ input: string }>({ slug: 'step_output_test' })
|
|
7
|
-
.step({ slug: 'step1' }, () => ({ value: 42, text: 'hello' }))
|
|
8
|
-
.step({ slug: 'step2', dependsOn: ['step1'] }, () => ({ flag: true }))
|
|
9
|
-
.step({ slug: 'step3' }, () => 'plain string');
|
|
10
|
-
|
|
11
|
-
type Step1Output = StepOutput<typeof flow, 'step1'>;
|
|
12
|
-
|
|
13
|
-
expectTypeOf<Step1Output>().toMatchTypeOf<{
|
|
14
|
-
value: number;
|
|
15
|
-
text: string;
|
|
16
|
-
}>();
|
|
17
|
-
|
|
18
|
-
type Step2Output = StepOutput<typeof flow, 'step2'>;
|
|
19
|
-
expectTypeOf<Step2Output>().toMatchTypeOf<{ flag: boolean }>();
|
|
20
|
-
|
|
21
|
-
type Step3Output = StepOutput<typeof flow, 'step3'>;
|
|
22
|
-
expectTypeOf<Step3Output>().toMatchTypeOf<string>();
|
|
23
|
-
|
|
24
|
-
type NonExistentOutput = StepOutput<typeof flow, 'nonExistentStep'>;
|
|
25
|
-
expectTypeOf<NonExistentOutput>().toMatchTypeOf<never>();
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('should work with complex nested types', () => {
|
|
29
|
-
const complexFlow = new Flow<{ id: number }>({
|
|
30
|
-
slug: 'complex_flow',
|
|
31
|
-
}).step({ slug: 'complexStep' }, () => ({
|
|
32
|
-
data: {
|
|
33
|
-
items: [
|
|
34
|
-
{ id: 1, name: 'Item 1' },
|
|
35
|
-
{ id: 2, name: 'Item 2' },
|
|
36
|
-
],
|
|
37
|
-
metadata: {
|
|
38
|
-
count: 2,
|
|
39
|
-
lastUpdated: '2023-01-01',
|
|
40
|
-
},
|
|
41
|
-
},
|
|
42
|
-
}));
|
|
43
|
-
|
|
44
|
-
type ComplexStepOutput = StepOutput<typeof complexFlow, 'complexStep'>;
|
|
45
|
-
expectTypeOf<ComplexStepOutput>().toMatchTypeOf<{
|
|
46
|
-
data: {
|
|
47
|
-
items: Array<{ id: number; name: string }>;
|
|
48
|
-
metadata: {
|
|
49
|
-
count: number;
|
|
50
|
-
lastUpdated: string;
|
|
51
|
-
};
|
|
52
|
-
};
|
|
53
|
-
}>();
|
|
54
|
-
});
|
|
55
|
-
});
|
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
## Brainstorming Alternatives to JSON Containment `@>` for Step Conditions
|
|
2
|
-
|
|
3
|
-
Below are various ideas for how to define and evaluate “conditions” on a step’s input, while still satisfying the constraints you outlined. Each approach aims to remain statically typed, only match the step’s input, be serializable to JSON, and avoid implementing a new DSL or separate custom operators.
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
### 1. Use PostgreSQL JSONPath Evaluations
|
|
8
|
-
Postgres supports JSONPath queries via functions like [`jsonb_path_exists`](https://www.postgresql.org/docs/current/functions-json.html#FUNCTIONS-SQLJSON-PATH). You could store the JSONPath expression in a JSON object and then evaluate it against the step’s combined input at runtime:
|
|
9
|
-
|
|
10
|
-
```sql
|
|
11
|
-
-- Step condition data stored as JSON:
|
|
12
|
-
-- e.g. { "type": "jsonpath", "expression": "$.website.content ? (@ like_regex \"^Hello.*\")" }
|
|
13
|
-
|
|
14
|
-
SELECT jsonb_path_exists(
|
|
15
|
-
payload,
|
|
16
|
-
condition->>'expression'
|
|
17
|
-
);
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
**Characteristics**
|
|
21
|
-
- **Statically typed**: You can maintain a TypeScript type that restricts which property paths are valid, for example with a string-literal type for domain-specific JSONPath expressions.
|
|
22
|
-
- **Single function**: PostgreSQL has a single function to evaluate arbitrary JSONPath expressions, so you don’t need to manually implement each comparison.
|
|
23
|
-
- **JSON-based**: The condition can be stored as JSON with a `"jsonpath"` key.
|
|
24
|
-
- **No raw SQL**: The `"expression"` is still a string, but the actual caller is the built-in `jsonb_path_exists`, which means you do not need to craft dynamic SQL.
|
|
25
|
-
|
|
26
|
-
**Downsides**
|
|
27
|
-
- Complex textual JSONPath expressions can be tricky to type-check at compile time unless you build advanced TS generics.
|
|
28
|
-
|
|
29
|
-
---
|
|
30
|
-
|
|
31
|
-
### 2. Embed a “Condition Schema” with Built-in Operators
|
|
32
|
-
You can store a small “condition schema” (like an AST) in JSON, referencing the step’s typed properties. A single Postgres function interprets that schema and performs the check. The schema might look like:
|
|
33
|
-
|
|
34
|
-
```json
|
|
35
|
-
{
|
|
36
|
-
"type": "comparison",
|
|
37
|
-
"lhsPath": ["website", "contentLength"],
|
|
38
|
-
"operator": "gt",
|
|
39
|
-
"rhsValue": 1000
|
|
40
|
-
}
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
Then at runtime:
|
|
44
|
-
|
|
45
|
-
```sql
|
|
46
|
-
-- Hypothetical function:
|
|
47
|
-
-- `evaluate_condition(payload JSONB, condition JSONB) RETURNS bool`
|
|
48
|
-
-- that interprets the "operator" string and does the check without separate DSL code.
|
|
49
|
-
|
|
50
|
-
SELECT evaluate_condition(payload, condition);
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
**Characteristics**
|
|
54
|
-
- **Statically typed**: In TypeScript, you can ensure `lhsPath` only references valid properties of the known input shape.
|
|
55
|
-
- **Single function**: All operators (`gt`, `eq`, `like`, etc.) are recognized by a single evaluator function inside Postgres.
|
|
56
|
-
- **JSON-based**: The entire condition is a JSON object.
|
|
57
|
-
- **No re-implementing operators**: You do need to parse `condition->>'operator'` but only in one place. Any new operator is recognized by the single function’s branching logic.
|
|
58
|
-
|
|
59
|
-
**Downsides**
|
|
60
|
-
- The internal function that interprets `operator` must handle each operator branch. However, that is one single location, and you don’t need a sprawling DSL – just a small piece of code.
|
|
61
|
-
|
|
62
|
-
---
|
|
63
|
-
|
|
64
|
-
### 3. Storing an Array of “Partial Objects” to Match with `@>`
|
|
65
|
-
Instead of a single containment check, you could chain multiple partial objects in an array, each of which must match:
|
|
66
|
-
|
|
67
|
-
```json
|
|
68
|
-
[
|
|
69
|
-
{ "website": { "status": 200 } },
|
|
70
|
-
{ "run": { "isInternal": true } }
|
|
71
|
-
]
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
Then your condition means “the step’s input must contain all keys from both listed partial objects.” You could combine them as a single structure inside TS or keep them separate. Evaluating is done in Postgres via multiple `@>` calls:
|
|
75
|
-
|
|
76
|
-
```sql
|
|
77
|
-
SELECT payload @> condition_item
|
|
78
|
-
FROM jsonb_array_elements(condition) AS condition_item
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
**Characteristics**
|
|
82
|
-
- **Statically typed**: TypeScript can ensure the partial objects only contain valid paths from your input shape.
|
|
83
|
-
- **Extremely simple**: Reuses Postgres’s built-in `@>` for partial matching.
|
|
84
|
-
- **Chaining**: Must pass all partials.
|
|
85
|
-
- **JSON-based**: No custom SQL or DSL.
|
|
86
|
-
|
|
87
|
-
**Downsides**
|
|
88
|
-
- This only gives you “containment” checks. More advanced comparisons like numeric ranges or `LIKE` patterns aren’t well supported out of the box.
|
|
89
|
-
- No direct way to handle “not equals” or more sophisticated conditions without reintroducing new fields.
|
|
90
|
-
|
|
91
|
-
---
|
|
92
|
-
|
|
93
|
-
### 4. Evaluate Conditions via JSON Schema (Using `is_jsonb_valid`)
|
|
94
|
-
You can represent certain conditions as a subset of [JSON Schema](https://json-schema.org/) (or a homegrown schema approach). Then store that schema as JSON. A single function in Postgres can validate the “payload” against that schema:
|
|
95
|
-
|
|
96
|
-
```json
|
|
97
|
-
{
|
|
98
|
-
"type": "object",
|
|
99
|
-
"properties": {
|
|
100
|
-
"website": {
|
|
101
|
-
"type": "object",
|
|
102
|
-
"properties": {
|
|
103
|
-
"status": { "enum": [200, 201] }
|
|
104
|
-
},
|
|
105
|
-
"required": ["status"]
|
|
106
|
-
}
|
|
107
|
-
},
|
|
108
|
-
"required": ["website"]
|
|
109
|
-
}
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
Then use a Postgres extension or a custom function to do a JSON Schema validation:
|
|
113
|
-
|
|
114
|
-
```sql
|
|
115
|
-
SELECT is_jsonb_valid(payload, condition_schema);
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
**Characteristics**
|
|
119
|
-
- **Statically typed**: You can restrict the JSON Schema to only describe your known shape so it can’t mention nonexistent keys.
|
|
120
|
-
- **Featureful**: JSON Schema has a wide array of constraints (enum, minLength, pattern, etc.).
|
|
121
|
-
- **No manual operator code**: The library handles it.
|
|
122
|
-
- **JSON-based**: The schema is stored as JSON.
|
|
123
|
-
|
|
124
|
-
**Downsides**
|
|
125
|
-
- Requires a JSON Schema validator that runs inside Postgres or as part of your application logic. Specialized operators are needed if you want it fully inside PG.
|
|
126
|
-
- Some performance overhead for complex schemas.
|
|
127
|
-
|
|
128
|
-
---
|
|
129
|
-
|
|
130
|
-
### 5. Combine Paths + Operator + Value, Using Runtime “SQL Expression” Generation WITHOUT Exposing SQL
|
|
131
|
-
Use a TS type that enforces a limited set of operators as strings. The condition remains in JSON form:
|
|
132
|
-
|
|
133
|
-
```ts
|
|
134
|
-
type AllowedOperator = '>' | '<' | '=' | 'LIKE';
|
|
135
|
-
type ConditionPart = {
|
|
136
|
-
path: string[]; // e.g. ["website", "status"]
|
|
137
|
-
operator: AllowedOperator;
|
|
138
|
-
compareValue: string | number;
|
|
139
|
-
};
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
**In JSON**:
|
|
143
|
-
```json
|
|
144
|
-
{
|
|
145
|
-
"path": ["website", "status"],
|
|
146
|
-
"operator": ">",
|
|
147
|
-
"compareValue": 199
|
|
148
|
-
}
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
You can store multiple ConditionParts, interpret them in a single function, and convert them to a safe, typed expression. For example:
|
|
152
|
-
|
|
153
|
-
```sql
|
|
154
|
-
-- Pseudocode
|
|
155
|
-
PERFORM safe_evaluate(payload, condition);
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
**Characteristics**
|
|
159
|
-
- **Statically typed**: TS ensures `path` only references known fields, `operator` is in an allowed set, etc.
|
|
160
|
-
- **Single function**: You do not manually code for each operator in the DSL. You do one parse logic in `safe_evaluate(...)`.
|
|
161
|
-
- **No raw SQL strings**: The “operator” is a typed string, not user-supplied free text; you escape or map it carefully so you don’t run insecure dynamic SQL.
|
|
162
|
-
- **JSON-based**: Clear JSON serialization.
|
|
163
|
-
|
|
164
|
-
**Downsides**
|
|
165
|
-
- Does require a small “mapping table” from the typed `AllowedOperator` to the actual Postgres operator.
|
|
166
|
-
- Only covers the operators you’ve enumerated; custom logic might require expansions to the typed list.
|
|
167
|
-
|
|
168
|
-
---
|
|
169
|
-
|
|
170
|
-
### 6. Rely on Built-In Comparisons of JSON Path Query with `vars`
|
|
171
|
-
Another variant of the JSONPath idea: `jsonb_path_exists` can be passed external variables. You’d store the JSONPath expression plus a dictionary of variables in your condition. Example:
|
|
172
|
-
|
|
173
|
-
```json
|
|
174
|
-
{
|
|
175
|
-
"expression": "$.website.status ? (@ == $statusVal)",
|
|
176
|
-
"vars": { "statusVal": 200 }
|
|
177
|
-
}
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
At runtime:
|
|
181
|
-
|
|
182
|
-
```sql
|
|
183
|
-
SELECT jsonb_path_exists(
|
|
184
|
-
payload,
|
|
185
|
-
condition->>'expression',
|
|
186
|
-
condition->'vars'
|
|
187
|
-
);
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
**Characteristics**
|
|
191
|
-
- **Statically typed**: In TS, you can ensure that `vars` align with the expression’s placeholders, tying them to the known step input shape.
|
|
192
|
-
- **Single function**: Again, calls the built-in JSONPath mechanism.
|
|
193
|
-
- **No custom DSL**: The only “DSL” is standard JSONPath strings.
|
|
194
|
-
|
|
195
|
-
**Downsides**
|
|
196
|
-
- You still rely on textual quoting inside the JSONPath expression. That can be type-safe in TS if you codify it carefully.
|
|
197
|
-
|
|
198
|
-
---
|
|
199
|
-
|
|
200
|
-
### Balancing Statically Typed Constraints with JSON Storage
|
|
201
|
-
|
|
202
|
-
All of the above illustrate slightly different ways to store a typed “match rule” in JSON. Whichever approach you choose, the key is to:
|
|
203
|
-
|
|
204
|
-
1. **Enforce at compile time** (TypeScript) that only valid property paths and operators can appear in the condition object.
|
|
205
|
-
2. **Serialize the condition object as JSON** for storing in the database.
|
|
206
|
-
3. **Use a single “evaluation function” in Postgres** (or the existing built-in JSONPath / partial matching) so you don’t re-implement each operator as a standalone DSL.
|
|
207
|
-
|
|
208
|
-
This lets you stay flexible but also safe and maintainable.
|
|
209
|
-
|
|
210
|
-
---
|
|
211
|
-
|
|
212
|
-
### Final Thoughts
|
|
213
|
-
|
|
214
|
-
- **For simple presence checks**: Using partial object matching with `@>` is already a good minimal solution, but it can’t handle numeric comparisons or negations without manual expansions.
|
|
215
|
-
- **For advanced logic**: Harnessing JSONPath (`jsonb_path_exists`) or a small “comparison schema” can yield more expressive power without building a custom DSL.
|
|
216
|
-
- **TypeScript Generics**: You can generate path-literal types from your known input shape, ensuring you never store an invalid path in the condition.
|
|
217
|
-
- **Single Evaluation**: Keep a single place in Postgres that does the real check. This means you don’t have to maintain multiple code paths or define new DSL operators every time.
|
|
218
|
-
|
|
219
|
-
These strategies collectively satisfy all the “non-negotiable” points, while allowing you to encode robust dynamic conditions in a typed manner and store them as JSON for runtime.
|