@dxos/plugin-automation 0.6.14-main.69511f5
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/LICENSE +8 -0
- package/README.md +15 -0
- package/dist/lib/browser/PromptContainer-RFMJXMUA.mjs +19 -0
- package/dist/lib/browser/PromptContainer-RFMJXMUA.mjs.map +7 -0
- package/dist/lib/browser/chunk-3TNRXJTD.mjs +14 -0
- package/dist/lib/browser/chunk-3TNRXJTD.mjs.map +7 -0
- package/dist/lib/browser/chunk-GGA62V7D.mjs +55 -0
- package/dist/lib/browser/chunk-GGA62V7D.mjs.map +7 -0
- package/dist/lib/browser/chunk-PSSWFA6A.mjs +217 -0
- package/dist/lib/browser/chunk-PSSWFA6A.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +1135 -0
- package/dist/lib/browser/index.mjs.map +7 -0
- package/dist/lib/browser/meta.json +1 -0
- package/dist/lib/browser/meta.mjs +9 -0
- package/dist/lib/browser/meta.mjs.map +7 -0
- package/dist/lib/browser/types/index.mjs +16 -0
- package/dist/lib/browser/types/index.mjs.map +7 -0
- package/dist/lib/node/PromptContainer-FRSY6FNL.cjs +45 -0
- package/dist/lib/node/PromptContainer-FRSY6FNL.cjs.map +7 -0
- package/dist/lib/node/chunk-E2H3AGKB.cjs +240 -0
- package/dist/lib/node/chunk-E2H3AGKB.cjs.map +7 -0
- package/dist/lib/node/chunk-N5IDAUFU.cjs +76 -0
- package/dist/lib/node/chunk-N5IDAUFU.cjs.map +7 -0
- package/dist/lib/node/chunk-UPPUTAPL.cjs +37 -0
- package/dist/lib/node/chunk-UPPUTAPL.cjs.map +7 -0
- package/dist/lib/node/index.cjs +1137 -0
- package/dist/lib/node/index.cjs.map +7 -0
- package/dist/lib/node/meta.cjs +30 -0
- package/dist/lib/node/meta.cjs.map +7 -0
- package/dist/lib/node/meta.json +1 -0
- package/dist/lib/node/types/index.cjs +38 -0
- package/dist/lib/node/types/index.cjs.map +7 -0
- package/dist/lib/node-esm/PromptContainer-4VHFPEQH.mjs +20 -0
- package/dist/lib/node-esm/PromptContainer-4VHFPEQH.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-HFTXKLRR.mjs +218 -0
- package/dist/lib/node-esm/chunk-HFTXKLRR.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-QAPWYK2P.mjs +16 -0
- package/dist/lib/node-esm/chunk-QAPWYK2P.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-UT6QKU6J.mjs +56 -0
- package/dist/lib/node-esm/chunk-UT6QKU6J.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +1136 -0
- package/dist/lib/node-esm/index.mjs.map +7 -0
- package/dist/lib/node-esm/meta.json +1 -0
- package/dist/lib/node-esm/meta.mjs +10 -0
- package/dist/lib/node-esm/meta.mjs.map +7 -0
- package/dist/lib/node-esm/types/index.mjs +17 -0
- package/dist/lib/node-esm/types/index.mjs.map +7 -0
- package/dist/types/src/AutomationPlugin.d.ts +4 -0
- package/dist/types/src/AutomationPlugin.d.ts.map +1 -0
- package/dist/types/src/components/AutomationPanel.d.ts +3 -0
- package/dist/types/src/components/AutomationPanel.d.ts.map +1 -0
- package/dist/types/src/components/Chain.d.ts +12 -0
- package/dist/types/src/components/Chain.d.ts.map +1 -0
- package/dist/types/src/components/PromptContainer.d.ts +6 -0
- package/dist/types/src/components/PromptContainer.d.ts.map +1 -0
- package/dist/types/src/components/PromptEditor/PromptEditor.d.ts +10 -0
- package/dist/types/src/components/PromptEditor/PromptEditor.d.ts.map +1 -0
- package/dist/types/src/components/PromptEditor/PromptEditor.stories.d.ts +6 -0
- package/dist/types/src/components/PromptEditor/PromptEditor.stories.d.ts.map +1 -0
- package/dist/types/src/components/PromptEditor/index.d.ts +2 -0
- package/dist/types/src/components/PromptEditor/index.d.ts.map +1 -0
- package/dist/types/src/components/PromptEditor/prompt-extension.d.ts +4 -0
- package/dist/types/src/components/PromptEditor/prompt-extension.d.ts.map +1 -0
- package/dist/types/src/components/PromptEditor/types.d.ts +18 -0
- package/dist/types/src/components/PromptEditor/types.d.ts.map +1 -0
- package/dist/types/src/components/TriggerEditor/Form.d.ts +5 -0
- package/dist/types/src/components/TriggerEditor/Form.d.ts.map +1 -0
- package/dist/types/src/components/TriggerEditor/TriggerEditor.d.ts +8 -0
- package/dist/types/src/components/TriggerEditor/TriggerEditor.d.ts.map +1 -0
- package/dist/types/src/components/TriggerEditor/TriggerEditor.stories.d.ts +5 -0
- package/dist/types/src/components/TriggerEditor/TriggerEditor.stories.d.ts.map +1 -0
- package/dist/types/src/components/TriggerEditor/email.d.ts +4 -0
- package/dist/types/src/components/TriggerEditor/email.d.ts.map +1 -0
- package/dist/types/src/components/TriggerEditor/index.d.ts +2 -0
- package/dist/types/src/components/TriggerEditor/index.d.ts.map +1 -0
- package/dist/types/src/components/TriggerEditor/invokation-handler.d.ts +5 -0
- package/dist/types/src/components/TriggerEditor/invokation-handler.d.ts.map +1 -0
- package/dist/types/src/components/TriggerEditor/meta.d.ts +25 -0
- package/dist/types/src/components/TriggerEditor/meta.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +6 -0
- package/dist/types/src/components/index.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +7 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/meta.d.ts +9 -0
- package/dist/types/src/meta.d.ts.map +1 -0
- package/dist/types/src/presets.d.ts +9 -0
- package/dist/types/src/presets.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +17 -0
- package/dist/types/src/translations.d.ts.map +1 -0
- package/dist/types/src/types/chain.d.ts +71 -0
- package/dist/types/src/types/chain.d.ts.map +1 -0
- package/dist/types/src/types/index.d.ts +3 -0
- package/dist/types/src/types/index.d.ts.map +1 -0
- package/dist/types/src/types/types.d.ts +7 -0
- package/dist/types/src/types/types.d.ts.map +1 -0
- package/package.json +98 -0
- package/src/AutomationPlugin.tsx +149 -0
- package/src/components/AutomationPanel.tsx +21 -0
- package/src/components/Chain.tsx +66 -0
- package/src/components/PromptContainer.tsx +19 -0
- package/src/components/PromptEditor/PromptEditor.stories.tsx +64 -0
- package/src/components/PromptEditor/PromptEditor.tsx +222 -0
- package/src/components/PromptEditor/index.ts +5 -0
- package/src/components/PromptEditor/prompt-extension.ts +43 -0
- package/src/components/PromptEditor/types.tsx +28 -0
- package/src/components/TriggerEditor/Form.tsx +18 -0
- package/src/components/TriggerEditor/TriggerEditor.stories.tsx +82 -0
- package/src/components/TriggerEditor/TriggerEditor.tsx +346 -0
- package/src/components/TriggerEditor/email.ts +49 -0
- package/src/components/TriggerEditor/index.ts +5 -0
- package/src/components/TriggerEditor/invokation-handler.ts +110 -0
- package/src/components/TriggerEditor/meta.tsx +225 -0
- package/src/components/index.ts +12 -0
- package/src/index.ts +12 -0
- package/src/meta.ts +14 -0
- package/src/presets.ts +248 -0
- package/src/translations.ts +30 -0
- package/src/types/chain.ts +38 -0
- package/src/types/index.ts +6 -0
- package/src/types/types.ts +27 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { type ChangeEventHandler, type FC, useEffect, useMemo, useState } from 'react';
|
|
6
|
+
|
|
7
|
+
import { Mutex } from '@dxos/async';
|
|
8
|
+
import { Context } from '@dxos/context';
|
|
9
|
+
import { createSubscriptionTrigger, createWebsocketTrigger, type TriggerFactory } from '@dxos/functions';
|
|
10
|
+
import {
|
|
11
|
+
FunctionTrigger,
|
|
12
|
+
type FunctionTriggerType,
|
|
13
|
+
type SubscriptionTrigger,
|
|
14
|
+
type TimerTrigger,
|
|
15
|
+
type TriggerSpec,
|
|
16
|
+
type WebhookTrigger,
|
|
17
|
+
type WebsocketTrigger,
|
|
18
|
+
} from '@dxos/functions/types';
|
|
19
|
+
import { log } from '@dxos/log';
|
|
20
|
+
import { ScriptType } from '@dxos/plugin-script/types';
|
|
21
|
+
import { useClient } from '@dxos/react-client';
|
|
22
|
+
import { Filter, type Space, useQuery } from '@dxos/react-client/echo';
|
|
23
|
+
import { Input, Select } from '@dxos/react-ui';
|
|
24
|
+
import { distinctBy } from '@dxos/util';
|
|
25
|
+
|
|
26
|
+
import { InputRow } from './Form';
|
|
27
|
+
import { invokeFunction } from './invokation-handler';
|
|
28
|
+
import { getFunctionMetaExtension, state } from './meta';
|
|
29
|
+
|
|
30
|
+
const triggerTypes: FunctionTriggerType[] = ['subscription', 'timer', 'webhook', 'websocket'];
|
|
31
|
+
|
|
32
|
+
const registerTriggersMutex = new Mutex();
|
|
33
|
+
|
|
34
|
+
export const TriggerEditor = ({ space, trigger }: { space: Space; trigger: FunctionTrigger }) => {
|
|
35
|
+
const client = useClient();
|
|
36
|
+
const scripts = useQuery(space, Filter.schema(ScriptType));
|
|
37
|
+
const script = useMemo(() => scripts.find((script) => script.id === trigger.function), [trigger.function, scripts]);
|
|
38
|
+
|
|
39
|
+
// TODO(burdon): Factor out, creating context for plugin (runs outside of component).
|
|
40
|
+
const [registry] = useState(new Map<string, Context>());
|
|
41
|
+
const triggers = useQuery(space, Filter.schema(FunctionTrigger));
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
setTimeout(async () => {
|
|
44
|
+
// Mark-and-sweep removing disabled triggers.
|
|
45
|
+
await registerTriggersMutex.executeSynchronized(async () => {
|
|
46
|
+
const deprecated = new Set(Array.from(registry.keys()));
|
|
47
|
+
log.info('triggers', {
|
|
48
|
+
deprecated,
|
|
49
|
+
all: triggers.map((t) => t.id),
|
|
50
|
+
enabled: triggers.filter((t) => t.enabled).map((t) => t.id),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
for (const trigger of triggers) {
|
|
54
|
+
if (trigger.enabled) {
|
|
55
|
+
if (registry.has(trigger.id)) {
|
|
56
|
+
deprecated.delete(trigger.id);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
log.info('activating trigger', trigger.id);
|
|
60
|
+
|
|
61
|
+
const ctx = new Context();
|
|
62
|
+
registry.set(trigger.id, ctx);
|
|
63
|
+
const triggerSpec = trigger.spec;
|
|
64
|
+
|
|
65
|
+
let triggerFactory: TriggerFactory<any>;
|
|
66
|
+
if (triggerSpec.type === 'subscription') {
|
|
67
|
+
triggerFactory = createSubscriptionTrigger;
|
|
68
|
+
} else if (triggerSpec.type === 'websocket') {
|
|
69
|
+
triggerFactory = createWebsocketTrigger;
|
|
70
|
+
} else {
|
|
71
|
+
log.info('unsupported trigger', { type: triggerSpec.type });
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
await triggerFactory(ctx, space, trigger.spec, (data: any) => {
|
|
76
|
+
return invokeFunction(client, space, trigger, data);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (const id of deprecated) {
|
|
82
|
+
const ctx = registry.get(id);
|
|
83
|
+
if (ctx) {
|
|
84
|
+
await ctx.dispose();
|
|
85
|
+
registry.delete(id);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
}, [JSON.stringify(triggers)]);
|
|
91
|
+
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
return () => {
|
|
94
|
+
for (const ctx of registry.values()) {
|
|
95
|
+
void ctx.dispose();
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}, []);
|
|
99
|
+
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
void space.db.schema
|
|
102
|
+
.list()
|
|
103
|
+
.then((schemas) => {
|
|
104
|
+
// TODO(zan): We should solve double adding of stored schemas in the schema registry.
|
|
105
|
+
state.schemas = distinctBy([...state.schemas, ...schemas], (schema) => schema.typename).sort((a, b) =>
|
|
106
|
+
a.typename < b.typename ? -1 : 1,
|
|
107
|
+
);
|
|
108
|
+
})
|
|
109
|
+
.catch(() => {});
|
|
110
|
+
}, [space]);
|
|
111
|
+
|
|
112
|
+
// Keen an enriched version of the schema in memory so we can share it with prompt editor.
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
const spec = trigger.spec;
|
|
115
|
+
if (spec.type === 'subscription') {
|
|
116
|
+
if (spec.filter && spec.filter.length > 0) {
|
|
117
|
+
const type = spec.filter[0].type;
|
|
118
|
+
const foundSchema = state.schemas.find((schema) => schema.typename === type);
|
|
119
|
+
if (foundSchema) {
|
|
120
|
+
state.selectedSchema[trigger.id] = foundSchema;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// TODO(burdon): API issue.
|
|
125
|
+
}, [JSON.stringify(trigger.spec), state.schemas]);
|
|
126
|
+
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
if (!trigger.meta) {
|
|
129
|
+
const extension = getFunctionMetaExtension(trigger, script);
|
|
130
|
+
trigger.meta = extension?.initialValue?.();
|
|
131
|
+
}
|
|
132
|
+
}, [trigger.function, trigger.meta]);
|
|
133
|
+
|
|
134
|
+
const handleSelectFunction = (value: string) => {
|
|
135
|
+
const match = scripts.find((fn) => fn.id === value);
|
|
136
|
+
if (match) {
|
|
137
|
+
trigger.function = match.id;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const handleSelectTriggerType = (triggerType: string) => {
|
|
142
|
+
switch (triggerType as FunctionTriggerType) {
|
|
143
|
+
case 'subscription': {
|
|
144
|
+
trigger.spec = { type: 'subscription', filter: [] };
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
case 'timer': {
|
|
148
|
+
trigger.spec = { type: 'timer', cron: '0 0 * * *' };
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
case 'webhook': {
|
|
152
|
+
trigger.spec = { type: 'webhook', method: 'GET' };
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
case 'websocket': {
|
|
156
|
+
// TODO(burdon): The `init` property is currently mail worker specific.
|
|
157
|
+
trigger.spec = { type: 'websocket', url: '', init: { type: 'sync' } };
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const TriggerMeta = getFunctionMetaExtension(trigger, script)?.component;
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<div className='flex flex-col py-1'>
|
|
167
|
+
<table className='is-full table-fixed'>
|
|
168
|
+
<tbody>
|
|
169
|
+
<InputRow label='Function'>
|
|
170
|
+
<Select.Root value={script?.id} onValueChange={handleSelectFunction}>
|
|
171
|
+
<Select.TriggerButton placeholder={'Select function'} />
|
|
172
|
+
<Select.Portal>
|
|
173
|
+
<Select.Content>
|
|
174
|
+
<Select.Viewport>
|
|
175
|
+
{scripts.map(({ id, name }) => (
|
|
176
|
+
<Select.Option key={id} value={id}>
|
|
177
|
+
{name ?? 'Unnamed'}
|
|
178
|
+
</Select.Option>
|
|
179
|
+
))}
|
|
180
|
+
</Select.Viewport>
|
|
181
|
+
</Select.Content>
|
|
182
|
+
</Select.Portal>
|
|
183
|
+
</Select.Root>
|
|
184
|
+
</InputRow>
|
|
185
|
+
{script?.description?.length && (
|
|
186
|
+
<InputRow>
|
|
187
|
+
<div className='px-2'>
|
|
188
|
+
<p className='text-sm text-description'>{script?.description?.length}</p>
|
|
189
|
+
</div>
|
|
190
|
+
</InputRow>
|
|
191
|
+
)}
|
|
192
|
+
<InputRow label='Type'>
|
|
193
|
+
<Select.Root value={trigger.spec?.type} onValueChange={handleSelectTriggerType}>
|
|
194
|
+
<Select.TriggerButton placeholder={'Select trigger'} />
|
|
195
|
+
<Select.Portal>
|
|
196
|
+
<Select.Content>
|
|
197
|
+
<Select.Viewport>
|
|
198
|
+
{triggerTypes.map((trigger) => (
|
|
199
|
+
<Select.Option key={trigger} value={trigger}>
|
|
200
|
+
{trigger}
|
|
201
|
+
</Select.Option>
|
|
202
|
+
))}
|
|
203
|
+
</Select.Viewport>
|
|
204
|
+
</Select.Content>
|
|
205
|
+
</Select.Portal>
|
|
206
|
+
</Select.Root>
|
|
207
|
+
</InputRow>
|
|
208
|
+
</tbody>
|
|
209
|
+
<tbody>
|
|
210
|
+
{trigger.spec && <TriggerSpec space={space} spec={trigger.spec} />}
|
|
211
|
+
<InputRow label='Enabled'>
|
|
212
|
+
{/* TODO(burdon): Hack to make the switch the same height as other controls. */}
|
|
213
|
+
<div className='flex items-center h-8'>
|
|
214
|
+
<Input.Switch checked={trigger.enabled} onCheckedChange={(checked) => (trigger.enabled = !!checked)} />
|
|
215
|
+
</div>
|
|
216
|
+
</InputRow>
|
|
217
|
+
</tbody>
|
|
218
|
+
{TriggerMeta && (
|
|
219
|
+
<tbody>
|
|
220
|
+
<tr>
|
|
221
|
+
<td />
|
|
222
|
+
<td className='py-2'>
|
|
223
|
+
<div className='border-b border-separator' />
|
|
224
|
+
</td>
|
|
225
|
+
</tr>
|
|
226
|
+
<TriggerMeta meta={trigger.meta} />
|
|
227
|
+
</tbody>
|
|
228
|
+
)}
|
|
229
|
+
</table>
|
|
230
|
+
</div>
|
|
231
|
+
);
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
//
|
|
235
|
+
// Trigger specs
|
|
236
|
+
//
|
|
237
|
+
|
|
238
|
+
const TriggerSpecSubscription = ({ spec }: TriggerSpecProps<SubscriptionTrigger>) => {
|
|
239
|
+
if (!spec.filter) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const handleSelectSchema = (typename: string) => {
|
|
244
|
+
spec.filter = [{ type: typename }];
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
return (
|
|
248
|
+
<>
|
|
249
|
+
<InputRow label='Filter'>
|
|
250
|
+
<Select.Root
|
|
251
|
+
value={spec.filter.length > 0 ? spec.filter[0].type : undefined}
|
|
252
|
+
onValueChange={handleSelectSchema}
|
|
253
|
+
>
|
|
254
|
+
<Select.TriggerButton classNames='w-full' placeholder={'Select type'} />
|
|
255
|
+
<Select.Portal>
|
|
256
|
+
<Select.Content>
|
|
257
|
+
<Select.Viewport>
|
|
258
|
+
{state.schemas.map(({ typename }: any) => (
|
|
259
|
+
<Select.Option key={typename} value={typename}>
|
|
260
|
+
{typename}
|
|
261
|
+
</Select.Option>
|
|
262
|
+
))}
|
|
263
|
+
</Select.Viewport>
|
|
264
|
+
</Select.Content>
|
|
265
|
+
</Select.Portal>
|
|
266
|
+
</Select.Root>
|
|
267
|
+
</InputRow>
|
|
268
|
+
</>
|
|
269
|
+
);
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const TriggerSpecTimer = ({ spec }: TriggerSpecProps<TimerTrigger>) => (
|
|
273
|
+
<>
|
|
274
|
+
<InputRow label='Cron'>
|
|
275
|
+
<Input.TextInput value={spec.cron} onChange={(event) => (spec.cron = event.target.value)} />
|
|
276
|
+
</InputRow>
|
|
277
|
+
</>
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
const methods = ['GET', 'POST'];
|
|
281
|
+
|
|
282
|
+
const TriggerSpecWebhook = ({ spec }: TriggerSpecProps<WebhookTrigger>) => (
|
|
283
|
+
<>
|
|
284
|
+
<InputRow label='Method'>
|
|
285
|
+
<Select.Root value={spec.method} onValueChange={(value) => (spec.method = value)}>
|
|
286
|
+
<Select.TriggerButton placeholder={'type'} />
|
|
287
|
+
<Select.Portal>
|
|
288
|
+
<Select.Content>
|
|
289
|
+
<Select.Viewport>
|
|
290
|
+
{methods.map((method) => (
|
|
291
|
+
<Select.Option key={method} value={method}>
|
|
292
|
+
{method}
|
|
293
|
+
</Select.Option>
|
|
294
|
+
))}
|
|
295
|
+
</Select.Viewport>
|
|
296
|
+
</Select.Content>
|
|
297
|
+
</Select.Portal>
|
|
298
|
+
</Select.Root>
|
|
299
|
+
</InputRow>
|
|
300
|
+
</>
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
const TriggerSpecWebsocket = ({ spec }: TriggerSpecProps<WebsocketTrigger>) => {
|
|
304
|
+
const handleChangeInit: ChangeEventHandler<HTMLInputElement> = (event) => {
|
|
305
|
+
try {
|
|
306
|
+
spec.init = JSON.parse(event.target.value);
|
|
307
|
+
} catch (err) {
|
|
308
|
+
// Ignore.
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
return (
|
|
313
|
+
<>
|
|
314
|
+
<InputRow label='Endpoint'>
|
|
315
|
+
<Input.TextInput
|
|
316
|
+
value={spec.url}
|
|
317
|
+
onChange={(event) => (spec.url = event.target.value)}
|
|
318
|
+
placeholder='https://'
|
|
319
|
+
/>
|
|
320
|
+
</InputRow>
|
|
321
|
+
<InputRow label='Init'>
|
|
322
|
+
<Input.TextInput value={JSON.stringify(spec.init)} onChange={handleChangeInit} placeholder='Initial message.' />
|
|
323
|
+
</InputRow>
|
|
324
|
+
</>
|
|
325
|
+
);
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
//
|
|
329
|
+
// Trigger spec.
|
|
330
|
+
//
|
|
331
|
+
|
|
332
|
+
type TriggerSpecProps<T = TriggerSpec> = { space: Space; spec: T };
|
|
333
|
+
|
|
334
|
+
const triggerRenderers: {
|
|
335
|
+
[key in FunctionTriggerType]: FC<TriggerSpecProps<any>>;
|
|
336
|
+
} = {
|
|
337
|
+
subscription: TriggerSpecSubscription,
|
|
338
|
+
timer: TriggerSpecTimer,
|
|
339
|
+
webhook: TriggerSpecWebhook,
|
|
340
|
+
websocket: TriggerSpecWebsocket,
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
const TriggerSpec = ({ space, spec }: TriggerSpecProps) => {
|
|
344
|
+
const Component = triggerRenderers[spec.type];
|
|
345
|
+
return Component ? <Component space={space} spec={spec} /> : null;
|
|
346
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { findObjectWithForeignKey } from '@dxos/echo-db';
|
|
6
|
+
import { create, foreignKey } from '@dxos/echo-schema';
|
|
7
|
+
import { log } from '@dxos/log';
|
|
8
|
+
import { MailboxType } from '@dxos/plugin-inbox/types';
|
|
9
|
+
import { MessageType } from '@dxos/plugin-space/types';
|
|
10
|
+
import { type Space, Filter } from '@dxos/react-client/echo';
|
|
11
|
+
|
|
12
|
+
export const SOURCE_ID = 'hub.dxos.network/api/mailbox';
|
|
13
|
+
|
|
14
|
+
export const handleEmail = async (space: Space, data: any) => {
|
|
15
|
+
const { messages } = data;
|
|
16
|
+
|
|
17
|
+
// Create mailbox if doesn't exist.
|
|
18
|
+
const { objects: mailboxes } = await space.db.query(Filter.schema(MailboxType)).run();
|
|
19
|
+
const mailbox = mailboxes[0] ?? space.db.add(create(MailboxType, { messages: [] }));
|
|
20
|
+
|
|
21
|
+
log.info('messages', { count: messages.length, existingMailbox: mailboxes.length > 0 });
|
|
22
|
+
|
|
23
|
+
const { objects } = await space.db.query(Filter.schema(MessageType)).run();
|
|
24
|
+
for (const message of messages) {
|
|
25
|
+
let object = findObjectWithForeignKey(objects, { source: SOURCE_ID, id: String(message.id) });
|
|
26
|
+
if (!object) {
|
|
27
|
+
object = space.db.add(
|
|
28
|
+
create(
|
|
29
|
+
MessageType,
|
|
30
|
+
{
|
|
31
|
+
sender: { email: message.from },
|
|
32
|
+
timestamp: new Date(message.created).toISOString(),
|
|
33
|
+
text: message.body,
|
|
34
|
+
properties: {
|
|
35
|
+
subject: message.subject,
|
|
36
|
+
to: [{ email: message.to }],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
keys: [foreignKey(SOURCE_ID, String(message.id))],
|
|
41
|
+
},
|
|
42
|
+
),
|
|
43
|
+
);
|
|
44
|
+
mailbox.messages?.push(object);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return 200;
|
|
49
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { sleep } from '@dxos/async';
|
|
6
|
+
import { getObjectCore } from '@dxos/echo-db';
|
|
7
|
+
import type { AnyObjectData } from '@dxos/echo-schema';
|
|
8
|
+
import { type FunctionTrigger } from '@dxos/functions';
|
|
9
|
+
import { invariant } from '@dxos/invariant';
|
|
10
|
+
import { DXN, LOCAL_SPACE_TAG } from '@dxos/keys';
|
|
11
|
+
import { log } from '@dxos/log';
|
|
12
|
+
import { FunctionType } from '@dxos/plugin-script';
|
|
13
|
+
import { type Client, type Config } from '@dxos/react-client';
|
|
14
|
+
import { type Space } from '@dxos/react-client/echo';
|
|
15
|
+
|
|
16
|
+
import { handleEmail } from './email';
|
|
17
|
+
|
|
18
|
+
const MAX_RETRIES = 3;
|
|
19
|
+
const RETRY_DELAY = 1_000;
|
|
20
|
+
|
|
21
|
+
const callFunction = async (funcUrl: string, trigger: any, data: any) => {
|
|
22
|
+
const body = { event: 'trigger', trigger, data };
|
|
23
|
+
|
|
24
|
+
let retryCount = 0;
|
|
25
|
+
while (retryCount < MAX_RETRIES) {
|
|
26
|
+
log.info('exec', { funcUrl, body, retryCount });
|
|
27
|
+
const response = await fetch(funcUrl, {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: {
|
|
30
|
+
'Content-Type': 'application/json',
|
|
31
|
+
},
|
|
32
|
+
body: JSON.stringify(body),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const data = await response.text();
|
|
36
|
+
log.info('response', { status: response.status, body: data });
|
|
37
|
+
if (response.status === 409) {
|
|
38
|
+
retryCount++;
|
|
39
|
+
await sleep(RETRY_DELAY * Math.min(retryCount, 2));
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return { status: response.status, data };
|
|
44
|
+
}
|
|
45
|
+
return { status: 500 };
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const invokeFunction = async (client: Client, space: Space, trigger: FunctionTrigger, data: any) => {
|
|
49
|
+
try {
|
|
50
|
+
if (trigger.spec.type === 'websocket') {
|
|
51
|
+
return handleEmail(space, data.data);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const script = await space.crud.query({ id: trigger.function }).first();
|
|
55
|
+
const { objects: functions } = await space.crud.query({ __typename: FunctionType.typename }).run();
|
|
56
|
+
const func = functions.find((fn) => referenceEquals(fn.source, trigger.function)) as AnyObjectData | undefined;
|
|
57
|
+
const funcSlug = func?.__meta.keys.find((key) => key.source === USERFUNCTIONS_META_KEY)?.id;
|
|
58
|
+
if (!funcSlug) {
|
|
59
|
+
log.warn('function not deployed', { scriptId: script.id, name: script.name });
|
|
60
|
+
return 404;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const funcUrl = getFunctionUrl(client.config, funcSlug, space.id);
|
|
64
|
+
const triggerData: AnyObjectData = {
|
|
65
|
+
...getObjectCore(trigger).toPlainObject(),
|
|
66
|
+
// TODO: Remove when functions can query by DXN.
|
|
67
|
+
promptId: trigger.meta?.prompt?.id,
|
|
68
|
+
};
|
|
69
|
+
// TODO: Remove when functions can add objects and easily modify collections (push, splice).
|
|
70
|
+
return (await callFunction(funcUrl, triggerData, data)).status;
|
|
71
|
+
} catch (err) {
|
|
72
|
+
return 400;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const USERFUNCTIONS_META_KEY = 'dxos.org/service/function';
|
|
77
|
+
|
|
78
|
+
const getFunctionUrl = (config: Config, slug: string, spaceId?: string) => {
|
|
79
|
+
const baseUrl = new URL('functions/', config.values.runtime?.services?.edge?.url);
|
|
80
|
+
|
|
81
|
+
// Leading slashes cause the URL to be treated as an absolute path.
|
|
82
|
+
const relativeUrl = slug.replace(/^\//, '');
|
|
83
|
+
const url = new URL(`./${relativeUrl}`, baseUrl.toString());
|
|
84
|
+
spaceId && url.searchParams.set('spaceId', spaceId);
|
|
85
|
+
url.protocol = 'https';
|
|
86
|
+
return url.toString();
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// TODO(dmaretskyi): Factor out.
|
|
90
|
+
|
|
91
|
+
type ReferenceLike = { '/': string } | string;
|
|
92
|
+
|
|
93
|
+
const referenceEquals = (a: ReferenceLike, b: ReferenceLike): boolean => {
|
|
94
|
+
const aDXN = toDXN(a);
|
|
95
|
+
const bDXN = toDXN(b);
|
|
96
|
+
return aDXN.toString() === bDXN.toString();
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const toDXN = (ref: ReferenceLike): DXN => {
|
|
100
|
+
if (typeof ref === 'string') {
|
|
101
|
+
if (ref.startsWith('dxn:')) {
|
|
102
|
+
return DXN.parse(ref);
|
|
103
|
+
} else {
|
|
104
|
+
return new DXN(DXN.kind.ECHO, [LOCAL_SPACE_TAG, ref]);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
invariant(typeof ref['/'] === 'string');
|
|
109
|
+
return DXN.parse(ref['/']);
|
|
110
|
+
};
|