1 | const fs = require('fs');
|
2 |
|
3 |
|
4 | const Swagger = require('swagger-client');
|
5 | const BufferType = require('buffer-type');
|
6 | const FormData = require('form-data');
|
7 | const MimeType = require('mime-types');
|
8 |
|
9 | const { SquidexTokenManager } = require('./token_manager');
|
10 | const { buildFilterString } = require('./filter');
|
11 | const { MergeRecords } = require('./merge');
|
12 | const { Log } = require('./logger');
|
13 |
|
14 | const { compatState, compatPayload } = require('./compat');
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 | function 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 |
|
30 |
|
31 |
|
32 | function 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 |
|
43 |
|
44 |
|
45 |
|
46 | class SquidexClientManager {
|
47 |
|
48 |
|
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 |
|
63 |
|
64 |
|
65 | async ensureValidClient() {
|
66 |
|
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 |
|
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 |
|
87 | this.squidexApi = await new Swagger({
|
88 | url: this.squidexSpecUrl,
|
89 | requestInterceptor: (req) => {
|
90 |
|
91 | if (req.body && req.body._currentStream !== undefined) {
|
92 |
|
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 |
|
104 |
|
105 | Models() {
|
106 | return this.client.apis;
|
107 | }
|
108 |
|
109 | |
110 |
|
111 |
|
112 |
|
113 | GetModelByName(name) {
|
114 | Log.Debug(`GetModelByName(${name})`);
|
115 |
|
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 |
|
126 |
|
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 |
|
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 |
|
152 |
|
153 |
|
154 |
|
155 | async RecordsAsync(modelName, opts) {
|
156 | await this.ensureValidClient();
|
157 | Log.Debug(`Records(${modelName}, ${opts})`);
|
158 | const model = this.GetModelByName(modelName);
|
159 |
|
160 | const payload = await model[`Query${modelName}Contents`](opts);
|
161 | ensureValidResponse(payload, 200);
|
162 | return JSON.parse(payload.data);
|
163 | }
|
164 |
|
165 | |
166 |
|
167 |
|
168 |
|
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 |
|
177 |
|
178 |
|
179 | ensureValidResponse(response, 200);
|
180 | return response.obj.data;
|
181 | }
|
182 |
|
183 | |
184 |
|
185 |
|
186 |
|
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 |
|
195 |
|
196 | ensureValidResponse(response, 201);
|
197 | return JSON.parse(response.data);
|
198 | }
|
199 |
|
200 | |
201 |
|
202 |
|
203 |
|
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 |
|
211 |
|
212 |
|
213 | ensureValidResponse(response, 204);
|
214 | return response;
|
215 | }
|
216 |
|
217 | |
218 |
|
219 |
|
220 |
|
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 |
|
229 |
|
230 |
|
231 | ensureValidResponse(response, 200);
|
232 | return response.obj;
|
233 | }
|
234 |
|
235 | |
236 |
|
237 |
|
238 |
|
239 |
|
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 |
|
253 |
|
254 |
|
255 |
|
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 |
|
280 |
|
281 |
|
282 |
|
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 |
|
306 |
|
307 |
|
308 | async CreateAssetAsync(assetUrl) {
|
309 | await this.ensureValidClient();
|
310 | let mimeType = MimeType.lookup(assetUrl);
|
311 |
|
312 |
|
313 |
|
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 | }
|
337 | module.exports.SquidexClientManager = SquidexClientManager;
|