Skedify SDK / API client
Release:
Development:
Goal
The goal of this SDK is to offer an easy to use tool to interact with the Skedify API when you're making a JavaScript integration.
Construction
One would create an SDK with a simple constructor function:
const SDK = new Skedify.API(options);
This would allow various authentication strategies to be represented in a similar way, using a "schema" methodology.
Construction utility
We also expose a utility to make creating instances a bit easier:
const SDK = new Skedify.API({
auth_provider: Skedify.API.createAuthProviderString("client", {
client_id: "someclientidtokengoeshere",
realm: "https://api.example.com"
}),
locale: "nl-BE"
});
{
"locale": "nl-BE",
"auth_provider": "client://client_id=someclientidtokengoeshere&realm=https%3A%2F%2Fapi.example.com"
}
Existing appointments using a resource_code
If you already have an appointment you also have a resource_code
. This code can be used to authenticate and get an appointment_token
. You can achieve this by using the following code:
const SDK = new Skedify.API({
auth_provider: Skedify.API.createAuthProviderString("client", {
client_id: "someclientidtokengoeshere",
realm: "https://api.example.com",
resource_code: "theresourcecodegoeshere"
}),
locale: "nl-BE"
});
{
"locale": "nl-BE",
"auth_provider": "client://client_id=someclientidtokengoeshere&realm=https%3A%2F%2Fapi.example.com&resource_code=theresourcecodegoeshere"
}
Options/configuration
The sdk requires several configuration options. These might also be updated on the fly after the client was already created.
console.log(SDK.configuration)
SDK.configure({ ... })
The options are:
-
auth_provider: Authentication Provider, a string which symbolizes how/where to get an Authorization header
-
locale: A string combination of an ISO-639 Language Code and an optional ISO-3166 Country Code signifying the language preference of the user:
-
Correct values:
-
Incorrect values:
NL
: The first part should be lowercase.nl-be
: The second part should be uppercase.nl-BE-VWVX
: The last part can only contain 2 or 3 characters.
-
onError: A function called when there is an API error that can't be recovered automatically by the SDK
Basic Usage
Let's start with a simple use case: getting a list of subjects
SDK.subjects()
.then(
(response) => {
response.data.forEach((record) => {
record.id;
record.title;
record.duration = 60;
record.save();
record.delete();
record.replace();
});
},
(error) => {
}
);
NOTE: check that, if an ID is provided but is falsey, it's still treated as a call for a single item (throw Error in this case) and not incorrectly interpreted as a collection call
NOTE: the retryable
and retry
properties on error are optional features at first and can be added later without conflicts.
NOTE: the isWritable
method on a record and any additional logic related to access control are optional and can be added later without conflicts.
Instead of getting a collection, you could also directly access one member of a collection.
This can be done by providing the ID of the member (as a string) to the collection function.
SDK.subjects("subject id here").then(
(response) => {
},
(error) => {
}
);
Subresources
You can get subresources by chaining the original collection call with a subresource call
SDK.subjects("subject id here")
.questions()
.then(
(response) => {
},
(error) => {}
);
Includes
In addition to regular GET requests, you can also include related resources in a request.
This is done by chaining the original call with an extra call to an include
method on the promise.
An include is described by the "collection" method on SDK. You can pass multiple includes in one include call.
SDK.subjects()
.include(SDK.include.subject_category, SDK.include.subjects.questions)
.then(
(response) => {},
(error) => {}
);
SDK.subjects()
.include(SDK.include.subject_category)
.include(SDK.include.subjects.questions)
.then(
(response) => {},
(error) => {}
);
SDK.subjects()
.include(SDK.include.subject_category, SDK.include.subjects.questions)
.then(
(response) => {
response.data.forEach((subjectRecord) => {
});
},
(error) => {}
);
Sorting
The user can request server-side sorting by using the .sort
method. It requires a callback parameter that configures the sorting order
SDK.subjects().sort(item => {
item.distanceTo(geo, item.ASCENDING))
item.lastName(item.ASCENDING).firstName(item.DESCENDING)
})
Paging
The user can limit the amount of responses by using the .limit
& .page
method.
SDK.subjects()
.limit(10)
.page(1)
.then((response) => {
response.paging.size;
response.paging.currentPage;
response.paging.totalResults;
response.paging.totalPages;
response.paging.hasNext;
response.paging.hasPrevious;
response.paging.next();
response.paging.previous();
});
Filters
It should be possible to filter the result set based on certain conditions.
SDK.subjects().filter((item) =>
item.or(
item
.schedulable_at_office(["3"])
.schedulable_with_contact(["1", "2"])
.schedulable(),
item.schedulable_at_office(["6"]).schedulable_with_contact(["4", "5"])
)
);
SDK.filter(cb).filter(cb);
SDK.filter(cb).orFilter(cb);
NOTE: item.or
is currently not supported by the API and therefor should not (yet) be implemented.
However, we leave this note to keep forward compatibility in mind.
This also applies to the .orFilter
method
Creation
A new member of a collection can be created by using the .new
method on the collection instead of calling it directly.
Such an action will be validated before actually attempting to perform it.
The user is required to confirm the action by calling the .create
method on the record
SDK.appointments()
.new({
})
.then(
(appointment) => appointment.create(),
(validation_error) => {}
)
.then(
(response) => {
},
(response_error) => {}
);
Updating
If you want to update an entity you can do it as follows:
SDK.appointments(1207)
.update({
})
.then((appointment) => appointment.save())
.then(
(response) => {
},
(response_error) => {}
);
If you want to update multiple entities at once, you can use an array as the data and omit the identifier:
SDK.appointments()
.update([{ id: 1 }, { id: 2 }, { id: 3 }])
.then((appointment) => appointment.save());
Delete
If you want to delete an entity you can do it as follows:
SDK.appointments(1207)
.delete()
.then((appointment) => appointment.delete());
If you want to delete multiple entities at once, you can use an array as the identifier:
SDK.appointments([1, 2, 3, 4])
.delete()
.then((appointment) => appointment.delete());
NOTE: adding validation is an optional feature that can be added on later without breaking backwards compatibility
NOTE: validation (or other) errors returned by the API could/should be machine-readable and parsed into a practical format usable by the SDK user.
NOTE: the above design regarding validation and normalisation of error responses also apply to .update()
and .replace()
calls on entities.
You can also specify custom headers on requests:
SDK.appointments
.headers({ "X-Scheduling-Rules": "disallow-appointment-overlap" })
.update({
})
.then((appointment) => appointment.save())
.then(
(response) => {
},
(response_error) => {}
);
External identifiers
Users of the SDK will often want to interact with entities that correspond to entities in their own applications.
These entities will be mapped by making use of external ID's.
This means that often, developers will query/interact with entities based on their external id rather than their (Skedify-internal) actual ID.
To make this use-case easier, ID strings can be overloaded with a schema to signify that an entity is reffered to by their external ID rather than the internal one.
SDK.appointments("external://abc").then((response) => response.data);
SDK.appointments()
.filter((item) => item.external_id(["abc"]))
.then((response) => response.data);
By using an schema-like syntax, we make this mechanism extendable for multiple external ID's.
In other words, a Skedify entity might be known in multiple external system with varying ID's.
These systems could each assign their own external ID in their own namespace.
const appointment = {
id: "123",
external_id: "456",
external_ids: {
integration_one: "abc",
integration_two: "def",
integration_three: "ghi",
},
};
SDK.appointments("123");
SDK.appointments("external://456");
SDK.appointments("integration_one://abc");
SDK.appointments("integration_two://def");
SDK.appointments("integration_three://ghi");
We assume (by default) that there is a one-on-one mapping between internal entities' IDs and external entities.
The overloading of ID's adds extra checks that take this mapping into account.
However, other applications might have a use case for one-on-many mappings.
We allow method chaining to indicate a diversion from our default behaviour.
const appointment1 = {
id: '123',
external_id: 'abc'
}
const appointment2 = {
id: '456',
external_id: 'abc'
}
SDK.appointments('external://abc').catch(error => {
console.assert(error.type === API.ERROR_RESPONSE)
console.assert(error.subtype === API.ERROR_RESPONSE_MULTIPLE_RESULTS_FOUND)
console.assert(error.alternatives ~= [appointment1, appointment2])
console.assert(error.response.data === error.alternatives)
})
SDK.appointments('external://def').catch(error => {
console.assert(error.type === API.ERROR_RESPONSE)
console.assert(error.subtype === API.ERROR_RESPONSE_NO_RESULTS_FOUND)
})
Some external applications might not follow our assumption that there is a one-on-one relation between external ID's and Skedify entities.
In this case, it remains possible to interact with the collection as a whole and filter:
SDK.appointments().filter((item) => {
item.external_id("abc");
});
We might provide utility functions on top of the shorthand for common use cases, but these can only be implemented once we have a strong understanding of what/how integrations will interact with the external ID's
SDK.appointments("external://abc")
.firstOfMultiple()
.then((result) => {
console.assert(result.data === appointment1);
});
SDK.appointments("external://abc")
.mostRecentlyUpdatedOfMultiple()
.then((result) => {
console.assert(result.data === appointment1);
});
Diversions
For ease of use, some methods are exposed that are intentionally different from what the API exposes to make usage more obvious
Overrides on subjects per office:
The endpoint /offices/:oid/subject_settings
offers entities which have a seperate ID and a subject_id. These are not identical, so searching for the overrides on a specific subject "should" be done using a filter. However, it makes more sense to treat the subjectid as an ID, since the overrides will only every be looked up in that way directly.
SDK.offices("officeid").subjectSettings("subjectid");
SDK.offices("officeid")
.subjectSettings()
.filter((item) => item.subject_id("subjectid"))
.then((response) => {
if (response.data.length !== 0) {
throw NotFoundError();
}
});
Enterprise settings
The enterprise_settings
endpoint exposes a collection which will only every have one member. This makes it confusing to use. Instead, treat it like a subresource of "enterprise"
SDK.enterprise()
.settings()
.then((response) => response.data);
SDK.enterpriseSettings().then((response) => response.data[0]);
Common actions on appointments
Since certain mutations are very common, there should be shorthands for those
SDK.appointments('appointment id').accept('possibility id')
SDK.appointments('appointment id').then({ data } => {
data.accepted_possibility_id = 'possibility id'
data.state = 'accepted'
data.save()
})
Custom Domain Map
Some endpoints require a domain mapping when instantiating the SDK. Consider the
following example:
const SDK = new API({
auth_provider: API.createAuthProviderString("public_client", {
client_id: "someclientidtokengoeshere",
realm: "https://api.example.com",
}),
locale: "nl-BE",
resource_domain_map: {
events: {
url: "https://events-api.example.com",
},
"users.events": {
url: "https://users-events-api.example.com",
},
},
});
Testing
If you want to test your application's code, you can use these test utils so that you can safely execute the calls.
import {
installSkedifySDKMock,
uninstallSkedifySDKMock,
matchRequest,
mockResponse,
mockMatchingURLResponse
mostRecentRequest,
mockedRequests
} from "skedify-sdk";
const SDK = new Skedify.API(options);
uninstallSkedifySDKMock(SDK);
installSkedifySDKMock(SDK);
mockResponse({
id: 1,
name: 'subject 1'
})
SDK.subjects().then(console.log)
{
status: 200,
headers: undefined,
data: { id: '1', name: 'subject 1' },
warnings: undefined,
meta: undefined
}
mockMatchingURLResponse(/appointments/, [{ id: 'appointment id 1' }])
mockMatchingURLResponse(/subjects/, [{ id: 'subject id 1' }])
SDK.appointments().then(console.log)
SDK.subjects().then(console.log)
it("should make a call to fetch all subjects", async () => {
expect(await matchRequest(SDK.subjects())).toMatchSnapshot()
})
Object {
"data": undefined,
"headers": Object {
"Accept": "application/json, text/plain, */*",
"Accept-Language": "nl-BE, nl;q=0.667, *;q=0.333",
"Authorization": "Bearer fake_example_access_token",
},
"method": "get",
"params": undefined,
"url": "https://api.example.com/subjects",
}
Contributing
Use npm run commit
when you want to commit a change.
Typescript
There are experimental type definition available.
You can include them in your typescript project by adding a .d.ts
file with the following content:
/// <reference types="skedify-sdk/types" />
So for example:
./src/skedify-sdk.d.ts
:
Will add typescript definitions for a couple of exposed API's.
Please note that these are experimental and will be updated with possible breaking changes.
Creating a release candidate
To create a release candidate, push a new tag. The version in package.json
will be the same as the tagname without the v
prefix.
git tag v5.0.0-rc.1 && git push origin v5.0.0-rc.1