@pintawebware/strapi-sync 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,347 @@
1
+ const axios = require('axios');
2
+
3
+ function unwrapError(error) {
4
+ if (error && error.errors && Array.isArray(error.errors) && error.errors.length > 0) {
5
+ return error.errors[0];
6
+ }
7
+ return error;
8
+ }
9
+
10
+ function formatStrapiError(error, context = '') {
11
+ const err = unwrapError(error);
12
+ if (!err.response) {
13
+ const msg = err.message || err.code || String(err);
14
+ const contextStr = context ? ` (${context})` : '';
15
+ return `Request failed${contextStr}. ${msg}. Check STRAPI_URL and network.`;
16
+ }
17
+ const status = err.response.status;
18
+ const msg = err.response.data?.error?.message || err.response.data?.message || err.message;
19
+ const contextStr = context ? ` (${context})` : '';
20
+ switch (status) {
21
+ case 401:
22
+ return `Strapi returned 401 Unauthorized${contextStr}. Check that STRAPI_API_TOKEN is set correctly and has access to the API.`;
23
+ case 403:
24
+ return `Strapi returned 403 Forbidden${contextStr}. The API token may not have permission for this action.`;
25
+ case 404:
26
+ return `Strapi returned 404 Not Found${contextStr}. ${msg || 'Resource not found.'}`;
27
+ case 405: {
28
+ const isEntry = context && /entry|content/i.test(context);
29
+ return isEntry
30
+ ? `Strapi returned 405 Method Not Allowed${contextStr}. The content type may not exist yet or the API may not allow this. If you just wrote schema files, restart Strapi to load the new content type, then run strapi-sync again. Otherwise check API permissions in Strapi Admin.`
31
+ : `Strapi returned 405 Method Not Allowed${contextStr}. This operation is not supported by the Strapi API.`;
32
+ }
33
+ case 500:
34
+ return `Strapi server error (500)${contextStr}. ${msg || 'Try again later.'}`;
35
+ default:
36
+ return `Strapi request failed (${status})${contextStr}. ${msg || err.message}`;
37
+ }
38
+ }
39
+
40
+ function sanitizePayload(data) {
41
+ const payload = { ...(data || {}) };
42
+ delete payload.id;
43
+ delete payload.documentId;
44
+ return payload;
45
+ }
46
+
47
+ class StrapiSyncClient {
48
+ constructor(config) {
49
+ this.client = axios.create({
50
+ baseURL: config.baseURL,
51
+ timeout: config.timeout || 30000,
52
+ headers: {
53
+ 'Content-Type': 'application/json',
54
+ ...(config.apiToken && { Authorization: `Bearer ${config.apiToken}` }),
55
+ },
56
+ });
57
+ }
58
+
59
+ async ping() {
60
+ try {
61
+ await this.client.get('/api', { timeout: 5000 });
62
+ return true;
63
+ } catch (e) {
64
+ if (e.code === 'ECONNREFUSED' || e.code === 'ECONNRESET' || e.code === 'ETIMEDOUT') return false;
65
+ if (e.response && typeof e.response.status === 'number') return true;
66
+ return false;
67
+ }
68
+ }
69
+
70
+ async _getContentTypeBuilderPayload() {
71
+ const response = await this.client.get('/api/content-type-builder/content-types');
72
+ return response.data?.data ?? response.data;
73
+ }
74
+
75
+ async getAllContentTypes() {
76
+ try {
77
+ const raw = await this._getContentTypeBuilderPayload();
78
+ if (!raw || typeof raw !== 'object') return [];
79
+ const list = Array.isArray(raw)
80
+ ? raw
81
+ : raw.contentTypes
82
+ ? Object.values(raw.contentTypes)
83
+ : Object.keys(raw).map((uid) => ({ uid }));
84
+ return list
85
+ .filter((x) => x && (x.uid || x.apiID || typeof x === 'string') && x.schema)
86
+ .map((x) => {
87
+ const uid = x.uid || x.apiID || x;
88
+ const str = typeof uid === 'string' ? uid : '';
89
+ const schema = x.schema || {};
90
+ return {
91
+ name: schema.displayName,
92
+ uid: str,
93
+ kind: schema.kind,
94
+ apiID: x.apiID || schema.displayName,
95
+ pluralName: schema.pluralName ? String(schema.pluralName).toLowerCase() : null,
96
+ schema
97
+ };
98
+ })
99
+ .filter((x) => x.uid.startsWith('api::'));
100
+ } catch (error) {
101
+ const status = error.response?.status;
102
+ if (status === 401 || status === 403) {
103
+ const e = new Error(formatStrapiError(error, 'fetching content types list'));
104
+ e.status = status;
105
+ throw e;
106
+ }
107
+ if (status === 404 || status === 405) return [];
108
+ throw new Error(formatStrapiError(error, 'fetching content types list'));
109
+ }
110
+ }
111
+
112
+ async getAllComponents() {
113
+ try {
114
+ let list = [];
115
+ try {
116
+ const response = await this.client.get('/api/content-type-builder/components');
117
+ const raw = response.data?.data ?? response.data;
118
+ if (raw) list = Array.isArray(raw) ? raw : (raw.components ? Object.values(raw.components) : []);
119
+ } catch (e) {
120
+ if (e.response?.status !== 404 && e.response?.status !== 405) throw e;
121
+ const payload = await this._getContentTypeBuilderPayload();
122
+ if (payload && typeof payload === 'object' && payload.components)
123
+ list = Array.isArray(payload.components) ? payload.components : Object.values(payload.components);
124
+ }
125
+ return list
126
+ .filter((c) => c && (c.uid || (c.category && c.schema)))
127
+ .map((c) => {
128
+ const uid = c.uid || (c.category && c.apiId ? `${c.category}.${c.apiId}` : (c.category && c.schema?.displayName ? `${c.category}.${String(c.schema.displayName).replace(/\s+/g, '-').toLowerCase()}` : ''));
129
+ const schema = c.schema || c;
130
+ return uid ? { uid: String(uid), schema: typeof schema === 'object' ? schema : {} } : null;
131
+ })
132
+ .filter(Boolean);
133
+ } catch (error) {
134
+ const status = error.response?.status;
135
+ if (status === 401 || status === 403) {
136
+ const e = new Error(formatStrapiError(error, 'fetching components list'));
137
+ e.status = status;
138
+ throw e;
139
+ }
140
+ if (status === 404 || status === 405) return [];
141
+ return [];
142
+ }
143
+ }
144
+
145
+ async getContentTypeSchema(contentType) {
146
+ try {
147
+ const response = await this.client.get(`/api/content-type-builder/content-types/api::${contentType}.${contentType}`);
148
+ return response.data;
149
+ } catch (error) {
150
+ if (error.response?.status === 404) {
151
+ return null;
152
+ }
153
+ const e = new Error(formatStrapiError(error, `content type "${contentType}"`));
154
+ e.status = error.response?.status;
155
+ throw e;
156
+ }
157
+ }
158
+
159
+ getApiId(contentType, options = {}) {
160
+ let id = options.apiId ?? (contentType.endsWith('s') ? contentType : `${contentType}s`);
161
+ if (typeof id !== 'string') return id;
162
+ if (options.singleType === true && id.endsWith('s')) return id.slice(0, -1);
163
+ if (options.singleType === false && !id.endsWith('s')) return id.endsWith('y') ? id.slice(0, -1) + 'ies' : id + 's';
164
+ return id;
165
+ }
166
+
167
+ async getLocales() {
168
+ try {
169
+ const r = await this.client.get('/api/i18n/locales');
170
+ const payload = r.data;
171
+ const list = Array.isArray(payload)
172
+ ? payload
173
+ : Array.isArray(payload?.data)
174
+ ? payload.data
175
+ : Array.isArray(payload?.locales)
176
+ ? payload.locales
177
+ : [];
178
+ if (list.length > 0) {
179
+ return list
180
+ .map((l) => (typeof l === 'string' ? l : (l.code || l.locale || l.name || l.id)))
181
+ .filter(Boolean);
182
+ }
183
+ return ['en'];
184
+ } catch (_) {
185
+ return ['en'];
186
+ }
187
+ }
188
+
189
+ async getEntries(contentType, filters = {}, options = {}) {
190
+ const { singleType = false, pageSize, locale, localized, populate = '*' } = options;
191
+ const apiId = this.getApiId(contentType, options);
192
+ const fetchOne = async (loc) => {
193
+ const params = { ...filters };
194
+ if (pageSize != null) params['pagination[pageSize]'] = pageSize;
195
+ if (loc != null && loc !== '') params.locale = loc;
196
+ if (populate != null) params.populate = populate;
197
+ if (singleType) {
198
+ const response = await this.client.get(`/api/${apiId}`, { params });
199
+ const data = response.data?.data;
200
+ // console.log('data', data);
201
+ return data ? [data] : [];
202
+ }
203
+ let all = [];
204
+ let page = 1;
205
+ let hasMore = true;
206
+ while (hasMore) {
207
+ params['pagination[page]'] = page;
208
+ const response = await this.client.get(`/api/${apiId}`, { params });
209
+ const data = response.data?.data;
210
+ const list = Array.isArray(data) ? data : [];
211
+ all = all.concat(list);
212
+ const meta = response.data?.meta?.pagination;
213
+ if (!meta || meta.pageCount == null || meta.pageCount <= page) hasMore = false;
214
+ else hasMore = page < meta.pageCount;
215
+ page += 1;
216
+ }
217
+ return all;
218
+ };
219
+ try {
220
+ if (locale === 'all') {
221
+ if (localized === false) {
222
+ return await fetchOne(undefined);
223
+ }
224
+ const locales = await this.getLocales();
225
+ let merged = [];
226
+ for (const loc of locales) {
227
+ let list = [];
228
+ try {
229
+ list = await fetchOne(loc);
230
+ } catch (error) {
231
+ if (error.response?.status === 404) continue;
232
+ throw error;
233
+ }
234
+ const withLocale = list.map((e) => (
235
+ e
236
+ ? { ...e, locale: e.locale ?? e.localization?.locale ?? loc }
237
+ : e
238
+ ));
239
+ merged = merged.concat(withLocale);
240
+ }
241
+ const seen = new Set();
242
+ return merged.filter((entry) => {
243
+ if (!entry || typeof entry !== 'object') return false;
244
+ const key = `${entry.documentId ?? entry.id ?? ''}_${entry.locale ?? ''}`;
245
+ if (seen.has(key)) return false;
246
+ seen.add(key);
247
+ return true;
248
+ });
249
+ }
250
+ return await fetchOne(locale);
251
+ } catch (error) {
252
+ if (error.response?.status === 404) {
253
+ return [];
254
+ }
255
+ const e = new Error(formatStrapiError(error, `fetching ${contentType} entries`));
256
+ e.status = error.response?.status;
257
+ throw e;
258
+ }
259
+ }
260
+
261
+ async createEntry(contentType, data, options = {}) {
262
+ const { singleType = false, locale } = options;
263
+ const apiId = this.getApiId(contentType, options);
264
+ const params = locale != null && locale !== '' ? { locale } : {};
265
+ const payload = sanitizePayload(data);
266
+ if (singleType) {
267
+ try {
268
+ const { data } = await this.client.put(`/api/${apiId}`, { data: payload }, { params });
269
+ return data;
270
+ } catch (error) {
271
+ throw new Error(formatStrapiError(error, `creating ${contentType} entry`));
272
+ }
273
+ }
274
+ try {
275
+ const { data } = await this.client.post(`/api/${apiId}`, { data: payload }, { params });
276
+ return data;
277
+ } catch (error) {
278
+ throw new Error(formatStrapiError(error, `creating ${contentType} entry`));
279
+ }
280
+ }
281
+
282
+ async updateEntry(contentType, id, data, options = {}) {
283
+ const { singleType = false, locale } = options;
284
+ const apiId = this.getApiId(contentType, options);
285
+ const params = locale != null && locale !== '' ? { locale } : {};
286
+ const payload = sanitizePayload(data);
287
+ if (singleType) {
288
+ try {
289
+ const response = await this.client.put(`/api/${apiId}`, { data: payload }, { params });
290
+ return response.data;
291
+ } catch (error) {
292
+ throw new Error(formatStrapiError(error, `updating ${contentType} entry`));
293
+ }
294
+ }
295
+ try {
296
+ const response = await this.client.put(`/api/${apiId}/${id}`, { data: payload }, { params });
297
+ return response.data;
298
+ } catch (error) {
299
+ throw new Error(formatStrapiError(error, `updating ${contentType} entry ${id}`));
300
+ }
301
+ }
302
+
303
+ async deleteEntry(contentType, id, options = {}) {
304
+ const apiId = this.getApiId(contentType, options);
305
+ const params = options.locale != null && options.locale !== '' ? { locale: options.locale } : {};
306
+ try {
307
+ await this.client.delete(`/api/${apiId}/${id}`, { params });
308
+ } catch (error) {
309
+ throw new Error(formatStrapiError(error, `deleting ${contentType} entry ${id}`));
310
+ }
311
+ }
312
+
313
+ async upsertContent(contentType, objData, options = {}) {
314
+ const { singleType = false } = options;
315
+ const data = objData.data;
316
+
317
+ if (Array.isArray(data)) {
318
+ for (const item of data) {
319
+ await this.upsertContent(contentType, { data: item, file: objData.file }, options);
320
+ }
321
+ return;
322
+ }
323
+
324
+ if (singleType) {
325
+ await this.updateEntry(contentType, null, data, options);
326
+ return;
327
+ }
328
+
329
+ if (data.id) {
330
+ const existing = await this.getEntries(contentType, {
331
+ filters: { id: { $eq: data.id } }
332
+ }, options);
333
+
334
+ if (existing.length > 0) {
335
+ const entryId = existing[0].documentId ?? existing[0].id;
336
+ await this.updateEntry(contentType, entryId, data, options);
337
+ } else {
338
+ await this.createEntry(contentType, data, options);
339
+ }
340
+ } else {
341
+ await this.createEntry(contentType, data, options);
342
+ }
343
+ }
344
+ }
345
+
346
+ module.exports = { StrapiSyncClient };
347
+