@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.
Files changed (71) hide show
  1. package/package.json +4 -1
  2. package/CHANGELOG.md +0 -7
  3. package/__tests__/runtime/flow.test.ts +0 -121
  4. package/__tests__/runtime/steps.test.ts +0 -183
  5. package/__tests__/runtime/utils.test.ts +0 -149
  6. package/__tests__/types/dsl-types.test-d.ts +0 -103
  7. package/__tests__/types/example-flow.test-d.ts +0 -76
  8. package/__tests__/types/extract-flow-input.test-d.ts +0 -71
  9. package/__tests__/types/extract-flow-steps.test-d.ts +0 -74
  10. package/__tests__/types/getStepDefinition.test-d.ts +0 -65
  11. package/__tests__/types/step-input.test-d.ts +0 -212
  12. package/__tests__/types/step-output.test-d.ts +0 -55
  13. package/brainstorming/condition/condition-alternatives.md +0 -219
  14. package/brainstorming/condition/condition-with-flexibility.md +0 -303
  15. package/brainstorming/condition/condition.md +0 -139
  16. package/brainstorming/condition/implementation-plan.md +0 -372
  17. package/brainstorming/dsl/cli-json-schema.md +0 -225
  18. package/brainstorming/dsl/cli.md +0 -179
  19. package/brainstorming/dsl/create-compilator.md +0 -25
  20. package/brainstorming/dsl/dsl-analysis-2.md +0 -166
  21. package/brainstorming/dsl/dsl-analysis.md +0 -512
  22. package/brainstorming/dsl/dsl-critique.md +0 -41
  23. package/brainstorming/fanouts/fanout-subflows-flattened-vs-subruns.md +0 -213
  24. package/brainstorming/fanouts/fanouts-task-index.md +0 -150
  25. package/brainstorming/fanouts/fanouts-with-conditions-and-subflows.md +0 -239
  26. package/brainstorming/subflows/branching.ts.md +0 -38
  27. package/brainstorming/subflows/subflows-callbacks.ts.md +0 -124
  28. package/brainstorming/subflows/subflows-classes.ts.md +0 -83
  29. package/brainstorming/subflows/subflows-flattening-versioned.md +0 -119
  30. package/brainstorming/subflows/subflows-flattening.md +0 -138
  31. package/brainstorming/subflows/subflows.md +0 -118
  32. package/brainstorming/subflows/subruns-table.md +0 -282
  33. package/brainstorming/subflows/subruns.md +0 -315
  34. package/brainstorming/versioning/breaking-and-non-breaking-flow-changes.md +0 -259
  35. package/docs/refactor-edge-worker.md +0 -146
  36. package/docs/versioning.md +0 -19
  37. package/eslint.config.cjs +0 -22
  38. package/out-tsc/vitest/__tests__/runtime/flow.test.d.ts +0 -2
  39. package/out-tsc/vitest/__tests__/runtime/flow.test.d.ts.map +0 -1
  40. package/out-tsc/vitest/__tests__/runtime/steps.test.d.ts +0 -2
  41. package/out-tsc/vitest/__tests__/runtime/steps.test.d.ts.map +0 -1
  42. package/out-tsc/vitest/__tests__/runtime/utils.test.d.ts +0 -2
  43. package/out-tsc/vitest/__tests__/runtime/utils.test.d.ts.map +0 -1
  44. package/out-tsc/vitest/__tests__/types/dsl-types.test-d.d.ts +0 -2
  45. package/out-tsc/vitest/__tests__/types/dsl-types.test-d.d.ts.map +0 -1
  46. package/out-tsc/vitest/__tests__/types/example-flow.test-d.d.ts +0 -2
  47. package/out-tsc/vitest/__tests__/types/example-flow.test-d.d.ts.map +0 -1
  48. package/out-tsc/vitest/__tests__/types/extract-flow-input.test-d.d.ts +0 -2
  49. package/out-tsc/vitest/__tests__/types/extract-flow-input.test-d.d.ts.map +0 -1
  50. package/out-tsc/vitest/__tests__/types/extract-flow-steps.test-d.d.ts +0 -2
  51. package/out-tsc/vitest/__tests__/types/extract-flow-steps.test-d.d.ts.map +0 -1
  52. package/out-tsc/vitest/__tests__/types/getStepDefinition.test-d.d.ts +0 -2
  53. package/out-tsc/vitest/__tests__/types/getStepDefinition.test-d.d.ts.map +0 -1
  54. package/out-tsc/vitest/__tests__/types/step-input.test-d.d.ts +0 -2
  55. package/out-tsc/vitest/__tests__/types/step-input.test-d.d.ts.map +0 -1
  56. package/out-tsc/vitest/__tests__/types/step-output.test-d.d.ts +0 -2
  57. package/out-tsc/vitest/__tests__/types/step-output.test-d.d.ts.map +0 -1
  58. package/out-tsc/vitest/tsconfig.spec.tsbuildinfo +0 -1
  59. package/out-tsc/vitest/vite.config.d.ts +0 -3
  60. package/out-tsc/vitest/vite.config.d.ts.map +0 -1
  61. package/project.json +0 -28
  62. package/prompts/edge-worker-refactor.md +0 -105
  63. package/src/dsl.ts +0 -318
  64. package/src/example-flow.ts +0 -67
  65. package/src/index.ts +0 -1
  66. package/src/utils.ts +0 -84
  67. package/tsconfig.json +0 -13
  68. package/tsconfig.lib.json +0 -26
  69. package/tsconfig.spec.json +0 -35
  70. package/typecheck.log +0 -120
  71. 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.