-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathservices.html
More file actions
181 lines (145 loc) · 6.54 KB
/
Copy pathservices.html
File metadata and controls
181 lines (145 loc) · 6.54 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
---
layout: default
---
<div class="content">
<h1>Services</h1>
<h2>Overview</h2>
<p><b>Services</b> provide a flexible way to:
<ol>
<li>run server code that can be <b>isomorphically</b> executed from the client or server while
<li>executing that code either on a web server or a background worker-type server.
</ol>
</p>
<p>A lot of <b>boilerplate code is eliminated</b> 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.</p>
<p>Furthermore, Tyranid services are <b>strongly type-checked</b> at both the client- and server-level using both
run-time type checking and compile-time type-checking (via generated TypeScript interfaces).</p>
<p>Client calls to server methods will also pass through the standard <u>fromClient()</u> and <u>toClient()</u>
mechanisms defined on types, fields, and documents.</p>
<p>Tyranid will also perform the following processing on service return values:
<ul>
<li>Embedded documents will be automatically wrapped in the appropriate <u>Tyr.Document</u> instance.
<li>Embedded documents will also update the local database cache (similar to what something like <u>normalizr</u>
would do).
</ul>
</p>
<p><a href="exceptions">Rethrowable exceptions</a> thrown from inside services will be caught and rethrown in the client to ensure that
error handling is also done isomorphically.
<div class="best-practice">Because exceptions are caught and rethrown in the client, this means you should generally not
return error codes or error messages from your services -- throw <a href="appError"><u>AppError</u></a>, <a href="secureError"><u>SecureError</u></a>,
or <a href="userError"><u>UserError</u></a> exceptions instead.</div>
<h2>API</h2>
<p>To implement a service:<p>
<ol>
<li>Define your service metadata in the <a href="collection#def">collection definition</a> under the <u>service</u> option.</p>
<pre><code class="js">const <i>MyCollection</i> = new Tyr.Collection({
...,
service: {
<i>my service methods</i>
},
...
}) as Tyr.<i>MyCollection</i>Collection;</code></pre>
<li>Implement the <u><i>MyCollection</i>Service</u> interface that <u>tyranid-tdgen</u> generates from your service metadata
and register it with the collection by assigning to <u>service</u>:
<pre><code class="js"><i>MyCollection</i>.service = {
async <i>myServiceMethod1</i>(<i>myServiceMethod1Parameters ...</i>) {
...
},
async <i>myServiceMethod2</i>(<i>myServiceMethod2Parameters ...</i>) {
...
},
...
};</code></pre>
<p>Note that <u><i>MyCollection</i>.service</u> is typed by <u>tyranid-tdgen</u> to implement <u><i>MyCollection</i>Service</u>.
</ol>
<h3 id="this"><u>this</u> Context</h3>
<p>Inside service methods, <u>this</u> is available with the following properties:</p>
<table class="def">
<thead>
<tr><th>Option<th>Notes
</thead>
<tr><td>{</td>
<tr><td> auth:<td>The authorization object (usually a user) if the request was invoked from the client, otherwise undefined.
<tr><td> req:<td>The <a href="https://expressjs.com/en/api.html#req">express request</a> object if the service was invoked from the client, otherwise undefined.
<tr><td> res:<td>The <a href="https://expressjs.com/en/api.html#res">express response</a> object if the service was invoked from the client, otherwise undefined.
<tr><td> source:<td>One of <u>'client'</u> or <u>'server'</u>.
<tr><td> user:<td>The user making the request if the request was invoked from the client, otherwise undefined.
<tr><td>}</td>
</table>
</article>
<h2>Example</h2>
<h3>Server <u>/user.model.ts</u></h3>
<pre><code class="js">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[]) {
…
}
};
</code></pre>
<p>Note that the</p>
<pre><code class="js">route: '/api/user/activationCodesCsv/',</code></pre>
<p>line is redundant because the default route format is:</p>
<div>
<pre><code class="js">/api/<i>collection name</i>/<i>service method name</i></code></pre>
<h3>Generated by <u>tyranid-tdgen</u></h3>
<pre><code class="js">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 }>;
}</code></pre>
In addition, the following line will be injected into the type definition for a collection, ensuring that
service methods are implemented correctly.
<pre><code class="js">...
service: UserService;
...
</code></pre>
<h3>Example Usage</h3>
<p>The example is the same for both client and server code.</p>
<pre><code class="js">const activationCodes = await User.activationCodesCsv(
userIds: myUserIds, groupIds: myGroupIds);</code></pre>
<h2 id="roadmap">Roadmap</h2>
<ul>
<li>Allow methods to be conditionally available on the client.
<li>Ability to run a service on a worker process
<li>... or as an AWS Lambda
<li>Allow the HTTP verb to be customizable (currently they are all POSTs).
<li>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.
<li>Add new <u>Tyr.Service</u> class so that services can be created without the need for a collection.
<li>Add ability to batch up service calls where would improve performance.
<li>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.
<li>Including generation of help similar to what swagger does and/or possibly generate a swagger interface.
</ul>
</div>