cogfy-data-exchange 1.0.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/.github/workflows/tests.yml +21 -0
- package/README.md +176 -0
- package/biome.json +73 -0
- package/package.json +20 -0
- package/src/__tests__/create-flow.test.ts +68 -0
- package/src/create-flow.ts +26 -0
- package/src/index.ts +2 -0
- package/src/types/flow-json.ts +15 -0
- package/src/types/index.ts +7 -0
- package/src/types/result.ts +29 -0
- package/src/types/routing.ts +7 -0
- package/src/types/schema.ts +13 -0
- package/src/types/screen-handler.ts +7 -0
- package/src/types/screen.ts +11 -0
- package/src/types/trigger.ts +9 -0
- package/tsconfig.json +12 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
on: [push]
|
|
2
|
+
|
|
3
|
+
concurrency:
|
|
4
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
5
|
+
cancel-in-progress: true
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
tests:
|
|
9
|
+
timeout-minutes: 8
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: actions/setup-node@v4
|
|
14
|
+
with:
|
|
15
|
+
node-version: 20.x
|
|
16
|
+
cache: npm
|
|
17
|
+
cache-dependency-path: package-lock.json
|
|
18
|
+
- run: npm ci
|
|
19
|
+
- run: npm run build
|
|
20
|
+
- run: npm run lint
|
|
21
|
+
- run: npm run test
|
package/README.md
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Cogfy Data Exchange
|
|
2
|
+
|
|
3
|
+
Uma biblioteca TypeScript para definição e execução de fluxos de navegação fortemente tipados.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Visão Geral
|
|
8
|
+
|
|
9
|
+
O **cogfy-data-exchange** permite definir fluxos (flows) baseados em telas (screens) com:
|
|
10
|
+
|
|
11
|
+
- Tipagem forte em tempo de compilação
|
|
12
|
+
- Controle de navegação entre telas
|
|
13
|
+
- Inferência automática de dados de entrada e saída
|
|
14
|
+
- Engine simples para execução (`createFlow`)
|
|
15
|
+
|
|
16
|
+
A ideia central é transformar um JSON de fluxo em uma máquina de estados tipada.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Conceitos
|
|
21
|
+
|
|
22
|
+
### Flow
|
|
23
|
+
|
|
24
|
+
Um flow descreve:
|
|
25
|
+
|
|
26
|
+
- `routing_model`: quais telas podem navegar para quais
|
|
27
|
+
- `screens`: quais dados cada tela recebe/retorna
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
const flow = {
|
|
31
|
+
routing_model: {
|
|
32
|
+
SCREEN_A: ['SCREEN_B', 'SCREEN_C'],
|
|
33
|
+
SCREEN_B: ['SCREEN_D'],
|
|
34
|
+
SCREEN_C: ['SCREEN_D'],
|
|
35
|
+
SCREEN_D: []
|
|
36
|
+
},
|
|
37
|
+
screens: [
|
|
38
|
+
{ id: 'SCREEN_A', data: { name: { type: 'string' } } },
|
|
39
|
+
{ id: 'SCREEN_B', data: { age: { type: 'number' } } },
|
|
40
|
+
{ id: 'SCREEN_C', data: { isActive: { type: 'boolean' } } },
|
|
41
|
+
{
|
|
42
|
+
id: 'SCREEN_D',
|
|
43
|
+
data: {
|
|
44
|
+
profile: {
|
|
45
|
+
type: 'object',
|
|
46
|
+
properties: {
|
|
47
|
+
name: { type: 'string' },
|
|
48
|
+
age: { type: 'number' }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
} as const
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Convenção de nomenclatura do trigger
|
|
58
|
+
Os triggers de navegação devem seguir a convenção `screen.{SCREEN_ID}`. Por exemplo, para `SCREEN_A`, o trigger seria `screen.SCREEN_A`.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Uso
|
|
63
|
+
|
|
64
|
+
### 1. Criar o flow
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { createFlow } from 'cogfy-data-exchange'
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 2. Definir handlers
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
const engine = createFlow(flow, {
|
|
74
|
+
SCREEN_A: async (data) => {
|
|
75
|
+
// data: { name: string }
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
screen: 'SCREEN_B',
|
|
79
|
+
data: { age: 30 }
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
SCREEN_B: async (data) => {
|
|
84
|
+
return {
|
|
85
|
+
screen: 'SCREEN_D',
|
|
86
|
+
data: {
|
|
87
|
+
profile: { name: 'John', age: 20 }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
SCREEN_C: async (data) => {
|
|
93
|
+
return {
|
|
94
|
+
screen: 'SCREEN_D',
|
|
95
|
+
data: {
|
|
96
|
+
profile: { name: 'Jane', age: 25 }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
SCREEN_D: async () => {
|
|
102
|
+
throw new Error('Fim do fluxo')
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### 3. Executar o flow
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
const result = await engine.dispatch('screen.SCREEN_A', { name: 'John' })
|
|
111
|
+
|
|
112
|
+
// result:
|
|
113
|
+
// {
|
|
114
|
+
// screen: 'SCREEN_B' | 'SCREEN_C'
|
|
115
|
+
// data: ...
|
|
116
|
+
// }
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Inferência de Tipos
|
|
122
|
+
|
|
123
|
+
A biblioteca infere automaticamente os seguintes aspectos:
|
|
124
|
+
|
|
125
|
+
**Entrada do handler**
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
SCREEN_A → { name: string }
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Próximas telas possíveis**
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
SCREEN_A → 'SCREEN_B' | 'SCREEN_C'
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**Dados da próxima tela**
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
SCREEN_B → { age: number }
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Garantias de Tipo
|
|
146
|
+
|
|
147
|
+
A biblioteca garante em tempo de compilação:
|
|
148
|
+
|
|
149
|
+
- Navegação restrita às telas válidas definidas no `routing_model`
|
|
150
|
+
- Formato correto do campo `data` em cada transição
|
|
151
|
+
- Implementação obrigatória de todos os handlers
|
|
152
|
+
- Tipos sempre sincronizados com a definição do flow
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Exemplos Inválidos
|
|
157
|
+
|
|
158
|
+
**Navegação inválida**
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
// Erro de tipo: SCREEN_D não é uma transição permitida a partir de SCREEN_A
|
|
162
|
+
return {
|
|
163
|
+
screen: 'SCREEN_D',
|
|
164
|
+
data: {}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**Data inválido**
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
// Erro de tipo: age deve ser do tipo number
|
|
172
|
+
return {
|
|
173
|
+
screen: 'SCREEN_B',
|
|
174
|
+
data: { age: 'wrong' }
|
|
175
|
+
}
|
|
176
|
+
```
|
package/biome.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"assist": {
|
|
3
|
+
"actions": {
|
|
4
|
+
"source": {
|
|
5
|
+
"organizeImports": "on",
|
|
6
|
+
"useSortedAttributes": "on",
|
|
7
|
+
"useSortedKeys": "on",
|
|
8
|
+
"useSortedProperties": "on"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"enabled": true
|
|
12
|
+
},
|
|
13
|
+
"files": {
|
|
14
|
+
"ignoreUnknown": false,
|
|
15
|
+
"includes": [
|
|
16
|
+
"./**/*.ts",
|
|
17
|
+
"./**/*.tsx",
|
|
18
|
+
"./**/*.js",
|
|
19
|
+
"./**/*.jsx",
|
|
20
|
+
"./**/*.json",
|
|
21
|
+
"!**/build/*",
|
|
22
|
+
"!package.json"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"formatter": {
|
|
26
|
+
"enabled": true,
|
|
27
|
+
"formatWithErrors": false,
|
|
28
|
+
"indentStyle": "space",
|
|
29
|
+
"indentWidth": 2,
|
|
30
|
+
"lineEnding": "lf",
|
|
31
|
+
"lineWidth": 120
|
|
32
|
+
},
|
|
33
|
+
"javascript": {
|
|
34
|
+
"formatter": {
|
|
35
|
+
"arrowParentheses": "always",
|
|
36
|
+
"bracketSpacing": true,
|
|
37
|
+
"quoteProperties": "asNeeded",
|
|
38
|
+
"quoteStyle": "single",
|
|
39
|
+
"semicolons": "asNeeded",
|
|
40
|
+
"trailingCommas": "none"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"json": {
|
|
44
|
+
"formatter": {
|
|
45
|
+
"lineWidth": 150
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"linter": {
|
|
49
|
+
"enabled": true,
|
|
50
|
+
"rules": {
|
|
51
|
+
"correctness": {
|
|
52
|
+
"noUnreachableSuper": "error",
|
|
53
|
+
"noUnusedVariables": "warn"
|
|
54
|
+
},
|
|
55
|
+
"recommended": true,
|
|
56
|
+
"style": {
|
|
57
|
+
"noEnum": "on",
|
|
58
|
+
"useBlockStatements": {
|
|
59
|
+
"fix": "safe",
|
|
60
|
+
"level": "warn"
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"suspicious": {
|
|
64
|
+
"noConstEnum": "off"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"vcs": {
|
|
69
|
+
"clientKind": "git",
|
|
70
|
+
"enabled": true,
|
|
71
|
+
"useIgnoreFile": true
|
|
72
|
+
}
|
|
73
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cogfy-data-exchange",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "tsc",
|
|
8
|
+
"lint": "biome lint",
|
|
9
|
+
"test": "vitest run"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [],
|
|
12
|
+
"author": "",
|
|
13
|
+
"license": "UNLICENSED",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@biomejs/biome": "2.4.8",
|
|
17
|
+
"typescript": "^5.9.3",
|
|
18
|
+
"vitest": "^4.1.0"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, test } from 'vitest'
|
|
2
|
+
import { createFlow } from '../create-flow'
|
|
3
|
+
import type { FlowJson } from '../types'
|
|
4
|
+
|
|
5
|
+
const flow = {
|
|
6
|
+
routing_model: {
|
|
7
|
+
SCREEN_A: ['SCREEN_B', 'SCREEN_C'],
|
|
8
|
+
SCREEN_B: ['SCREEN_D'],
|
|
9
|
+
SCREEN_C: ['SCREEN_D'],
|
|
10
|
+
SCREEN_D: []
|
|
11
|
+
},
|
|
12
|
+
screens: [
|
|
13
|
+
{ data: { name: { type: 'string' } }, id: 'SCREEN_A' },
|
|
14
|
+
{ data: { age: { type: 'number' } }, id: 'SCREEN_B' },
|
|
15
|
+
{ data: { isActive: { type: 'boolean' } }, id: 'SCREEN_C' },
|
|
16
|
+
{
|
|
17
|
+
data: {
|
|
18
|
+
profile: {
|
|
19
|
+
properties: {
|
|
20
|
+
age: { type: 'number' },
|
|
21
|
+
name: { type: 'string' }
|
|
22
|
+
},
|
|
23
|
+
type: 'object'
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
id: 'SCREEN_D'
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
} as const satisfies FlowJson
|
|
30
|
+
|
|
31
|
+
describe('createFlow', () => {
|
|
32
|
+
test('should dispatch screens correctly', async ({ expect }) => {
|
|
33
|
+
const engine = createFlow(flow, {
|
|
34
|
+
SCREEN_A: async () => {
|
|
35
|
+
return {
|
|
36
|
+
data: { age: 30 },
|
|
37
|
+
screen: 'SCREEN_B'
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
SCREEN_B: async () => {
|
|
41
|
+
return {
|
|
42
|
+
data: {
|
|
43
|
+
profile: { age: 20, name: 'John' }
|
|
44
|
+
},
|
|
45
|
+
screen: 'SCREEN_D'
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
SCREEN_C: async () => {
|
|
49
|
+
return {
|
|
50
|
+
data: {
|
|
51
|
+
profile: { age: 25, name: 'Jane' }
|
|
52
|
+
},
|
|
53
|
+
screen: 'SCREEN_D'
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
SCREEN_D: async () => {
|
|
57
|
+
throw new Error('end')
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const result = await engine.dispatch('screen.SCREEN_A', { name: 'John' })
|
|
62
|
+
|
|
63
|
+
expect(result).toEqual({
|
|
64
|
+
data: { age: 30 },
|
|
65
|
+
screen: 'SCREEN_B'
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ExtractScreen, FlowJson, NextResult, ScreenData, ScreenName, ScreenTrigger } from './types'
|
|
2
|
+
|
|
3
|
+
type Handlers<F extends FlowJson> = {
|
|
4
|
+
[K in ScreenName<F>]: (data: ScreenData<F, K>) => Promise<NextResult<F, K>>
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates a flow engine based on the provided flow definition and handlers.
|
|
9
|
+
* @param flow The flow definition, which includes the routing model and screen definitions.
|
|
10
|
+
* @param handlers An object mapping screen IDs to their corresponding handler functions. Each handler receives the data for its screen and returns a promise that resolves to the next result, which includes the next screen and its data.
|
|
11
|
+
* @returns An object with a `dispatch` method to trigger screen handlers and the original flow definition.
|
|
12
|
+
*/
|
|
13
|
+
export function createFlow<F extends FlowJson>(flow: F, handlers: Handlers<F>) {
|
|
14
|
+
return {
|
|
15
|
+
dispatch<T extends ScreenTrigger<ScreenName<F>>>(
|
|
16
|
+
trigger: T,
|
|
17
|
+
data: ScreenData<F, ExtractScreen<T, F>>
|
|
18
|
+
): Promise<NextResult<F, ExtractScreen<T, F>>> {
|
|
19
|
+
const screen = trigger.replace('screen.', '') as ExtractScreen<T, F>
|
|
20
|
+
|
|
21
|
+
return handlers[screen](data)
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
flow
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type FlowJsonDataValue =
|
|
2
|
+
| { type: 'string' }
|
|
3
|
+
| { type: 'number' }
|
|
4
|
+
| { type: 'boolean' }
|
|
5
|
+
| { type: 'object'; properties: Record<string, FlowJsonDataValue> }
|
|
6
|
+
| { type: 'array'; items: FlowJsonDataValue }
|
|
7
|
+
|
|
8
|
+
export type FlowJsonData = Record<string, FlowJsonDataValue>
|
|
9
|
+
|
|
10
|
+
export type FlowJsonScreenId = string
|
|
11
|
+
|
|
12
|
+
export type FlowJson = {
|
|
13
|
+
routing_model: Record<FlowJsonScreenId, FlowJsonScreenId[]>
|
|
14
|
+
screens: { id: FlowJsonScreenId; data: FlowJsonData }[]
|
|
15
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { FlowJson } from './flow-json'
|
|
2
|
+
import type { NextScreens } from './routing'
|
|
3
|
+
import type { ScreenData, ScreenName } from './screen'
|
|
4
|
+
|
|
5
|
+
type ScreenErrorResult<S extends string> = {
|
|
6
|
+
screen: S
|
|
7
|
+
data: {
|
|
8
|
+
error_message: string
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type FlowSuccessResult = {
|
|
13
|
+
screen: 'SUCCESS'
|
|
14
|
+
data: {
|
|
15
|
+
extension_message_response: {
|
|
16
|
+
params: Record<string, unknown> & { flow_token: string }
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type NextResult<F extends FlowJson, S extends ScreenName<F>> =
|
|
22
|
+
| {
|
|
23
|
+
[K in NextScreens<F, S>]: {
|
|
24
|
+
screen: K
|
|
25
|
+
data: ScreenData<F, K>
|
|
26
|
+
}
|
|
27
|
+
}[NextScreens<F, S>]
|
|
28
|
+
| ScreenErrorResult<S>
|
|
29
|
+
| FlowSuccessResult
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type FromSchema<S> = S extends { type: 'string' }
|
|
2
|
+
? string
|
|
3
|
+
: S extends { type: 'number' }
|
|
4
|
+
? number
|
|
5
|
+
: S extends { type: 'boolean' }
|
|
6
|
+
? boolean
|
|
7
|
+
: S extends { type: 'array'; items: infer I }
|
|
8
|
+
? FromSchema<I>[]
|
|
9
|
+
: S extends { type: 'object'; properties: infer P }
|
|
10
|
+
? { [K in keyof P]: FromSchema<P[K]> }
|
|
11
|
+
: S extends Record<string, unknown>
|
|
12
|
+
? { [K in keyof S]: FromSchema<S[K]> }
|
|
13
|
+
: unknown
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FlowJson } from './flow-json'
|
|
2
|
+
import type { NextResult } from './result'
|
|
3
|
+
import type { ScreenData, ScreenName } from './screen'
|
|
4
|
+
|
|
5
|
+
export type ScreenHandler<F extends FlowJson, S extends ScreenName<F>> = {
|
|
6
|
+
handle(data: ScreenData<F, S>): Promise<NextResult<F, S>>
|
|
7
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { FlowJson } from './flow-json'
|
|
2
|
+
import type { FromSchema } from './schema'
|
|
3
|
+
|
|
4
|
+
export type ScreenName<F extends FlowJson> = Extract<keyof F['routing_model'], string>
|
|
5
|
+
|
|
6
|
+
export type ScreenSchema<F extends FlowJson, Id extends ScreenName<F>> = Extract<
|
|
7
|
+
F['screens'][number],
|
|
8
|
+
{ id: Id }
|
|
9
|
+
>['data']
|
|
10
|
+
|
|
11
|
+
export type ScreenData<F extends FlowJson, Id extends ScreenName<F>> = FromSchema<ScreenSchema<F, Id>>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FlowJson } from './flow-json'
|
|
2
|
+
import type { ScreenName } from './screen'
|
|
3
|
+
|
|
4
|
+
export type ScreenTrigger<S extends string> = `screen.${S}`
|
|
5
|
+
|
|
6
|
+
export type ExtractScreen<T extends string, F extends FlowJson> = Extract<
|
|
7
|
+
T extends `screen.${infer S}` ? S : never,
|
|
8
|
+
ScreenName<F>
|
|
9
|
+
>
|
package/tsconfig.json
ADDED