UNPKG

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