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.
@@ -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,2 @@
1
+ export * from './create-flow'
2
+ export * from './types'
@@ -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,7 @@
1
+ export * from './flow-json'
2
+ export * from './result'
3
+ export * from './routing'
4
+ export * from './schema'
5
+ export * from './screen'
6
+ export * from './screen-handler'
7
+ export * from './trigger'
@@ -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,7 @@
1
+ import type { FlowJson } from './flow-json'
2
+ import type { ScreenName } from './screen'
3
+
4
+ export type NextScreens<F extends FlowJson, S extends ScreenName<F>> = Extract<
5
+ F['routing_model'][S][number],
6
+ ScreenName<F>
7
+ >
@@ -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
@@ -0,0 +1,12 @@
1
+ {
2
+ "compilerOptions": {
3
+ "esModuleInterop": true,
4
+ "forceConsistentCasingInFileNames": true,
5
+ "module": "commonjs",
6
+ "outDir": "dist",
7
+ "skipLibCheck": true,
8
+ "strict": true,
9
+ "target": "es2024"
10
+ },
11
+ "exclude": []
12
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from 'vitest/config'
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ exclude: ['dist/**']
6
+ }
7
+ })