Skip to content

Commit fe7dd3c

Browse files
committed
feat: sync private provider updates
1 parent d07e27b commit fe7dd3c

181 files changed

Lines changed: 31817 additions & 3574 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/core/cast.test.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, it } from "vitest";
2-
import { base64Bytes, positiveInteger, requiredStringArray } from "./cast.ts";
2+
import { base64Bytes, optionalStringArray, positiveInteger, requiredStringArray } from "./cast.ts";
33

44
describe("cast helpers", () => {
55
it("decodes strict base64 bytes", () => {
@@ -26,4 +26,13 @@ describe("cast helpers", () => {
2626
it("rejects non-string array items", () => {
2727
expect(() => requiredStringArray(["one", 2], "values")).toThrow("values must be an array of strings");
2828
});
29+
30+
it("optionally reads arrays containing only strings", () => {
31+
const values = ["one", "two"];
32+
33+
expect(optionalStringArray(values)).toBe(values);
34+
expect(optionalStringArray([])).toEqual([]);
35+
expect(optionalStringArray(["one", 2])).toBeUndefined();
36+
expect(optionalStringArray(undefined)).toBeUndefined();
37+
});
2938
});

src/core/cast.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,14 @@ export function requiredStringArray(
162162
return value;
163163
}
164164

165+
/**
166+
* Return an array only when every item is already a string. Invalid or absent
167+
* values return undefined.
168+
*/
169+
export function optionalStringArray(value: unknown): string[] | undefined {
170+
return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : undefined;
171+
}
172+
165173
/**
166174
* Return every array item as a plain object record, or throw. Examples:
167175
* `objectArray([{ a: 1 }], "items") => [{ a: 1 }]`, `objectArray([1], "items")` throws.

src/core/json-schema.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,62 @@ describe("jsonSchema.looseObject", () => {
2626
});
2727
});
2828
});
29+
30+
describe("jsonSchema.nonEmptyString", () => {
31+
it("preserves additional string constraints", () => {
32+
expect(
33+
jsonSchema.nonEmptyString("A constrained identifier.", {
34+
maxLength: 16,
35+
pattern: "^[a-z]+$",
36+
}),
37+
).toEqual({
38+
type: "string",
39+
minLength: 1,
40+
maxLength: 16,
41+
pattern: "^[a-z]+$",
42+
description: "A constrained identifier.",
43+
});
44+
});
45+
});
46+
47+
describe("jsonSchema.nonWhitespaceString", () => {
48+
it("rejects empty and whitespace-only strings", () => {
49+
expect(jsonSchema.nonWhitespaceString("A meaningful value.", { maxLength: 64 })).toEqual({
50+
type: "string",
51+
minLength: 1,
52+
maxLength: 64,
53+
pattern: "\\S",
54+
description: "A meaningful value.",
55+
});
56+
});
57+
});
58+
59+
describe("jsonSchema.looseRequiredObject", () => {
60+
it("requires every property except explicitly optional properties", () => {
61+
expect(
62+
jsonSchema.looseRequiredObject(
63+
"A provider resource.",
64+
{
65+
id: jsonSchema.string("The resource identifier."),
66+
label: jsonSchema.string("The optional label."),
67+
},
68+
{ optional: ["label"] },
69+
),
70+
).toEqual({
71+
type: "object",
72+
properties: {
73+
id: { type: "string", description: "The resource identifier." },
74+
label: { type: "string", description: "The optional label." },
75+
},
76+
required: ["id"],
77+
additionalProperties: true,
78+
description: "A provider resource.",
79+
});
80+
81+
expect(
82+
jsonSchema.looseRequiredObject("A fully required resource.", {
83+
id: jsonSchema.string("The resource identifier."),
84+
}),
85+
).toMatchObject({ required: ["id"] });
86+
});
87+
});

src/core/json-schema.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export const jsonSchema = {
7777
options: { optional?: readonly string[] } = {},
7878
): JsonSchema {
7979
return this.object(description, properties, {
80-
optional: options.optional,
80+
optional: options.optional ?? [],
8181
additionalProperties: true,
8282
});
8383
},
@@ -115,10 +115,17 @@ export const jsonSchema = {
115115
return schema;
116116
},
117117

118-
nonEmptyString(description: string, options: Omit<JsonSchemaOptions, "description"> = {}): JsonSchema {
118+
nonEmptyString(description: string, options: Omit<StringOptions, "description" | "minLength"> = {}): JsonSchema {
119119
return this.string({ ...options, minLength: 1, description });
120120
},
121121

122+
nonWhitespaceString(
123+
description: string,
124+
options: Omit<StringOptions, "description" | "minLength" | "pattern"> = {},
125+
): JsonSchema {
126+
return this.string({ ...options, minLength: 1, pattern: "\\S", description });
127+
},
128+
122129
unknown(description: string): JsonSchema {
123130
return { description };
124131
},

src/providers/altiria/actions.ts

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
import type { ActionDefinition } from "../../core/types.ts";
2+
3+
import { s } from "../../core/json-schema.ts";
4+
import { defineProviderAction } from "../../core/provider-definition.ts";
5+
6+
const service = "altiria";
7+
8+
const customFieldSchema = s.object(
9+
"One Altiria contact custom field value.",
10+
{
11+
key: s.nonEmptyString("Custom field name."),
12+
type: s.stringEnum("Custom field type.", ["string", "date", "decimal"]),
13+
value: s.nonEmptyString("Custom field value."),
14+
},
15+
{ optional: ["type"] },
16+
);
17+
18+
const includeSchema = s.array(
19+
"Associated contact resources to include in the response.",
20+
s.stringEnum("One associated contact resource name.", ["customFields", "groups"]),
21+
{ minItems: 1 },
22+
);
23+
24+
const metadataSchema = s.looseObject("Pagination and other metadata returned by Altiria.");
25+
26+
const smsSchema = s.looseObject("One normalized Altiria SMS message.", {
27+
id: s.nullable(s.string("Altiria SMS message identifier.")),
28+
from: s.nullable(s.string("Sender value used for the SMS message.")),
29+
to: s.nullable(s.string("Recipient phone number.")),
30+
message: s.nullable(s.string("SMS message body.")),
31+
campaignId: s.nullable(s.integer("Altiria campaign identifier.")),
32+
sendingId: s.nullable(s.integer("Altiria sending identifier.")),
33+
isDelivered: s.nullable(s.boolean("Whether Altiria reports the message as delivered.")),
34+
isClicked: s.nullable(s.boolean("Whether Altiria reports a shortened link click.")),
35+
raw: s.looseObject("Raw Altiria SMS object."),
36+
});
37+
38+
const sendSmsOutputSchema = s.requiredObject("The result returned by Altiria after sending SMS.", {
39+
accepted: s.boolean("Whether Altiria accepted the SMS request."),
40+
messages: s.array("SMS message records returned by Altiria.", smsSchema),
41+
raw: s.unknown("Raw Altiria response payload."),
42+
});
43+
44+
const contactSchema = s.looseObject("One normalized Altiria contact.", {
45+
id: s.nullable(s.integer("Altiria contact identifier.")),
46+
email: s.nullable(s.string("Contact email address.")),
47+
phone: s.nullable(s.string("Contact mobile phone number.")),
48+
landline: s.nullable(s.string("Contact landline phone number.")),
49+
countryIso: s.nullable(s.string("Two-letter contact country code.")),
50+
name: s.nullable(s.string("Contact first name.")),
51+
surname: s.nullable(s.string("Contact surname.")),
52+
createdAt: s.nullable(s.string("Contact creation timestamp returned by Altiria.")),
53+
updatedAt: s.nullable(s.string("Contact update timestamp returned by Altiria.")),
54+
raw: s.looseObject("Raw Altiria contact object."),
55+
});
56+
57+
const groupSchema = s.looseObject("One normalized Altiria contact group.", {
58+
id: s.nullable(s.integer("Altiria group identifier.")),
59+
name: s.nullable(s.string("Altiria group name.")),
60+
raw: s.looseObject("Raw Altiria group object."),
61+
});
62+
63+
const contactWriteFields = {
64+
email: s.email("Contact email address."),
65+
phone: s.nonEmptyString("Contact mobile phone number with international prefix."),
66+
landline: s.nonEmptyString("Contact landline phone number."),
67+
countryIso: s.string("Two-letter country code required when phone or landline is specified.", {
68+
minLength: 2,
69+
maxLength: 2,
70+
}),
71+
groupsIds: s.array("Contact group IDs to attach this contact to.", s.integer("One Altiria group ID."), {
72+
minItems: 1,
73+
}),
74+
groupsNames: s.array("Contact group names to attach this contact to.", s.nonEmptyString("One Altiria group name."), {
75+
minItems: 1,
76+
}),
77+
name: s.nonEmptyString("Contact first name."),
78+
surname: s.nonEmptyString("Contact surname."),
79+
customFields: s.array("Custom fields to set on this contact.", customFieldSchema, {
80+
minItems: 1,
81+
}),
82+
};
83+
84+
const contactWriteOptionalKeys: readonly (keyof typeof contactWriteFields & string)[] = [
85+
"email",
86+
"phone",
87+
"landline",
88+
"countryIso",
89+
"groupsIds",
90+
"groupsNames",
91+
"name",
92+
"surname",
93+
"customFields",
94+
];
95+
96+
const sendSms = defineProviderAction(service, {
97+
name: "send_sms",
98+
description: "Send an SMS message through Altiria's REST SMS endpoint.",
99+
inputSchema: s.object(
100+
"The input payload for sending an Altiria SMS message.",
101+
{
102+
to: s.array(
103+
"Recipient mobile phone numbers with international prefix.",
104+
s.nonEmptyString("One recipient mobile phone number."),
105+
{ minItems: 1 },
106+
),
107+
from: s.nonEmptyString("SMS sender value registered or allowed in Altiria."),
108+
message: s.nonEmptyString("SMS body text to send."),
109+
notificationUrl: s.anyOf("Delivery notification URL, either shared by all recipients or one URL per recipient.", [
110+
s.url("One delivery notification URL."),
111+
s.array("Delivery notification URLs by recipient.", s.url("One delivery URL."), {
112+
minItems: 1,
113+
}),
114+
]),
115+
campaignName: s.nonEmptyString("Optional campaign name used in Altiria statistics."),
116+
tags: s.array("Campaign tags to attach to the send.", s.nonEmptyString("One tag."), {
117+
minItems: 1,
118+
}),
119+
certified: s.boolean("Whether to send the message as certified SMS."),
120+
splitParts: s.boolean("Whether Altiria should split long SMS messages into SMS parts."),
121+
flash: s.boolean("Whether to send the message as a Flash SMS."),
122+
sub: s.array(
123+
"Substitution variable objects, one per recipient.",
124+
s.record("Substitution variables for one recipient.", s.unknown("Variable value.")),
125+
{ minItems: 1 },
126+
),
127+
},
128+
{
129+
optional: ["notificationUrl", "campaignName", "tags", "certified", "splitParts", "flash", "sub"],
130+
},
131+
),
132+
outputSchema: sendSmsOutputSchema,
133+
});
134+
135+
const getSms = defineProviderAction(service, {
136+
name: "get_sms",
137+
description: "Fetch SMS information from Altiria by one or more message identifiers.",
138+
inputSchema: s.requiredObject("The input payload for fetching Altiria SMS information.", {
139+
id: s.nonEmptyString("Altiria SMS message identifier, or multiple identifiers separated by commas."),
140+
}),
141+
outputSchema: s.requiredObject("SMS messages returned by Altiria.", {
142+
messages: s.array("SMS message records returned by Altiria.", smsSchema),
143+
raw: s.unknown("Raw Altiria response payload."),
144+
}),
145+
});
146+
147+
const listContacts = defineProviderAction(service, {
148+
name: "list_contacts",
149+
description: "List contacts from the connected Altiria account.",
150+
inputSchema: s.object(
151+
"The input payload for listing Altiria contacts.",
152+
{
153+
limit: s.integer("Maximum contacts to return in one request.", { minimum: 1, maximum: 1000 }),
154+
page: s.integer("Result page to request.", { minimum: 1 }),
155+
include: includeSchema,
156+
},
157+
{ optional: ["limit", "page", "include"] },
158+
),
159+
outputSchema: s.requiredObject("Contacts returned by Altiria.", {
160+
contacts: s.array("Contact records returned by Altiria.", contactSchema),
161+
meta: s.nullable(metadataSchema),
162+
raw: s.unknown("Raw Altiria response payload."),
163+
}),
164+
});
165+
166+
const getContact = defineProviderAction(service, {
167+
name: "get_contact",
168+
description: "Fetch one Altiria contact by contact ID.",
169+
inputSchema: s.object(
170+
"The input payload for fetching one Altiria contact.",
171+
{
172+
id: s.integer("Altiria contact identifier."),
173+
include: includeSchema,
174+
},
175+
{ optional: ["include"] },
176+
),
177+
outputSchema: s.requiredObject("Contact returned by Altiria.", {
178+
contact: contactSchema,
179+
raw: s.unknown("Raw Altiria response payload."),
180+
}),
181+
});
182+
183+
const createContact = defineProviderAction(service, {
184+
name: "create_contact",
185+
description: "Create a new Altiria contact.",
186+
inputSchema: s.object("The input payload for creating an Altiria contact.", contactWriteFields, {
187+
optional: contactWriteOptionalKeys,
188+
}),
189+
outputSchema: s.requiredObject("Contact created by Altiria.", {
190+
contact: contactSchema,
191+
raw: s.unknown("Raw Altiria response payload."),
192+
}),
193+
});
194+
195+
const updateContact = defineProviderAction(service, {
196+
name: "update_contact",
197+
description: "Update an existing Altiria contact.",
198+
inputSchema: s.object(
199+
"The input payload for updating an Altiria contact.",
200+
{
201+
id: s.integer("Altiria contact identifier."),
202+
...contactWriteFields,
203+
},
204+
{ optional: contactWriteOptionalKeys },
205+
),
206+
outputSchema: s.requiredObject("Contact updated by Altiria.", {
207+
contact: contactSchema,
208+
raw: s.unknown("Raw Altiria response payload."),
209+
}),
210+
});
211+
212+
const deleteContact = defineProviderAction(service, {
213+
name: "delete_contact",
214+
description: "Delete one Altiria contact by contact ID.",
215+
inputSchema: s.requiredObject("The input payload for deleting one Altiria contact.", {
216+
id: s.integer("Altiria contact identifier."),
217+
}),
218+
outputSchema: s.requiredObject("Delete status returned after removing an Altiria contact.", {
219+
deleted: s.boolean("Whether the contact delete request succeeded."),
220+
}),
221+
});
222+
223+
const listGroups = defineProviderAction(service, {
224+
name: "list_groups",
225+
description: "List contact groups from the connected Altiria account.",
226+
inputSchema: s.object(
227+
"The input payload for listing Altiria contact groups.",
228+
{
229+
limit: s.integer("Maximum groups to return in one request.", { minimum: 1, maximum: 1000 }),
230+
page: s.integer("Result page to request.", { minimum: 1 }),
231+
},
232+
{ optional: ["limit", "page"] },
233+
),
234+
outputSchema: s.requiredObject("Contact groups returned by Altiria.", {
235+
groups: s.array("Group records returned by Altiria.", groupSchema),
236+
meta: s.nullable(metadataSchema),
237+
raw: s.unknown("Raw Altiria response payload."),
238+
}),
239+
});
240+
241+
export const altiriaActions: ActionDefinition[] = [
242+
sendSms,
243+
getSms,
244+
listContacts,
245+
getContact,
246+
createContact,
247+
updateContact,
248+
deleteContact,
249+
listGroups,
250+
];

0 commit comments

Comments
 (0)