Services
Overview
Services provide a flexible way to:
- run server code that can be isomorphically executed from the client or server while
- executing that code either on a web server or a background worker-type server.
A lot of boilerplate code is eliminated since server-side routes and controllers and client-side service code does not need to be written since Tyranid handles the marshaling and unmarshaling of data automatically across the HTTP call.
Furthermore, Tyranid services are strongly type-checked at both the client- and server-level using both run-time type checking and compile-time type-checking (via generated TypeScript interfaces).
Client calls to server methods will also pass through the standard fromClient() and toClient() mechanisms defined on types, fields, and documents.
Tyranid will also perform the following processing on service return values:
- Embedded documents will be automatically wrapped in the appropriate Tyr.Document instance.
- Embedded documents will also update the local database cache (similar to what something like normalizr would do).
Rethrowable exceptions thrown from inside services will be caught and rethrown in the client to ensure that error handling is also done isomorphically.
API
To implement a service:
- Define your service metadata in the collection definition under the service option.
const MyCollection = new Tyr.Collection({ ..., service: { my service methods }, ... }) as Tyr.MyCollectionCollection;
- Implement the MyCollectionService interface that tyranid-tdgen generates from your service metadata
and register it with the collection by assigning to service:
MyCollection.service = { async myServiceMethod1(myServiceMethod1Parameters ...) { ... }, async myServiceMethod2(myServiceMethod2Parameters ...) { ... }, ... };
Note that MyCollection.service is typed by tyranid-tdgen to implement MyCollectionService.
this Context
Inside service methods, this is available with the following properties:
Option | Notes |
---|---|
{ | |
auth: | The authorization object (usually a user) if the request was invoked from the client, otherwise undefined. |
req: | The express request object if the service was invoked from the client, otherwise undefined. |
res: | The express response object if the service was invoked from the client, otherwise undefined. |
source: | One of 'client' or 'server'. |
user: | The user making the request if the request was invoked from the client, otherwise undefined. |
} |
Example
Server /user.model.ts
const User = new Tyr.Collection({
...,
service: {
activationCodesCsv: {
route: '/api/user/activationCodesCsv',
help: ‘Lorem ipsum dolor sit amet, ...’,
params: {
orgIds: {
help: ‘Amet dolor sit ipsum’,
is: 'array',
of: { link: 'organization' }
},
groupIds: { is: 'array', of: { link: 'group' } },
},
return: {
is: 'object',
help: ‘Lorem ipsum dolor sit amet, ...’ },
fields: {
userId: { link: ‘user’ },
activationCode: { is: ‘url’ },
}
}
}
},
...
}) as Tyr.UserCollection;
// could also be defined in a user.service.ts file or someplace else
User.service = {
async activationCodesCsv(orgIds: ObjectId[], groupIds: ObjectId[]) {
…
}
};
Note that the
route: '/api/user/activationCodesCsv/',
line is redundant because the default route format is:
/api/collection name/service method name
Generated by tyranid-tdgen
interface UserService {
/**
* Lorem ipsum dolor sit amet, ...
* @param orgIds Amet dolor sit ipsum
* @return Lorem ipsum dolor sit amet, ...
*/
activationCodesCsv(orgIds: ObjectId[], groupIds: ObjectId[]):
Promise<{ userId: ObjectId, activationCode: string }>;
}
In addition, the following line will be injected into the type definition for a collection, ensuring that
service methods are implemented correctly.
...
service: UserService;
...
Example Usage
The example is the same for both client and server code.
const activationCodes = await User.activationCodesCsv(
userIds: myUserIds, groupIds: myGroupIds);
Roadmap
- Allow methods to be conditionally available on the client.
- Ability to run a service on a worker process
- ... or as an AWS Lambda
- Allow the HTTP verb to be customizable (currently they are all POSTs).
- Ability to indicate that some services can be "queued" up and do not need to be called immediately. For example, a telemetry service call could be queued up while a client is briefly offline.
- Add new Tyr.Service class so that services can be created without the need for a collection.
- Add ability to batch up service calls where would improve performance.
- Ability to conditionally expose service methods via a public API based on whether the service is internal and/or the security access of the client.
- Including generation of help similar to what swagger does and/or possibly generate a swagger interface.