UNPKG

10.7 kBJavaScriptView Raw
1const fs = require('fs');
2
3// The API is generated from the swagger-js module
4const Swagger = require('swagger-client');
5const BufferType = require('buffer-type');
6const FormData = require('form-data');
7const MimeType = require('mime-types');
8
9const { SquidexTokenManager } = require('./token_manager');
10const { buildFilterString } = require('./filter');
11const { MergeRecords } = require('./merge');
12const { Log } = require('./logger');
13
14/**
15 * Check response status is correct
16 * @param {the HTTP response} response
17 * @param {the expected status code} status
18 */
19function ensureValidResponse(response, status) {
20 if (response.status !== status) {
21 Log.Debug(`expected response status ${status} but received ${response.status}`);
22 throw new Error(`Status code is not OK (${status}) but ${response.status}`);
23 }
24}
25
26/**
27 * Check that a value is set or throw error
28 * @param {the argument to be checked} argument
29 */
30function ensureValidArg(argument) {
31 if (argument === undefined) {
32 const name = Object.keys({ argument })[0];
33 return new Error(`expected argument ${name} is undefined`);
34 }
35 return null;
36}
37
38
39/**
40 * SquidexClientManager is Javascript wrapper around the Squidex API provided via Swagger.
41 * The implementation relies on the swagger-js generation of the API.
42 *
43 */
44class SquidexClientManager {
45 // TODO: consider changing constructor to take a options object
46 // TODO: Should we expose variables like allowDrafts or pass .headers?
47 constructor(url, appName, id, secret) {
48 ensureValidArg(url); ensureValidArg(id); ensureValidArg(secret);
49 this.connectUrl = `${url}/identity-server/connect/token`;
50 this.projectSpecUrl = `${url}/api/content/${appName}/swagger/v1/swagger.json`;
51 this.squidexSpecUrl = `${url}/api/swagger/v1/swagger.json`;
52 this.appName = appName;
53 this.tokenManager = new SquidexTokenManager(
54 this.connectUrl, id, secret, process.env.DEBUG_TOKEN_CACHE,
55 );
56 this.options = { allowDrafts: true };
57 }
58
59 /**
60 * Handle token checking and renew if invalid. This function should be called before any
61 * API calls and is handled transparently by the squidex client manager instance.
62 */
63 async ensureValidClient() {
64 // Make sure we have a valid token before proceeding
65 if (this.squidexApi && this.client && this.tokenManager.isTokenValid()) {
66 return;
67 }
68
69 const token = await this.tokenManager.getToken();
70 const self = this;
71 // This client is for our project API
72 this.client = await new Swagger({
73 url: this.projectSpecUrl,
74 requestInterceptor: (req) => {
75 if (req.body && !req.headers['Content-Type']) {
76 req.headers['Content-Type'] = 'application/json';
77 }
78 req.headers.Authorization = `Bearer ${token}`;
79 if (self.options.allowDrafts) {
80 req.headers['X-Unpublished'] = 'DRAFT';
81 }
82 },
83 });
84 // The squidexApi client gives us access to the general API's like asset
85 this.squidexApi = await new Swagger({
86 url: this.squidexSpecUrl,
87 requestInterceptor: (req) => {
88 // eslint-disable-next-line no-underscore-dangle
89 if (req.body && req.body._currentStream !== undefined) {
90 // If a stream is detected, use multipart/form-data
91 req.headers['Content-Type'] = 'multipart/form-data';
92 } else if (req.body && !req.headers['Content-Type']) {
93 req.headers['Content-Type'] = 'application/json';
94 }
95 req.headers.Authorization = `Bearer ${token}`;
96 },
97 });
98 }
99
100 /**
101 * Return list of the API endpoints available, there should be one for each model.
102 */
103 Models() {
104 return this.client.apis;
105 }
106
107 /**
108 * Lookup the endpoint in the apis available
109 * @param {the CMS model name} name
110 */
111 GetModelByName(name) {
112 Log.Debug(`GetModelByName(${name})`);
113 // Find the API endpoint
114 const models = this.Models();
115 const model = models[name];
116 if (!model) {
117 throw new Error(`Unknown model name ${model}`);
118 }
119 return model;
120 }
121
122 /**
123 * Convenience function to retrieve all items for a model
124 * @param {the schema name} modelName
125 */
126 async AllRecordsAsync(modelName) {
127 await this.ensureValidClient();
128 Log.Debug(`AllRecords(${modelName})`);
129
130 const records = await this.RecordsAsync(modelName, { skip: 0 });
131 const all = records.items.slice();
132
133 if (records.total > 200) {
134 let top = records.total - all.length;
135 for (let i = all.length; i <= records.total; i += top) {
136 // eslint-disable-next-line no-await-in-loop
137 const s = await this.RecordsAsync(modelName, { skip: all.length, $top: top });
138 all.push(...s.items);
139 top = records.total - all.length;
140 if (records.total === all.length) {
141 break;
142 }
143 }
144 }
145 return all;
146 }
147
148 /**
149 * Retrieve the items for the model
150 * @param {the schema name} modelName
151 * @param {the query options} opts
152 */
153 async RecordsAsync(modelName, opts) {
154 await this.ensureValidClient();
155 Log.Debug(`Records(${modelName}, ${opts})`);
156 const model = this.GetModelByName(modelName);
157 // Query the contents of the endpoint
158 const payload = await model[`Query${modelName}Contents`](opts);
159 ensureValidResponse(payload, 200);
160 return JSON.parse(payload.data);
161 }
162
163 /**
164 * Get a record content
165 * @param {the API endpoint} modelName
166 * @param {object containing the id property} payload
167 */
168 async RecordAsync(modelName, payload) {
169 await this.ensureValidClient();
170 Log.Debug(`Record(${modelName}, ${payload})`);
171 const model = this.GetModelByName(modelName);
172 const response = await model[`Get${modelName}Content`](payload);
173 /**
174 * 200 OK
175 * The standard response for successful HTTP requests.
176 */
177 ensureValidResponse(response, 200);
178 return response.obj.data;
179 }
180
181 /**
182 * Create a record content
183 * @param {the API endpoint} modelName
184 * @param {the object representing what to create} payload
185 */
186 async CreateAsync(modelName, payload) {
187 await this.ensureValidClient();
188 Log.Debug(`Create(${modelName}, ${payload})`);
189 const model = this.GetModelByName(modelName);
190 const response = await model[`Create${modelName}Content`](payload);
191 // 201 means Created:
192 // The request has been fulfilled and a new resource has been created.
193 ensureValidResponse(response, 201);
194 return JSON.parse(response.data);
195 }
196
197 /**
198 * Delete a record content
199 * @param {the API endpoint} modelName
200 * @param {the object containing the id property} payload
201 */
202 async DeleteAsync(modelName, payload) {
203 await this.ensureValidClient();
204 Log.Debug(`Delete(${modelName}, ${payload})`);
205 const model = this.GetModelByName(modelName);
206 const response = await model[`Delete${modelName}Content`](payload);
207 // 204 No content
208 // The server accepted the request but is not returning any content.
209 // This is often used as a response to a DELETE request.
210 ensureValidResponse(response, 204);
211 return response;
212 }
213
214 /**
215 * Update a record content
216 * @param {the API endpoint} modelName
217 * @param {the object containing the id property} payload
218 */
219 async UpdateAsync(modelName, payload) {
220 await this.ensureValidClient();
221 Log.Debug(`Update(${modelName}, ${payload})`);
222 const model = this.GetModelByName(modelName);
223 const response = await model[`Update${modelName}Content`](payload);
224 /**
225 * 200 OK
226 * The standard response for successful HTTP requests.
227 */
228 ensureValidResponse(response, 200);
229 return response.obj;
230 }
231
232 /**
233 * Retrieve one item
234 * @param {the API endpoint} name
235 * @param {the field for the filter} identifier
236 * @param {the unique filter value} value
237 */
238 async FindOne(name, identifier, value) {
239 await this.ensureValidClient();
240 const filter = buildFilterString(`data/${identifier}/iv`, 'eq', value);
241 const records = await this.RecordsAsync(name, {
242 $filter: filter,
243 $top: 1,
244 });
245 return records.items[0];
246 }
247
248 /**
249 * Filter record contents
250 * @param {the API endpoint} name
251 * @param {the object to use for filtering} payload
252 * @param {the filter field name} fieldName
253 */
254 async FilterRecordsAsync(name, payload, fieldName) {
255 await this.ensureValidClient();
256 let uniqueValue = null;
257
258 const field = payload.data[`${fieldName}`];
259 if (field && field.iv) {
260 uniqueValue = field.iv;
261 } else if (field && !field.iv) {
262 throw new Error(`Found field but .iv is ${field.iv}`);
263 } else {
264 Log.Debug('assuming unique value is null');
265 }
266
267 const filter = buildFilterString(`data/${fieldName}/iv`, 'eq', uniqueValue);
268 const records = await this.RecordsAsync(name, {
269 $filter: filter,
270 top: 0,
271 });
272 return records.items;
273 }
274
275 /**
276 * Create or update a record content
277 * @param {the API endpoint} name
278 * @param {the object to create or update} payload
279 * @param {the unique field to identify} fieldName
280 */
281 async CreateOrUpdateAsync(name, payload, fieldName) {
282 await this.ensureValidClient();
283 Log.Debug(`CreateOrUpdate(${name}, ${payload}, ${fieldName})`);
284 const uniqueValue = payload.data[`${fieldName}`].iv;
285 const record = await this.FindOne(name, fieldName, uniqueValue);
286 const self = this;
287 if (record) {
288 const update = await self.UpdateAsync(name, {
289 id: record.id,
290 data: MergeRecords(record.data, payload.data),
291 });
292 if (update && !update.id) {
293 update.id = record.id;
294 }
295 return update;
296 }
297 const create = await this.CreateAsync(name, payload);
298 return create;
299 }
300
301 /**
302 * Create asset from local file
303 * @param {path to the file} assetUrl
304 */
305 async CreateAssetAsync(assetUrl) {
306 await this.ensureValidClient();
307 let mimeType = MimeType.lookup(assetUrl);
308 // TODO: add support for remote URLS
309
310 // Try using a buffer if mime type can't be recognized from the local file
311 if (!mimeType) {
312 const info = BufferType(fs.readFileSync(assetUrl));
313 mimeType = info.type;
314 }
315
316 if (!mimeType) {
317 throw new Error(`Invalid content type when looking up mime type for ${assetUrl}`);
318 }
319
320 try {
321 const form = new FormData();
322 form.append('file', fs.createReadStream(assetUrl));
323 form.append('mimeType', mimeType);
324
325 const res = await this.squidexApi.apis.Assets
326 .Assets_PostAsset({ app: this.appName }, { requestBody: form });
327 return res;
328 } catch (error) {
329 Log.Error(error);
330 return null;
331 }
332 }
333}
334module.exports.SquidexClientManager = SquidexClientManager;