@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.
- package/README.md +322 -0
- package/bin/cli.js +275 -0
- package/index.js +5 -0
- package/lib/async.js +22 -0
- package/lib/config.js +183 -0
- package/lib/constants.js +20 -0
- package/lib/format.js +46 -0
- package/lib/preview.js +373 -0
- package/lib/schema-writer.js +356 -0
- package/lib/snapshot-io.js +343 -0
- package/lib/snapshot-utils.js +392 -0
- package/lib/strapi-client.js +347 -0
- package/lib/sync-engine.js +379 -0
- package/package.json +34 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
function simplifyAttributeType(def) {
|
|
2
|
+
if (!def || typeof def !== 'object') return 'json';
|
|
3
|
+
const t = def.type;
|
|
4
|
+
const multi = def.multiple === true;
|
|
5
|
+
if (['string', 'text', 'richtext', 'uid'].includes(t)) return 'string';
|
|
6
|
+
if (['number', 'integer', 'float', 'decimal'].includes(t)) return 'number';
|
|
7
|
+
if (t === 'boolean') return 'boolean';
|
|
8
|
+
if (['date', 'datetime', 'time'].includes(t)) return 'date';
|
|
9
|
+
if (t === 'media') return multi ? 'media[]' : 'media';
|
|
10
|
+
if (t === 'component') {
|
|
11
|
+
const uid = def.component || '';
|
|
12
|
+
const repeatable = def.repeatable === true;
|
|
13
|
+
return uid ? `component:${uid}${repeatable ? ':repeatable' : ''}` : 'json';
|
|
14
|
+
}
|
|
15
|
+
if (t === 'relation') {
|
|
16
|
+
const relType = def.relation || 'oneToMany';
|
|
17
|
+
const target = compactRelationTarget(def.target || '');
|
|
18
|
+
return target ? `relation:${target}:${relType}` : 'json';
|
|
19
|
+
}
|
|
20
|
+
if (t === 'dynamiczone') {
|
|
21
|
+
return Array.isArray(def.components) ? [...def.components] : [];
|
|
22
|
+
}
|
|
23
|
+
return 'json';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseComponentType(simpleType) {
|
|
27
|
+
if (typeof simpleType !== 'string' || !simpleType.startsWith('component:')) return null;
|
|
28
|
+
const rest = simpleType.slice('component:'.length);
|
|
29
|
+
const repeatable = rest.endsWith(':repeatable');
|
|
30
|
+
const uid = repeatable ? rest.slice(0, -':repeatable'.length) : rest;
|
|
31
|
+
return uid ? { uid, repeatable } : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseDynamicZoneType(simpleType) {
|
|
35
|
+
if (Array.isArray(simpleType)) return { components: simpleType };
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseRelationType(simpleType) {
|
|
40
|
+
if (typeof simpleType !== 'string' || !simpleType.startsWith('relation:')) return null;
|
|
41
|
+
const rest = simpleType.slice('relation:'.length);
|
|
42
|
+
const lastColon = rest.lastIndexOf(':');
|
|
43
|
+
if (lastColon === -1) return null;
|
|
44
|
+
const target = rest.slice(0, lastColon);
|
|
45
|
+
const relType = rest.slice(lastColon + 1);
|
|
46
|
+
return target ? { target, relType } : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function compactRelationTarget(target) {
|
|
50
|
+
if (typeof target !== 'string' || !target) return target;
|
|
51
|
+
const match = target.match(/^api::([a-z0-9-]+)\.\1$/i);
|
|
52
|
+
return match ? match[1] : target;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function expandRelationTarget(target) {
|
|
56
|
+
if (typeof target !== 'string' || !target) return target;
|
|
57
|
+
if (target.includes('::') || target.includes('.')) return target;
|
|
58
|
+
return `api::${target}.${target}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeSimpleAttributeType(simpleType) {
|
|
62
|
+
if (Array.isArray(simpleType)) return [...simpleType];
|
|
63
|
+
if (typeof simpleType !== 'string') return simpleType;
|
|
64
|
+
const relation = parseRelationType(simpleType);
|
|
65
|
+
if (!relation) return simpleType;
|
|
66
|
+
return `relation:${compactRelationTarget(relation.target)}:${relation.relType}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isPlainObject(value) {
|
|
70
|
+
return value != null && typeof value === 'object' && !Array.isArray(value);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isLocaleKey(value) {
|
|
74
|
+
return typeof value === 'string' && /^[a-z]{2}(?:-[A-Z]{2})?$/.test(value);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isSingleTypeLocaleMap(entries) {
|
|
78
|
+
if (!isPlainObject(entries)) return false;
|
|
79
|
+
const pairs = Object.entries(entries);
|
|
80
|
+
if (pairs.length === 0) return false;
|
|
81
|
+
return pairs.every(([key, value]) => isLocaleKey(key) && isPlainObject(value));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function validateSimpleAttributeType(simpleType, fieldPath) {
|
|
85
|
+
const errors = [];
|
|
86
|
+
const scalarTypes = new Set(['string', 'number', 'boolean', 'date', 'media', 'media[]', 'json']);
|
|
87
|
+
const relationTypes = new Set([
|
|
88
|
+
'oneToOne',
|
|
89
|
+
'oneToMany',
|
|
90
|
+
'manyToOne',
|
|
91
|
+
'manyToMany',
|
|
92
|
+
'oneWay',
|
|
93
|
+
'manyWay',
|
|
94
|
+
'morphOne',
|
|
95
|
+
'morphMany'
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
if (Array.isArray(simpleType)) {
|
|
99
|
+
const invalidItems = simpleType.filter((item) => typeof item !== 'string' || item.trim() === '');
|
|
100
|
+
if (invalidItems.length > 0) {
|
|
101
|
+
errors.push(`${fieldPath} must be an array of non-empty component UIDs`);
|
|
102
|
+
}
|
|
103
|
+
return errors;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (typeof simpleType !== 'string') {
|
|
107
|
+
errors.push(`${fieldPath} must be a string or an array`);
|
|
108
|
+
return errors;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (scalarTypes.has(simpleType)) return errors;
|
|
112
|
+
|
|
113
|
+
if (simpleType.startsWith('component:')) {
|
|
114
|
+
const parsed = parseComponentType(simpleType);
|
|
115
|
+
if (!parsed || !parsed.uid || !parsed.uid.includes('.')) {
|
|
116
|
+
errors.push(`${fieldPath} has invalid component syntax: ${simpleType}`);
|
|
117
|
+
}
|
|
118
|
+
return errors;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (simpleType.startsWith('relation:')) {
|
|
122
|
+
const parsed = parseRelationType(simpleType);
|
|
123
|
+
if (!parsed || !parsed.target || !parsed.relType) {
|
|
124
|
+
errors.push(`${fieldPath} has invalid relation syntax: ${simpleType}`);
|
|
125
|
+
return errors;
|
|
126
|
+
}
|
|
127
|
+
if (!/^[a-z0-9-]+$/i.test(parsed.target)) {
|
|
128
|
+
errors.push(`${fieldPath} must use short relation syntax like relation:article:oneToMany`);
|
|
129
|
+
}
|
|
130
|
+
if (!relationTypes.has(parsed.relType)) {
|
|
131
|
+
errors.push(`${fieldPath} uses unsupported relation type "${parsed.relType}"`);
|
|
132
|
+
}
|
|
133
|
+
return errors;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
errors.push(`${fieldPath} uses unsupported attribute type "${simpleType}"`);
|
|
137
|
+
return errors;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function flattenRelationValue(val) {
|
|
141
|
+
if (val == null) return null;
|
|
142
|
+
if (typeof val !== 'object') return val;
|
|
143
|
+
if ('data' in val) {
|
|
144
|
+
const data = val.data;
|
|
145
|
+
if (Array.isArray(data)) return data.map((e) => e?.id).filter((v) => v != null);
|
|
146
|
+
if (data && typeof data === 'object') return data.id ?? null;
|
|
147
|
+
return data;
|
|
148
|
+
}
|
|
149
|
+
return val;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const DZ_SYSTEM_KEYS = new Set(['createdAt', 'updatedAt', 'publishedAt', 'locale', 'localizations', 'createdBy', 'updatedBy']);
|
|
153
|
+
|
|
154
|
+
function cleanDynamicZoneItem(item) {
|
|
155
|
+
if (!item || typeof item !== 'object') return item;
|
|
156
|
+
const result = {};
|
|
157
|
+
for (const [k, v] of Object.entries(item)) {
|
|
158
|
+
if (DZ_SYSTEM_KEYS.has(k)) continue;
|
|
159
|
+
if (v && typeof v === 'object' && 'data' in v) {
|
|
160
|
+
result[k] = flattenRelationValue(v);
|
|
161
|
+
} else {
|
|
162
|
+
result[k] = v;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function flattenStrapiEntry(entry) {
|
|
169
|
+
if (!entry || typeof entry !== 'object') return entry;
|
|
170
|
+
if (entry.attributes && typeof entry.attributes === 'object') {
|
|
171
|
+
return { id: entry.id, documentId: entry.documentId, ...entry.attributes };
|
|
172
|
+
}
|
|
173
|
+
return entry;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function entryToSnapshot(entry, schemaAttributes) {
|
|
177
|
+
const flat = flattenStrapiEntry(entry);
|
|
178
|
+
if (!flat || typeof flat !== 'object') return flat;
|
|
179
|
+
const isAttrsObj = !Array.isArray(schemaAttributes) && typeof schemaAttributes === 'object' && schemaAttributes !== null;
|
|
180
|
+
const keys = isAttrsObj
|
|
181
|
+
? Object.keys(schemaAttributes)
|
|
182
|
+
: (Array.isArray(schemaAttributes) ? schemaAttributes : []);
|
|
183
|
+
const keep = new Set(keys.length ? keys : []);
|
|
184
|
+
if (Object.prototype.hasOwnProperty.call(flat, 'id')) keep.add('id');
|
|
185
|
+
const result = {};
|
|
186
|
+
for (const k of keep) {
|
|
187
|
+
const typeStr = isAttrsObj ? schemaAttributes[k] : null;
|
|
188
|
+
if (!Object.prototype.hasOwnProperty.call(flat, k)) {
|
|
189
|
+
if (typeStr && typeof typeStr === 'string' && typeStr.startsWith('relation:')) {
|
|
190
|
+
const parsed = parseRelationType(typeStr);
|
|
191
|
+
result[k] = parsed && (parsed.relType === 'oneToMany' || parsed.relType === 'manyToMany') ? [] : null;
|
|
192
|
+
} else if (typeStr === 'media') {
|
|
193
|
+
result[k] = null;
|
|
194
|
+
} else if (typeStr === 'media[]') {
|
|
195
|
+
result[k] = [];
|
|
196
|
+
} else if (Array.isArray(typeStr)) {
|
|
197
|
+
result[k] = [];
|
|
198
|
+
}
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const val = flat[k];
|
|
202
|
+
if (typeStr && typeof typeStr === 'string' && typeStr.startsWith('relation:')) {
|
|
203
|
+
result[k] = flattenRelationValue(val);
|
|
204
|
+
} else if (Array.isArray(typeStr)) {
|
|
205
|
+
result[k] = Array.isArray(val) ? val.map(cleanDynamicZoneItem) : (val ?? []);
|
|
206
|
+
} else {
|
|
207
|
+
result[k] = val;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function valuesEqual(a, b) {
|
|
214
|
+
if (a === b) return true;
|
|
215
|
+
if (typeof a === 'object' && typeof b === 'object') return JSON.stringify(a) === JSON.stringify(b);
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function normalizeValueByType(value, typeStr) {
|
|
220
|
+
if (typeStr === 'media') return value ?? null;
|
|
221
|
+
if (typeStr === 'media[]') return Array.isArray(value) ? value : (value ?? []);
|
|
222
|
+
if (typeStr && typeof typeStr === 'string' && typeStr.startsWith('relation:')) {
|
|
223
|
+
if (value === undefined) {
|
|
224
|
+
const parsed = parseRelationType(typeStr);
|
|
225
|
+
return parsed && (parsed.relType === 'oneToMany' || parsed.relType === 'manyToMany') ? [] : null;
|
|
226
|
+
}
|
|
227
|
+
return flattenRelationValue(value);
|
|
228
|
+
}
|
|
229
|
+
if (Array.isArray(typeStr)) {
|
|
230
|
+
if (value === undefined) return [];
|
|
231
|
+
return Array.isArray(value) ? value.map(cleanDynamicZoneItem) : (value ?? []);
|
|
232
|
+
}
|
|
233
|
+
return value;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function entryDataDiffers(snapshotData, existingEntry, schemaKeysOrAttrs) {
|
|
237
|
+
if (!existingEntry || !snapshotData) return true;
|
|
238
|
+
const existing = flattenStrapiEntry(existingEntry);
|
|
239
|
+
const isAttrsObj = !Array.isArray(schemaKeysOrAttrs) && typeof schemaKeysOrAttrs === 'object' && schemaKeysOrAttrs !== null;
|
|
240
|
+
const keys = isAttrsObj
|
|
241
|
+
? Object.keys(schemaKeysOrAttrs).filter((k) => k !== 'id' && Object.prototype.hasOwnProperty.call(snapshotData, k))
|
|
242
|
+
: (Array.isArray(schemaKeysOrAttrs)
|
|
243
|
+
? schemaKeysOrAttrs.filter((k) => Object.prototype.hasOwnProperty.call(snapshotData, k))
|
|
244
|
+
: Object.keys(schemaKeysOrAttrs || snapshotData).filter((k) => k !== 'id'));
|
|
245
|
+
for (const k of keys) {
|
|
246
|
+
let snapshotVal = snapshotData[k];
|
|
247
|
+
let existingVal = existing[k];
|
|
248
|
+
const typeStr = isAttrsObj ? schemaKeysOrAttrs[k] : null;
|
|
249
|
+
snapshotVal = normalizeValueByType(snapshotVal, typeStr);
|
|
250
|
+
existingVal = normalizeValueByType(existingVal, typeStr);
|
|
251
|
+
if (!valuesEqual(snapshotVal, existingVal)) return true;
|
|
252
|
+
}
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function entryDataDiff(snapshotData, existingEntry, schemaKeysOrAttrs) {
|
|
257
|
+
if (!existingEntry || !snapshotData) return snapshotData ? { ...snapshotData } : {};
|
|
258
|
+
const existing = flattenStrapiEntry(existingEntry);
|
|
259
|
+
const isAttrsObj = !Array.isArray(schemaKeysOrAttrs) && typeof schemaKeysOrAttrs === 'object' && schemaKeysOrAttrs !== null;
|
|
260
|
+
const keys = isAttrsObj
|
|
261
|
+
? Object.keys(schemaKeysOrAttrs).filter((k) => k !== 'id' && Object.prototype.hasOwnProperty.call(snapshotData, k))
|
|
262
|
+
: (Array.isArray(schemaKeysOrAttrs)
|
|
263
|
+
? schemaKeysOrAttrs.filter((k) => Object.prototype.hasOwnProperty.call(snapshotData, k))
|
|
264
|
+
: Object.keys(schemaKeysOrAttrs || snapshotData).filter((k) => k !== 'id'));
|
|
265
|
+
const diff = {};
|
|
266
|
+
for (const k of keys) {
|
|
267
|
+
let snapshotVal = snapshotData[k];
|
|
268
|
+
let existingVal = existing[k];
|
|
269
|
+
const typeStr = isAttrsObj ? schemaKeysOrAttrs[k] : null;
|
|
270
|
+
snapshotVal = normalizeValueByType(snapshotVal, typeStr);
|
|
271
|
+
existingVal = normalizeValueByType(existingVal, typeStr);
|
|
272
|
+
if (!valuesEqual(snapshotVal, existingVal)) diff[k] = snapshotData[k];
|
|
273
|
+
}
|
|
274
|
+
return diff;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function buildSampleFromSimpleAttributes(attributes) {
|
|
278
|
+
if (!attributes || typeof attributes !== 'object') return {};
|
|
279
|
+
const obj = {};
|
|
280
|
+
for (const [key, typeStr] of Object.entries(attributes)) {
|
|
281
|
+
const t = typeof typeStr === 'string' ? typeStr : 'json';
|
|
282
|
+
if (t === 'string') obj[key] = '';
|
|
283
|
+
else if (t === 'number') obj[key] = 0;
|
|
284
|
+
else if (t === 'boolean') obj[key] = false;
|
|
285
|
+
else if (t === 'date') obj[key] = null;
|
|
286
|
+
else if (t === 'media') obj[key] = null;
|
|
287
|
+
else if (t === 'media[]') obj[key] = [];
|
|
288
|
+
else if (t.startsWith('relation:')) {
|
|
289
|
+
const parsed = parseRelationType(t);
|
|
290
|
+
obj[key] = parsed && (parsed.relType === 'oneToMany' || parsed.relType === 'manyToMany') ? [] : null;
|
|
291
|
+
} else if (Array.isArray(t)) {
|
|
292
|
+
obj[key] = [];
|
|
293
|
+
} else {
|
|
294
|
+
obj[key] = {};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return obj;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function getContentTypeId(type) {
|
|
301
|
+
if (!type) return null;
|
|
302
|
+
const uid = type.uid || type.apiID || type.name;
|
|
303
|
+
if (typeof uid === 'string' && uid.includes('.')) return uid.split('.')[1];
|
|
304
|
+
return type.apiID || type.name;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function getEntryLocale(entry) {
|
|
308
|
+
return entry?.locale ?? entry?.localization?.locale ?? '';
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function getSchemaKeysFromSnapshot(snapshotContentTypes, contentType) {
|
|
312
|
+
const attrs = snapshotContentTypes[contentType]?.attributes;
|
|
313
|
+
return attrs ? Object.keys(attrs) : [];
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function getSchemaAttrsFromSnapshot(snapshotContentTypes, contentType) {
|
|
317
|
+
const attributes = snapshotContentTypes[contentType]?.attributes ?? {};
|
|
318
|
+
return Object.fromEntries(
|
|
319
|
+
Object.entries(attributes).map(([key, value]) => [key, normalizeSimpleAttributeType(value)])
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function getComparableSchemaAttrs(attributes) {
|
|
324
|
+
if (!attributes || typeof attributes !== 'object' || Array.isArray(attributes)) return {};
|
|
325
|
+
return Object.fromEntries(
|
|
326
|
+
Object.entries(attributes).filter(([, typeStr]) => typeStr !== 'media' && typeStr !== 'media[]')
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function stripIgnoredFields(data, schemaAttributes) {
|
|
331
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) return data;
|
|
332
|
+
const comparableAttrs = getComparableSchemaAttrs(schemaAttributes);
|
|
333
|
+
if (Object.keys(comparableAttrs).length === Object.keys(schemaAttributes || {}).length) {
|
|
334
|
+
return { ...data };
|
|
335
|
+
}
|
|
336
|
+
return Object.fromEntries(
|
|
337
|
+
Object.entries(data).filter(([key]) => key === 'id' || key === 'documentId' || key in comparableAttrs)
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function isLocalizedSnapshotBlock(block) {
|
|
342
|
+
if (!block || typeof block !== 'object') return false;
|
|
343
|
+
const entries = block.entries;
|
|
344
|
+
if (block.singleType === true) return isSingleTypeLocaleMap(entries);
|
|
345
|
+
return (
|
|
346
|
+
entries != null &&
|
|
347
|
+
typeof entries === 'object' &&
|
|
348
|
+
!Array.isArray(entries) &&
|
|
349
|
+
Object.keys(entries).length > 0
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function hasSnapshotEntries(block) {
|
|
354
|
+
if (!block || typeof block !== 'object') return false;
|
|
355
|
+
const entries = block.entries;
|
|
356
|
+
if (block.singleType === true) {
|
|
357
|
+
if (!isPlainObject(entries)) return false;
|
|
358
|
+
if (isSingleTypeLocaleMap(entries)) return Object.keys(entries).length > 0;
|
|
359
|
+
return Object.keys(entries).length > 0;
|
|
360
|
+
}
|
|
361
|
+
if (Array.isArray(entries)) return entries.length > 0;
|
|
362
|
+
if (!entries || typeof entries !== 'object') return false;
|
|
363
|
+
return Object.values(entries).some((list) => Array.isArray(list) && list.length > 0);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
module.exports = {
|
|
367
|
+
simplifyAttributeType,
|
|
368
|
+
parseComponentType,
|
|
369
|
+
parseRelationType,
|
|
370
|
+
parseDynamicZoneType,
|
|
371
|
+
validateSimpleAttributeType,
|
|
372
|
+
flattenRelationValue,
|
|
373
|
+
cleanDynamicZoneItem,
|
|
374
|
+
flattenStrapiEntry,
|
|
375
|
+
entryToSnapshot,
|
|
376
|
+
entryDataDiffers,
|
|
377
|
+
entryDataDiff,
|
|
378
|
+
buildSampleFromSimpleAttributes,
|
|
379
|
+
getContentTypeId,
|
|
380
|
+
getEntryLocale,
|
|
381
|
+
getSchemaKeysFromSnapshot,
|
|
382
|
+
getSchemaAttrsFromSnapshot,
|
|
383
|
+
normalizeSimpleAttributeType,
|
|
384
|
+
compactRelationTarget,
|
|
385
|
+
expandRelationTarget,
|
|
386
|
+
isPlainObject,
|
|
387
|
+
isSingleTypeLocaleMap,
|
|
388
|
+
getComparableSchemaAttrs,
|
|
389
|
+
stripIgnoredFields,
|
|
390
|
+
isLocalizedSnapshotBlock,
|
|
391
|
+
hasSnapshotEntries
|
|
392
|
+
};
|