@proveanything/smartlinks 1.16.7 → 1.17.4

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,141 @@
1
+ # Integrations
2
+
3
+ An **integration flow** is one input/output pipeline between SmartLinks and an external
4
+ system. There are two directions:
5
+
6
+ - **outbound** — read a SmartLinks entity (v1: a product), transform it with field
7
+ mappings, and send it to an external endpoint.
8
+ - **inbound** — fetch from an external system and write a SmartLinks entity. *(Executor is
9
+ outbound-first; inbound lands in a later increment.)*
10
+
11
+ Flows are triggered three ways, all converging on the same executor:
12
+
13
+ - **manual** — `integrations.runFlow(...)`, inline (returns a run summary) or enqueued.
14
+ - **event** — an outbound flow subscribed to an event type (e.g. `product.updated`) fires
15
+ automatically when that entity changes.
16
+ - **schedule** — a flow carrying a cron/interval `schedule` is run by the scan job. *(next)*
17
+
18
+ Credentials are **never** stored on the flow. The connection holds an opaque
19
+ `credentialRef` into the **sealed-secret store** (`secrets` namespace); the value is sealed
20
+ at rest and resolved server-side only, at execution.
21
+
22
+ ---
23
+
24
+ ## The flow model
25
+
26
+ ```ts
27
+ interface IntegrationFlow {
28
+ id: string
29
+ direction: 'inbound' | 'outbound'
30
+ name: string
31
+ status: 'draft' | 'active' | 'paused' | 'error' // only 'active' flows fire on events/schedule
32
+ eventTypes: string[] // e.g. ['product.updated']
33
+ schedule: string | null // cron/interval for scheduled flows
34
+ sourceEntity: string | null // outbound source, v1: 'product'
35
+ targetEntity: string | null // inbound target
36
+ config: {
37
+ connection?: {
38
+ baseUrl?: string
39
+ sendEndpoint?: string // outbound: appended to baseUrl
40
+ defaultHeaders?: Record<string, string>
41
+ auth?: { method: 'api_key' | 'bearer' | 'basic' | ..., headerName?: string, credentialRef?: string }
42
+ }
43
+ fieldMappings?: FieldMapping[]
44
+ }
45
+ // ...run watermark/telemetry: lastRunAt, lastRunStatus, lastRunCount, totalSynced
46
+ }
47
+ ```
48
+
49
+ ### Field mappings (transform)
50
+
51
+ Each mapping produces one field on the target payload:
52
+
53
+ | transformType | uses | meaning |
54
+ |---|---|---|
55
+ | `direct` | `sourcePath` | copy the value at that dot-path |
56
+ | `static` | `transformExpression` | a constant |
57
+ | `template` | `transformExpression` | a Liquid template rendered against the source record |
58
+ | `jsonata` / `ai` | — | recognised but not yet executed; reported as a per-field error |
59
+
60
+ A single field's failure is collected and the rest continue (partial success) — it never
61
+ aborts the whole record.
62
+
63
+ ---
64
+
65
+ ## Secrets (write-only)
66
+
67
+ The secret store is **write-only from the client**: you can set, rotate, list (refs +
68
+ masked hints + metadata) and delete — but a value never comes back over the API.
69
+
70
+ ```ts
71
+ import { secrets, integrations } from '@proveanything/smartlinks'
72
+
73
+ // 1. Store the destination credential — keep the returned ref.
74
+ const { ref } = await secrets.set(collectionId, {
75
+ name: 'Acme API key',
76
+ purpose: 'integration',
77
+ value: 'sk_live_…', // sent once; never retrievable
78
+ })
79
+
80
+ // list shows refs + masked hints only (safe to render)
81
+ const { secrets: list } = await secrets.list(collectionId)
82
+ // → [{ ref, name: 'Acme API key', hint: '…live_1a2b', purpose, createdAt, ... }]
83
+ ```
84
+
85
+ ---
86
+
87
+ ## Creating and running a flow
88
+
89
+ ```ts
90
+ // 2. Create an outbound flow that pushes products to Acme, authed by the secret above.
91
+ const flow = await integrations.createFlow(collectionId, {
92
+ appId: 'my-integration-app',
93
+ direction: 'outbound',
94
+ name: 'Push products to Acme',
95
+ status: 'active',
96
+ eventTypes: ['product.updated'], // fire whenever a product changes
97
+ sourceEntity: 'product',
98
+ config: {
99
+ connection: {
100
+ baseUrl: 'https://api.acme.example',
101
+ sendEndpoint: '/v1/products',
102
+ auth: { method: 'api_key', headerName: 'X-API-Key', credentialRef: ref },
103
+ },
104
+ fieldMappings: [
105
+ { targetPath: 'sku', sourcePath: 'sku', transformType: 'direct' },
106
+ { targetPath: 'name', sourcePath: 'name', transformType: 'direct' },
107
+ { targetPath: 'label', transformType: 'template', transformExpression: '{{name}} ({{sku}})' },
108
+ ],
109
+ },
110
+ })
111
+
112
+ // 3a. Test it now against one product — inline, returns a summary.
113
+ const result = await integrations.runFlow(collectionId, flow.id, { entityId: 'P1045716' })
114
+ if (integrations.isRunSummary(result)) {
115
+ console.log(result) // { records: 1, sent: 1, failed: 0, status: 'success' }
116
+ }
117
+
118
+ // 3b. Or enqueue on the worker (returns immediately).
119
+ await integrations.runFlow(collectionId, flow.id, { entityId: 'P1045716', async: true })
120
+ ```
121
+
122
+ Once `status: 'active'` with `eventTypes: ['product.updated']`, editing that product in the
123
+ admin API fires the flow automatically — no manual run needed.
124
+
125
+ ---
126
+
127
+ ## Reference
128
+
129
+ | Function | HTTP |
130
+ |---|---|
131
+ | `integrations.listFlows(collectionId, query?)` | `GET /integrations/flows` |
132
+ | `integrations.createFlow(collectionId, input)` | `POST /integrations/flows` |
133
+ | `integrations.getFlow(collectionId, id)` | `GET /integrations/flows/:id` |
134
+ | `integrations.updateFlow(collectionId, id, input)` | `PUT /integrations/flows/:id` |
135
+ | `integrations.deleteFlow(collectionId, id)` | `DELETE /integrations/flows/:id` |
136
+ | `integrations.runFlow(collectionId, id, opts?)` | `POST /integrations/flows/:id/run` |
137
+ | `secrets.list(collectionId, query?)` | `GET /secrets` |
138
+ | `secrets.set(collectionId, input)` | `POST /secrets` |
139
+ | `secrets.get(collectionId, ref)` | `GET /secrets/:ref` |
140
+ | `secrets.rotate(collectionId, ref, input)` | `PUT /secrets/:ref` |
141
+ | `secrets.remove(collectionId, ref)` | `DELETE /secrets/:ref` |