@slates/provider 1.0.0-rc.34 → 1.0.0-rc.35
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/dist/index.cjs +193 -10
- package/dist/index.d.cts +71 -1
- package/dist/index.d.ts +71 -1
- package/dist/index.module.js +191 -10
- package/package.json +1 -1
- package/src/action/action.ts +1 -0
- package/src/action/builder.ts +73 -1
- package/src/action/index.ts +1 -0
- package/src/action/optionList.ts +94 -0
- package/src/action/trigger.test.ts +132 -0
- package/src/action/trigger.ts +7 -0
- package/src/action/triggerGroup.test.ts +82 -0
- package/src/action/triggerGroup.ts +117 -1
- package/src/specification/slate.test.ts +70 -1
- package/src/specification/slate.ts +27 -3
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { SlateContext } from '../context';
|
|
3
|
+
import { SlateDeclarationError } from '../error';
|
|
4
|
+
import type { SlateSpecification } from '../specification/specification';
|
|
5
|
+
|
|
6
|
+
export interface SlateOption {
|
|
7
|
+
value: string | number;
|
|
8
|
+
label: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface SlateOptionListPage {
|
|
13
|
+
options: SlateOption[];
|
|
14
|
+
nextPageToken?: any | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type SlateOptionListHandler<
|
|
18
|
+
ConfigType extends {},
|
|
19
|
+
AuthType extends {},
|
|
20
|
+
ParamsType extends {} = {}
|
|
21
|
+
> = (
|
|
22
|
+
context: SlateContext<
|
|
23
|
+
ConfigType,
|
|
24
|
+
AuthType,
|
|
25
|
+
{ query: string | null; pageToken: any | null; params: ParamsType }
|
|
26
|
+
>
|
|
27
|
+
) => Promise<SlateOptionListPage>;
|
|
28
|
+
|
|
29
|
+
export interface SlateOptionListParameters<
|
|
30
|
+
ConfigType extends {},
|
|
31
|
+
AuthType extends {},
|
|
32
|
+
ParamsSchema extends z.ZodObject<any> = z.ZodObject<{}>
|
|
33
|
+
> {
|
|
34
|
+
key: string;
|
|
35
|
+
/** Made strict when declared, so params with a key the schema does not name are rejected. */
|
|
36
|
+
paramsSchema?: ParamsSchema;
|
|
37
|
+
list: SlateOptionListHandler<ConfigType, AuthType, z.output<ParamsSchema>>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A list of options other integrations can link their selection fields to. */
|
|
41
|
+
export class SlateOptionList<
|
|
42
|
+
ConfigType extends {},
|
|
43
|
+
AuthType extends {},
|
|
44
|
+
ParamsSchema extends z.ZodObject<any> = z.ZodObject<{}>
|
|
45
|
+
> {
|
|
46
|
+
private constructor(
|
|
47
|
+
private readonly _spec: SlateSpecification<ConfigType, AuthType>,
|
|
48
|
+
private readonly _params: SlateOptionListParameters<ConfigType, AuthType, ParamsSchema>,
|
|
49
|
+
private readonly _paramsSchema: z.ZodObject<any>
|
|
50
|
+
) {}
|
|
51
|
+
|
|
52
|
+
static create<
|
|
53
|
+
ConfigType extends {},
|
|
54
|
+
AuthType extends {},
|
|
55
|
+
ParamsSchema extends z.ZodObject<any> = z.ZodObject<{}>
|
|
56
|
+
>(
|
|
57
|
+
spec: SlateSpecification<ConfigType, AuthType>,
|
|
58
|
+
params: SlateOptionListParameters<ConfigType, AuthType, ParamsSchema>
|
|
59
|
+
) {
|
|
60
|
+
let paramsSchema: z.ZodObject<any> = params.paramsSchema ?? z.object({});
|
|
61
|
+
if (paramsSchema?._zod?.def?.type !== 'object') {
|
|
62
|
+
throw new SlateDeclarationError(
|
|
63
|
+
`Option list "${params.key}" params schema must be a zod object`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return new SlateOptionList(spec, params, paramsSchema.strict());
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get spec() {
|
|
71
|
+
return this._spec;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
get key() {
|
|
75
|
+
return this._params.key;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
get paramsSchema() {
|
|
79
|
+
return this._paramsSchema;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
get list() {
|
|
83
|
+
return this._params.list;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export let optionList = <
|
|
88
|
+
ConfigType extends {},
|
|
89
|
+
AuthType extends {},
|
|
90
|
+
ParamsSchema extends z.ZodObject<any> = z.ZodObject<{}>
|
|
91
|
+
>(
|
|
92
|
+
spec: SlateSpecification<ConfigType, AuthType>,
|
|
93
|
+
params: SlateOptionListParameters<ConfigType, AuthType, ParamsSchema>
|
|
94
|
+
) => SlateOptionList.create(spec, params);
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { SlateAuth } from '../auth';
|
|
4
|
+
import { SlateConfig } from '../config';
|
|
5
|
+
import { SlateSpecification } from '../specification/specification';
|
|
6
|
+
import { SlateTool } from './tool';
|
|
7
|
+
import { SlateTrigger } from './trigger';
|
|
8
|
+
import { triggerGroup } from './triggerGroup';
|
|
9
|
+
|
|
10
|
+
let spec = SlateSpecification.create({
|
|
11
|
+
key: 'demo',
|
|
12
|
+
name: 'Demo',
|
|
13
|
+
config: SlateConfig.create(z.object({})),
|
|
14
|
+
auth: SlateAuth.create<{}>().output(z.object({}))
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
let group = triggerGroup(spec, { key: 'group', name: 'Group' })
|
|
18
|
+
.polling({ pollEvents: async () => ({ events: [] }) })
|
|
19
|
+
.routingMatchers(async () => [])
|
|
20
|
+
.build();
|
|
21
|
+
|
|
22
|
+
let output = z.object({
|
|
23
|
+
fileId: z.string(),
|
|
24
|
+
parents: z.array(z.string()),
|
|
25
|
+
size: z.number().optional()
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
let create = () =>
|
|
29
|
+
SlateTrigger.create(spec, { key: 'changed', name: 'Changed' })
|
|
30
|
+
.input(z.object({}))
|
|
31
|
+
.triggerGroup(group)
|
|
32
|
+
.matches(() => true)
|
|
33
|
+
.map(async () => ({
|
|
34
|
+
type: 'file.changed',
|
|
35
|
+
id: '1',
|
|
36
|
+
output: { fileId: 'a', parents: [] }
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
describe('trigger event attributes', () => {
|
|
40
|
+
it('exposes the declared fields on the built trigger', () => {
|
|
41
|
+
let trigger = create().output(output).eventAttributes(['fileId', 'parents']).build();
|
|
42
|
+
|
|
43
|
+
expect(trigger.eventAttributes).toEqual(['fileId', 'parents']);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('is empty when none are declared', () => {
|
|
47
|
+
expect(create().output(output).build().eventAttributes).toEqual([]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('checks the fields against an output schema set after the declaration', () => {
|
|
51
|
+
expect(
|
|
52
|
+
create()
|
|
53
|
+
.eventAttributes(['fileId'] as never[])
|
|
54
|
+
.output(output)
|
|
55
|
+
.build().eventAttributes
|
|
56
|
+
).toEqual(['fileId']);
|
|
57
|
+
|
|
58
|
+
expect(() =>
|
|
59
|
+
create()
|
|
60
|
+
.eventAttributes(['folderId'] as never[])
|
|
61
|
+
.output(output)
|
|
62
|
+
.build()
|
|
63
|
+
).toThrow('Event attribute "folderId" is not a top-level property of the output schema');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('rejects fields that are not top-level output properties', () => {
|
|
67
|
+
expect(() =>
|
|
68
|
+
create()
|
|
69
|
+
.output(output)
|
|
70
|
+
.eventAttributes(['folderId' as any])
|
|
71
|
+
).toThrow('Event attribute "folderId" is not a top-level property of the output schema');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('requires the output schema to be a zod object', () => {
|
|
75
|
+
expect(() =>
|
|
76
|
+
create()
|
|
77
|
+
.output(z.record(z.string(), z.string()) as unknown as z.ZodType<{ fileId: string }>)
|
|
78
|
+
.eventAttributes(['fileId'])
|
|
79
|
+
).toThrow('Event attributes require the output schema to be a zod object');
|
|
80
|
+
|
|
81
|
+
expect(() =>
|
|
82
|
+
create()
|
|
83
|
+
.eventAttributes(['fileId'] as never[])
|
|
84
|
+
.output(z.record(z.string(), z.string()) as unknown as z.ZodType<{ fileId: string }>)
|
|
85
|
+
.build()
|
|
86
|
+
).toThrow('Event attributes require the output schema to be a zod object');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('rejects names that do not match the key pattern', () => {
|
|
90
|
+
let schema = z.object({ _id: z.string(), '1st': z.string(), 'a-b': z.string() });
|
|
91
|
+
for (let field of ['_id', '1st', 'a-b']) {
|
|
92
|
+
expect(() =>
|
|
93
|
+
create()
|
|
94
|
+
.output(schema)
|
|
95
|
+
.eventAttributes([field as any])
|
|
96
|
+
).toThrow(`Event attribute "${field}" must start with a letter`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let long = 'a'.repeat(65);
|
|
100
|
+
expect(() => create().eventAttributes([long as never])).toThrow(
|
|
101
|
+
`Event attribute "${long}" must start with a letter`
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('rejects duplicates', () => {
|
|
106
|
+
expect(() => create().output(output).eventAttributes(['fileId', 'fileId'])).toThrow(
|
|
107
|
+
'Event attribute "fileId" is declared more than once'
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('allows at most eight fields', () => {
|
|
112
|
+
let fields = Array.from({ length: 9 }, (_, i) => `field${i}`);
|
|
113
|
+
let schema = z.object(Object.fromEntries(fields.map(field => [field, z.string()])));
|
|
114
|
+
|
|
115
|
+
expect(
|
|
116
|
+
create().output(schema).eventAttributes(fields.slice(0, 8)).build().eventAttributes
|
|
117
|
+
).toHaveLength(8);
|
|
118
|
+
expect(() => create().output(schema).eventAttributes(fields)).toThrow(
|
|
119
|
+
'A trigger can declare at most 8 event attributes'
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('can only be declared on triggers', () => {
|
|
124
|
+
let tool = SlateTool.create(spec, { key: 'send', name: 'Send' })
|
|
125
|
+
.input(z.object({}))
|
|
126
|
+
.output(z.object({ id: z.string() }));
|
|
127
|
+
|
|
128
|
+
expect(() => tool.eventAttributes(['id'])).toThrow(
|
|
129
|
+
'eventAttributes can only be set for trigger actions'
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
});
|
package/src/action/trigger.ts
CHANGED
|
@@ -21,6 +21,7 @@ export interface SlateTriggerParameters<
|
|
|
21
21
|
triggerGroup: SlateTriggerGroup<ConfigType, AuthType>;
|
|
22
22
|
matches: SlateTriggerMatchesHandler;
|
|
23
23
|
map: SlateTriggerMappingHandler<ConfigType, AuthType, InputType, OutputType>;
|
|
24
|
+
eventAttributes?: string[];
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
export class SlateTrigger<
|
|
@@ -34,6 +35,7 @@ export class SlateTrigger<
|
|
|
34
35
|
#triggerGroup: SlateTriggerGroup<ConfigType, AuthType>;
|
|
35
36
|
#matches: SlateTriggerMatchesHandler;
|
|
36
37
|
#map: SlateTriggerMappingHandler<ConfigType, AuthType, InputType, OutputType>;
|
|
38
|
+
#eventAttributes: string[];
|
|
37
39
|
|
|
38
40
|
private constructor(
|
|
39
41
|
spec: SlateSpecification<ConfigType, AuthType>,
|
|
@@ -46,6 +48,7 @@ export class SlateTrigger<
|
|
|
46
48
|
this.#triggerGroup = params.triggerGroup;
|
|
47
49
|
this.#matches = params.matches;
|
|
48
50
|
this.#map = params.map;
|
|
51
|
+
this.#eventAttributes = params.eventAttributes ?? [];
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
static create<ConfigType extends {}, AuthType extends {}>(
|
|
@@ -69,6 +72,10 @@ export class SlateTrigger<
|
|
|
69
72
|
get map() {
|
|
70
73
|
return this.#map;
|
|
71
74
|
}
|
|
75
|
+
|
|
76
|
+
get eventAttributes(): string[] {
|
|
77
|
+
return [...this.#eventAttributes];
|
|
78
|
+
}
|
|
72
79
|
}
|
|
73
80
|
|
|
74
81
|
export let trigger = <ConfigType extends {}, AuthType extends {}>(
|
|
@@ -88,3 +88,85 @@ describe('webhook trigger group declaration', () => {
|
|
|
88
88
|
).toThrow('A webhook delivery config requires an authenticate handler');
|
|
89
89
|
});
|
|
90
90
|
});
|
|
91
|
+
|
|
92
|
+
describe('trigger group selection options', () => {
|
|
93
|
+
let polling = (
|
|
94
|
+
selectionSchema: z.ZodType<Record<string, any>> = z.object({
|
|
95
|
+
fileId: z.string(),
|
|
96
|
+
tab: z.string().optional(),
|
|
97
|
+
fileIds: z.array(z.string()),
|
|
98
|
+
folder: z.object({ id: z.string() })
|
|
99
|
+
})
|
|
100
|
+
) =>
|
|
101
|
+
triggerGroup(spec, { key: 'group', name: 'Group', selectionSchema })
|
|
102
|
+
.polling({ pollEvents: async () => ({ events: [] }) })
|
|
103
|
+
.routingMatchers(async () => []);
|
|
104
|
+
let list = async () => ({ options: [{ value: 'a', label: 'A' }] });
|
|
105
|
+
let linked = { slate: 'acme/files', optionList: 'files', params: { kind: 'sheet' } };
|
|
106
|
+
|
|
107
|
+
it('rejects fields that are not top-level properties of the selection schema', () => {
|
|
108
|
+
for (let field of ['fileid', 'folder.id', 'id', 'constructor']) {
|
|
109
|
+
expect(() => polling().selectionOptions({ [field]: { list } })).toThrow(
|
|
110
|
+
`Selection field "${field}" is not a top-level property of the selection schema`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('rejects a selection schema that is not a zod object', () => {
|
|
116
|
+
for (let selectionSchema of [
|
|
117
|
+
z.record(z.string(), z.string()),
|
|
118
|
+
z.object({ fileId: z.string() }).optional() as z.ZodType<Record<string, any>>
|
|
119
|
+
]) {
|
|
120
|
+
expect(() => polling(selectionSchema).selectionOptions({ fileId: { list } })).toThrow(
|
|
121
|
+
'Selection options require the selection schema to be a zod object'
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
expect(() =>
|
|
125
|
+
triggerGroup(spec, { key: 'group', name: 'Group' }).selectionOptions({
|
|
126
|
+
fileId: { list }
|
|
127
|
+
})
|
|
128
|
+
).toThrow('Selection options require a selection schema');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('rejects a bare handler instead of { list, linked }', () => {
|
|
132
|
+
for (let declaration of [list, null]) {
|
|
133
|
+
expect(() => polling().selectionOptions({ fileId: declaration as any })).toThrow(
|
|
134
|
+
'Selection field "fileId" must be declared as { list, linked }'
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('accepts an own list, a linked list, or both per field', () => {
|
|
140
|
+
let group = polling()
|
|
141
|
+
.selectionOptions({ fileId: { list, linked } })
|
|
142
|
+
.selectionOptions({
|
|
143
|
+
tab: { linked: { slate: 'acme/files', optionList: 'tabs' } },
|
|
144
|
+
fileIds: { list }
|
|
145
|
+
})
|
|
146
|
+
.build();
|
|
147
|
+
expect(group.selectionOptions).toEqual({
|
|
148
|
+
fileId: { list, linked },
|
|
149
|
+
tab: { linked: { slate: 'acme/files', optionList: 'tabs' } },
|
|
150
|
+
fileIds: { list }
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('rejects a field without an own list or a linked list', () => {
|
|
155
|
+
expect(() => polling().selectionOptions({ fileId: {} })).toThrow(
|
|
156
|
+
'Selection field "fileId" needs its own options list, a linked option list, or both'
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('rejects linked lists that do not name a scoped slate and a list', () => {
|
|
161
|
+
expect(() =>
|
|
162
|
+
polling().selectionOptions({
|
|
163
|
+
fileId: { linked: { slate: 'files', optionList: 'files' } }
|
|
164
|
+
})
|
|
165
|
+
).toThrow('must be written as <scope>/<name>');
|
|
166
|
+
expect(() =>
|
|
167
|
+
polling().selectionOptions({
|
|
168
|
+
fileId: { linked: { slate: 'acme/files', optionList: ' ' } }
|
|
169
|
+
})
|
|
170
|
+
).toThrow('Selection field "fileId" must name the option list it links to');
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -3,6 +3,7 @@ import type { SlateContext } from '../context';
|
|
|
3
3
|
import { SlateDeclarationError } from '../error';
|
|
4
4
|
import type { SlateSpecification } from '../specification/specification';
|
|
5
5
|
import type { SlateWebhookHttpResponseInit } from './action';
|
|
6
|
+
import type { SlateOptionListPage } from './optionList';
|
|
6
7
|
|
|
7
8
|
export let SlateDefaultPollingIntervalSeconds = 60 * 10;
|
|
8
9
|
|
|
@@ -26,6 +27,80 @@ export type SlateTriggerGroupRoutingMatchersHandler<
|
|
|
26
27
|
AuthType extends {}
|
|
27
28
|
> = (context: SlateContext<ConfigType, AuthType, {}>) => Promise<SlateTriggerRoutingMatcher[]>;
|
|
28
29
|
|
|
30
|
+
/** Lists the values an account can pick for one selection field. `selection` holds what is entered so far and may be incomplete. */
|
|
31
|
+
export type SlateTriggerGroupSelectionOptionsHandler<
|
|
32
|
+
ConfigType extends {},
|
|
33
|
+
AuthType extends {}
|
|
34
|
+
> = (
|
|
35
|
+
context: SlateContext<
|
|
36
|
+
ConfigType,
|
|
37
|
+
AuthType,
|
|
38
|
+
{ query: string | null; pageToken: any | null; selection: SlateTriggerGroupSelection }
|
|
39
|
+
>
|
|
40
|
+
) => Promise<SlateOptionListPage>;
|
|
41
|
+
|
|
42
|
+
/** An option list exported by another integration, run with that integration's connection for the same account. */
|
|
43
|
+
export interface SlateLinkedOptionList {
|
|
44
|
+
/** Registry name of the integration, as `<scope>/<name>`. */
|
|
45
|
+
slate: string;
|
|
46
|
+
optionList: string;
|
|
47
|
+
params?: Record<string, any>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface SlateTriggerGroupSelectionOptionsSources<
|
|
51
|
+
ConfigType extends {},
|
|
52
|
+
AuthType extends {}
|
|
53
|
+
> {
|
|
54
|
+
list?: SlateTriggerGroupSelectionOptionsHandler<ConfigType, AuthType>;
|
|
55
|
+
linked?: SlateLinkedOptionList;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let LINKED_SLATE_PATTERN = /^[^/\s]+\/[^/\s]+$/;
|
|
59
|
+
|
|
60
|
+
let checkSelectionOptions = <ConfigType extends {}, AuthType extends {}>(
|
|
61
|
+
field: string,
|
|
62
|
+
sources: SlateTriggerGroupSelectionOptionsSources<ConfigType, AuthType>,
|
|
63
|
+
shape: Record<string, unknown>
|
|
64
|
+
): SlateTriggerGroupSelectionOptionsSources<ConfigType, AuthType> => {
|
|
65
|
+
if (!Object.hasOwn(shape, field)) {
|
|
66
|
+
throw new SlateDeclarationError(
|
|
67
|
+
`Selection field "${field}" is not a top-level property of the selection schema`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
if (typeof sources !== 'object' || sources === null) {
|
|
71
|
+
throw new SlateDeclarationError(
|
|
72
|
+
`Selection field "${field}" must be declared as { list, linked }`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let { list, linked } = sources;
|
|
77
|
+
if (!list && !linked) {
|
|
78
|
+
throw new SlateDeclarationError(
|
|
79
|
+
`Selection field "${field}" needs its own options list, a linked option list, or both`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
if (list !== undefined && typeof list !== 'function') {
|
|
83
|
+
throw new SlateDeclarationError(`Selection field "${field}" list must be a function`);
|
|
84
|
+
}
|
|
85
|
+
if (linked) {
|
|
86
|
+
if (typeof linked.slate !== 'string' || !LINKED_SLATE_PATTERN.test(linked.slate)) {
|
|
87
|
+
throw new SlateDeclarationError(
|
|
88
|
+
`Selection field "${field}" links to slate "${linked.slate}", which must be written as <scope>/<name>`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
if (typeof linked.optionList !== 'string' || !linked.optionList.trim()) {
|
|
92
|
+
throw new SlateDeclarationError(
|
|
93
|
+
`Selection field "${field}" must name the option list it links to`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
...(list ? { list } : {}),
|
|
100
|
+
...(linked ? { linked } : {})
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
|
|
29
104
|
export type SlateTriggerGroupPollHandler<ConfigType extends {}, AuthType extends {}> = (
|
|
30
105
|
context: SlateContext<
|
|
31
106
|
ConfigType,
|
|
@@ -292,6 +367,10 @@ export interface SlateTriggerGroupParameters<
|
|
|
292
367
|
webhook?: SlateTriggerGroupWebhookParameters<ConfigType, AuthType>;
|
|
293
368
|
gateway?: SlateTriggerGroupGatewayParameters<ConfigType, AuthType>;
|
|
294
369
|
routingMatchers: SlateTriggerGroupRoutingMatchersHandler<ConfigType, AuthType>;
|
|
370
|
+
selectionOptions?: Record<
|
|
371
|
+
string,
|
|
372
|
+
SlateTriggerGroupSelectionOptionsSources<ConfigType, AuthType>
|
|
373
|
+
>;
|
|
295
374
|
}
|
|
296
375
|
|
|
297
376
|
export class SlateTriggerGroup<
|
|
@@ -362,6 +441,10 @@ export class SlateTriggerGroup<
|
|
|
362
441
|
get routingMatchers() {
|
|
363
442
|
return this._params.routingMatchers;
|
|
364
443
|
}
|
|
444
|
+
|
|
445
|
+
get selectionOptions() {
|
|
446
|
+
return this._params.selectionOptions ?? {};
|
|
447
|
+
}
|
|
365
448
|
}
|
|
366
449
|
|
|
367
450
|
export class SlateTriggerGroupBuilder<
|
|
@@ -375,6 +458,10 @@ export class SlateTriggerGroupBuilder<
|
|
|
375
458
|
#gateway: SlateTriggerGroupGatewayParameters<ConfigType, AuthType> | null = null;
|
|
376
459
|
#routingMatchers: SlateTriggerGroupRoutingMatchersHandler<ConfigType, AuthType> | null =
|
|
377
460
|
null;
|
|
461
|
+
#selectionOptions: Record<
|
|
462
|
+
string,
|
|
463
|
+
SlateTriggerGroupSelectionOptionsSources<ConfigType, AuthType>
|
|
464
|
+
> = {};
|
|
378
465
|
|
|
379
466
|
constructor(
|
|
380
467
|
private readonly spec: SlateSpecification<ConfigType, AuthType>,
|
|
@@ -497,6 +584,34 @@ export class SlateTriggerGroupBuilder<
|
|
|
497
584
|
return this;
|
|
498
585
|
}
|
|
499
586
|
|
|
587
|
+
/**
|
|
588
|
+
* Lets the dashboard offer a list for these selection fields instead of a typed-in value.
|
|
589
|
+
* Each field must be a top-level property of the selection schema, which must be a zod object.
|
|
590
|
+
* For an array property the listed options are values for its items.
|
|
591
|
+
*/
|
|
592
|
+
selectionOptions(
|
|
593
|
+
declarations: Record<
|
|
594
|
+
string,
|
|
595
|
+
SlateTriggerGroupSelectionOptionsSources<ConfigType, AuthType>
|
|
596
|
+
>
|
|
597
|
+
): SlateTriggerGroupBuilder<ConfigType, AuthType, InputType> {
|
|
598
|
+
let schema = this.params.selectionSchema;
|
|
599
|
+
if (!schema) {
|
|
600
|
+
throw new SlateDeclarationError('Selection options require a selection schema');
|
|
601
|
+
}
|
|
602
|
+
if (schema._zod?.def?.type !== 'object') {
|
|
603
|
+
throw new SlateDeclarationError(
|
|
604
|
+
'Selection options require the selection schema to be a zod object'
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
let shape = (schema as z.ZodObject<any>).shape;
|
|
609
|
+
for (let [field, sources] of Object.entries(declarations)) {
|
|
610
|
+
this.#selectionOptions[field] = checkSelectionOptions(field, sources, shape);
|
|
611
|
+
}
|
|
612
|
+
return this;
|
|
613
|
+
}
|
|
614
|
+
|
|
500
615
|
build(): SlateTriggerGroup<ConfigType, AuthType, InputType> {
|
|
501
616
|
if (!this.#source) {
|
|
502
617
|
throw new SlateDeclarationError(
|
|
@@ -513,7 +628,8 @@ export class SlateTriggerGroupBuilder<
|
|
|
513
628
|
polling: this.#polling ?? undefined,
|
|
514
629
|
webhook: this.#webhook ?? undefined,
|
|
515
630
|
gateway: this.#gateway ?? undefined,
|
|
516
|
-
routingMatchers: this.#routingMatchers
|
|
631
|
+
routingMatchers: this.#routingMatchers,
|
|
632
|
+
selectionOptions: this.#selectionOptions
|
|
517
633
|
});
|
|
518
634
|
}
|
|
519
635
|
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
optionList,
|
|
5
|
+
SlateTool,
|
|
6
|
+
SlateTrigger,
|
|
7
|
+
SlateTriggerGroup,
|
|
8
|
+
toolCategory
|
|
9
|
+
} from '../action';
|
|
4
10
|
import { SlateAuth } from '../auth';
|
|
5
11
|
import { SlateConfig } from '../config';
|
|
6
12
|
import { Slate } from './slate';
|
|
@@ -320,3 +326,66 @@ describe('Slate.create tool categories', () => {
|
|
|
320
326
|
).toThrow('Tool category "crm" lists tool "stray", which is not registered in this slate');
|
|
321
327
|
});
|
|
322
328
|
});
|
|
329
|
+
|
|
330
|
+
describe('Slate.create option lists', () => {
|
|
331
|
+
it('keeps option lists in declared order with their params schema', () => {
|
|
332
|
+
let spec = createTestSpec();
|
|
333
|
+
let files = optionList(spec, {
|
|
334
|
+
key: 'files',
|
|
335
|
+
paramsSchema: z.object({ kind: z.string().optional() }),
|
|
336
|
+
list: async ctx => ({
|
|
337
|
+
options: [{ value: ctx.input.params.kind ?? 'any', label: 'Any' }]
|
|
338
|
+
})
|
|
339
|
+
});
|
|
340
|
+
let folders = optionList(spec, {
|
|
341
|
+
key: 'folders',
|
|
342
|
+
list: async () => ({ options: [] })
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
let slate = Slate.create({ spec, tools: [], triggers: [], optionLists: [files, folders] });
|
|
346
|
+
|
|
347
|
+
expect(slate.optionLists.map(list => list.key)).toEqual(['files', 'folders']);
|
|
348
|
+
expect(folders.paramsSchema.parse({})).toEqual({});
|
|
349
|
+
expect(files.paramsSchema.parse({ kind: 'sheet' })).toEqual({ kind: 'sheet' });
|
|
350
|
+
expect(Slate.create({ spec, tools: [], triggers: [] }).optionLists).toEqual([]);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('rejects params with unknown keys for the default and custom params schemas', () => {
|
|
354
|
+
let spec = createTestSpec();
|
|
355
|
+
let list = async () => ({ options: [] });
|
|
356
|
+
let folders = optionList(spec, { key: 'folders', list });
|
|
357
|
+
let files = optionList(spec, {
|
|
358
|
+
key: 'files',
|
|
359
|
+
paramsSchema: z.object({ kind: z.string().optional() }),
|
|
360
|
+
list
|
|
361
|
+
});
|
|
362
|
+
let loose = optionList(spec, {
|
|
363
|
+
key: 'loose',
|
|
364
|
+
paramsSchema: z.object({ kind: z.string() }).passthrough(),
|
|
365
|
+
list
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
expect(folders.paramsSchema.safeParse({ kind: 'sheet' }).success).toBe(false);
|
|
369
|
+
expect(files.paramsSchema.safeParse({ knd: 'sheet' }).success).toBe(false);
|
|
370
|
+
expect(loose.paramsSchema.safeParse({ kind: 'sheet', extra: 1 }).success).toBe(false);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('rejects a params schema that is not a zod object', () => {
|
|
374
|
+
expect(() =>
|
|
375
|
+
optionList(createTestSpec(), {
|
|
376
|
+
key: 'files',
|
|
377
|
+
paramsSchema: z.record(z.string(), z.string()) as any,
|
|
378
|
+
list: async () => ({ options: [] })
|
|
379
|
+
})
|
|
380
|
+
).toThrow('Option list "files" params schema must be a zod object');
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('rejects duplicate option list keys', () => {
|
|
384
|
+
let spec = createTestSpec();
|
|
385
|
+
let list = () => optionList(spec, { key: 'files', list: async () => ({ options: [] }) });
|
|
386
|
+
|
|
387
|
+
expect(() =>
|
|
388
|
+
Slate.create({ spec, tools: [], triggers: [], optionLists: [list(), list()] })
|
|
389
|
+
).toThrow('Option list "files" is registered more than once');
|
|
390
|
+
});
|
|
391
|
+
});
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
SlateOptionList,
|
|
3
|
+
SlateTool,
|
|
4
|
+
SlateToolCategory,
|
|
5
|
+
SlateTrigger,
|
|
6
|
+
SlateTriggerGroup
|
|
7
|
+
} from '../action';
|
|
2
8
|
import type { SlateAdapter } from '../adapter';
|
|
3
9
|
import { SlateDeclarationError } from '../error';
|
|
4
10
|
import type { SlateSpecification } from './specification';
|
|
@@ -13,7 +19,8 @@ export class Slate<ConfigType extends {}, AuthType extends {}> {
|
|
|
13
19
|
private readonly _adapters: SlateAdapter<ConfigType, AuthType>[],
|
|
14
20
|
private readonly _triggerGroups: SlateTriggerGroup<ConfigType, AuthType>[],
|
|
15
21
|
private readonly _toolCategories: SlateToolCategory[],
|
|
16
|
-
private readonly _toolCategoryKeysByActionKey: Map<string, string[]
|
|
22
|
+
private readonly _toolCategoryKeysByActionKey: Map<string, string[]>,
|
|
23
|
+
private readonly _optionLists: SlateOptionList<ConfigType, AuthType, any>[]
|
|
17
24
|
) {}
|
|
18
25
|
|
|
19
26
|
static create<ConfigType extends {}, AuthType extends {}>(params: {
|
|
@@ -23,6 +30,7 @@ export class Slate<ConfigType extends {}, AuthType extends {}> {
|
|
|
23
30
|
adapters?: SlateAdapter<ConfigType, AuthType>[];
|
|
24
31
|
triggerGroups?: SlateTriggerGroup<ConfigType, AuthType>[];
|
|
25
32
|
toolCategories?: SlateToolCategory[];
|
|
33
|
+
optionLists?: SlateOptionList<ConfigType, AuthType, any>[];
|
|
26
34
|
}) {
|
|
27
35
|
let adapters = params.adapters ?? [];
|
|
28
36
|
let actions = [...params.triggers, ...params.tools];
|
|
@@ -76,6 +84,17 @@ export class Slate<ConfigType extends {}, AuthType extends {}> {
|
|
|
76
84
|
);
|
|
77
85
|
}
|
|
78
86
|
|
|
87
|
+
let optionLists = params.optionLists ?? [];
|
|
88
|
+
let seenOptionListKeys = new Set<string>();
|
|
89
|
+
for (let list of optionLists) {
|
|
90
|
+
if (seenOptionListKeys.has(list.key)) {
|
|
91
|
+
throw new SlateDeclarationError(
|
|
92
|
+
`Option list "${list.key}" is registered more than once`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
seenOptionListKeys.add(list.key);
|
|
96
|
+
}
|
|
97
|
+
|
|
79
98
|
let toolCategories = params.toolCategories ?? [];
|
|
80
99
|
let toolCategoryKeysByActionKey = resolveToolCategoryMembership(toolCategories, actions);
|
|
81
100
|
|
|
@@ -85,7 +104,8 @@ export class Slate<ConfigType extends {}, AuthType extends {}> {
|
|
|
85
104
|
adapters,
|
|
86
105
|
triggerGroups,
|
|
87
106
|
toolCategories,
|
|
88
|
-
toolCategoryKeysByActionKey
|
|
107
|
+
toolCategoryKeysByActionKey,
|
|
108
|
+
optionLists
|
|
89
109
|
);
|
|
90
110
|
}
|
|
91
111
|
|
|
@@ -109,6 +129,10 @@ export class Slate<ConfigType extends {}, AuthType extends {}> {
|
|
|
109
129
|
return this._toolCategories ?? [];
|
|
110
130
|
}
|
|
111
131
|
|
|
132
|
+
get optionLists() {
|
|
133
|
+
return this._optionLists ?? [];
|
|
134
|
+
}
|
|
135
|
+
|
|
112
136
|
// Keys of the tool categories an action belongs to, in declared category order.
|
|
113
137
|
getToolCategoryKeys(actionKey: string) {
|
|
114
138
|
return [...(this._toolCategoryKeysByActionKey?.get(actionKey) ?? [])];
|