@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
package/lib/config.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const dotenv = require('dotenv');
|
|
4
|
+
const { CONFIG_FILES, DEFAULT_OUTPUT } = require('./constants');
|
|
5
|
+
|
|
6
|
+
function loadConfigFile(configPath) {
|
|
7
|
+
if (!fs.existsSync(configPath)) return null;
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
10
|
+
} catch (error) {
|
|
11
|
+
console.warn(`ā ļø Failed to load config file ${configPath}: ${error.message}`);
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function findConfigFile(projectPath) {
|
|
17
|
+
for (const name of CONFIG_FILES) {
|
|
18
|
+
const configPath = path.join(projectPath, name);
|
|
19
|
+
if (fs.existsSync(configPath)) return configPath;
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function loadConfig(projectPath) {
|
|
25
|
+
const configPath = findConfigFile(projectPath);
|
|
26
|
+
if (!configPath) return null;
|
|
27
|
+
console.log(`š Loading config from: ${configPath}`);
|
|
28
|
+
return loadConfigFile(configPath);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function loadEnv(projectPath) {
|
|
32
|
+
dotenv.config({ path: path.join(projectPath, '.env') });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const FLAGS = {
|
|
36
|
+
project: ['--project', '-p'],
|
|
37
|
+
strapiUrl: ['--strapi-url', '-u'],
|
|
38
|
+
apiToken: ['--api-token'],
|
|
39
|
+
export: ['--export'],
|
|
40
|
+
output: ['--output'],
|
|
41
|
+
strapiProjectPath: ['--strapi-project'],
|
|
42
|
+
strapiSSHHost: ['--strapi-ssh-host'],
|
|
43
|
+
strapiSSHUser: ['--strapi-ssh-user'],
|
|
44
|
+
strapiSSHPort: ['--strapi-ssh-port'],
|
|
45
|
+
strapiSSHPassword: ['--strapi-ssh-password'],
|
|
46
|
+
strapiSSHPrivateKeyPath: ['--strapi-ssh-key'],
|
|
47
|
+
config: ['--config'],
|
|
48
|
+
help: ['--help', '-h'],
|
|
49
|
+
version: ['--version', '-v']
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const ENV_BY_OPTION = {
|
|
53
|
+
strapiUrl: 'STRAPI_URL',
|
|
54
|
+
apiToken: 'STRAPI_API_TOKEN',
|
|
55
|
+
strapiProjectPath: 'STRAPI_PROJECT_PATH',
|
|
56
|
+
strapiSSHHost: 'STRAPI_SSH_HOST',
|
|
57
|
+
strapiSSHUser: 'STRAPI_SSH_USER',
|
|
58
|
+
strapiSSHPort: 'STRAPI_SSH_PORT',
|
|
59
|
+
strapiSSHPassword: 'STRAPI_SSH_PASSWORD',
|
|
60
|
+
strapiSSHPrivateKeyPath: 'STRAPI_SSH_PRIVATE_KEY_PATH'
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
function matchesFlag(flagSet, value) {
|
|
64
|
+
return flagSet.includes(value);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseNumber(value, fallback) {
|
|
68
|
+
const parsed = Number(value);
|
|
69
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isMissing(value) {
|
|
73
|
+
return value == null || value === '';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function applyEnvFallbacks(options) {
|
|
77
|
+
for (const [optionKey, envKey] of Object.entries(ENV_BY_OPTION)) {
|
|
78
|
+
if (!isMissing(options[optionKey])) continue;
|
|
79
|
+
const envValue = process.env[envKey];
|
|
80
|
+
if (isMissing(envValue)) continue;
|
|
81
|
+
options[optionKey] = optionKey === 'strapiSSHPort'
|
|
82
|
+
? parseNumber(envValue, undefined)
|
|
83
|
+
: envValue;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function requireFlagValue(args, index, flag) {
|
|
88
|
+
const value = args[index + 1];
|
|
89
|
+
if (value == null || value.startsWith('-')) {
|
|
90
|
+
throw new Error(`Unknown or incomplete CLI arguments: expected value after ${flag}`);
|
|
91
|
+
}
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function parseCliArgs(args) {
|
|
96
|
+
const cliOptions = {};
|
|
97
|
+
let customConfigPath = null;
|
|
98
|
+
|
|
99
|
+
for (let i = 0; i < args.length; i++) {
|
|
100
|
+
const arg = args[i];
|
|
101
|
+
if (matchesFlag(FLAGS.help, arg)) {
|
|
102
|
+
cliOptions.help = true;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (matchesFlag(FLAGS.version, arg)) {
|
|
106
|
+
cliOptions.version = true;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (matchesFlag(FLAGS.project, arg)) cliOptions.projectPath = requireFlagValue(args, i++, arg);
|
|
110
|
+
else if (matchesFlag(FLAGS.strapiUrl, arg)) cliOptions.strapiUrl = requireFlagValue(args, i++, arg);
|
|
111
|
+
else if (matchesFlag(FLAGS.apiToken, arg)) cliOptions.apiToken = requireFlagValue(args, i++, arg);
|
|
112
|
+
else if (matchesFlag(FLAGS.export, arg)) cliOptions.mode = 'export';
|
|
113
|
+
else if (matchesFlag(FLAGS.output, arg)) cliOptions.output = requireFlagValue(args, i++, arg);
|
|
114
|
+
else if (matchesFlag(FLAGS.strapiProjectPath, arg)) cliOptions.strapiProjectPath = requireFlagValue(args, i++, arg);
|
|
115
|
+
else if (matchesFlag(FLAGS.strapiSSHHost, arg)) cliOptions.strapiSSHHost = requireFlagValue(args, i++, arg);
|
|
116
|
+
else if (matchesFlag(FLAGS.strapiSSHUser, arg)) cliOptions.strapiSSHUser = requireFlagValue(args, i++, arg);
|
|
117
|
+
else if (matchesFlag(FLAGS.strapiSSHPort, arg)) cliOptions.strapiSSHPort = parseNumber(requireFlagValue(args, i++, arg), undefined);
|
|
118
|
+
else if (matchesFlag(FLAGS.strapiSSHPassword, arg)) cliOptions.strapiSSHPassword = requireFlagValue(args, i++, arg);
|
|
119
|
+
else if (matchesFlag(FLAGS.strapiSSHPrivateKeyPath, arg)) cliOptions.strapiSSHPrivateKeyPath = requireFlagValue(args, i++, arg);
|
|
120
|
+
else if (matchesFlag(FLAGS.config, arg)) customConfigPath = requireFlagValue(args, i++, arg);
|
|
121
|
+
else if (arg.startsWith('-')) throw new Error(`Unknown CLI argument: ${arg}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { cliOptions, customConfigPath };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function finalizeMode(options) {
|
|
128
|
+
const mode = options.mode || 'apply';
|
|
129
|
+
return {
|
|
130
|
+
...options,
|
|
131
|
+
mode,
|
|
132
|
+
export: mode === 'export'
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function parseArgs() {
|
|
137
|
+
const args = process.argv.slice(2);
|
|
138
|
+
const cwd = process.cwd();
|
|
139
|
+
const { cliOptions, customConfigPath } = parseCliArgs(args);
|
|
140
|
+
|
|
141
|
+
if (cliOptions.help) return { help: true };
|
|
142
|
+
if (cliOptions.version) return { version: true };
|
|
143
|
+
|
|
144
|
+
const projectPath = path.resolve(cliOptions.projectPath || cwd);
|
|
145
|
+
loadEnv(projectPath);
|
|
146
|
+
const options = {
|
|
147
|
+
projectPath,
|
|
148
|
+
strapiUrl: null,
|
|
149
|
+
apiToken: null,
|
|
150
|
+
output: DEFAULT_OUTPUT,
|
|
151
|
+
updateSchema: true,
|
|
152
|
+
strapiProjectPath: null,
|
|
153
|
+
reloadPollInterval: 1500,
|
|
154
|
+
reloadTimeoutMs: 60000,
|
|
155
|
+
strapiSSHHost: null,
|
|
156
|
+
strapiSSHUser: null,
|
|
157
|
+
strapiSSHPort: null,
|
|
158
|
+
strapiSSHPassword: null,
|
|
159
|
+
strapiSSHPrivateKeyPath: null,
|
|
160
|
+
mode: 'apply'
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const discoveredConfig = loadConfig(projectPath);
|
|
164
|
+
if (discoveredConfig) Object.assign(options, discoveredConfig);
|
|
165
|
+
|
|
166
|
+
if (customConfigPath) {
|
|
167
|
+
const customConfig = loadConfigFile(path.resolve(customConfigPath));
|
|
168
|
+
if (customConfig) Object.assign(options, customConfig);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
Object.assign(options, cliOptions);
|
|
172
|
+
applyEnvFallbacks(options);
|
|
173
|
+
options.projectPath = projectPath;
|
|
174
|
+
|
|
175
|
+
return finalizeMode(options);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
module.exports = {
|
|
179
|
+
loadConfig,
|
|
180
|
+
loadConfigFile,
|
|
181
|
+
findConfigFile,
|
|
182
|
+
parseArgs
|
|
183
|
+
};
|
package/lib/constants.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const SNAPSHOT_VERSION = 2;
|
|
2
|
+
|
|
3
|
+
const COLORS = {
|
|
4
|
+
RED: '\x1b[31m',
|
|
5
|
+
GREEN: '\x1b[32m',
|
|
6
|
+
YELLOW: '\x1b[33m',
|
|
7
|
+
RESET: '\x1b[0m'
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const CONFIG_FILES = ['.strapi-sync.json', 'strapi-sync.config.json', 'strapi-sync.json'];
|
|
11
|
+
const DEFAULT_OUTPUT = 'strapi-snapshot.json';
|
|
12
|
+
const MAX_LOG_STRING = 120;
|
|
13
|
+
|
|
14
|
+
module.exports = {
|
|
15
|
+
SNAPSHOT_VERSION,
|
|
16
|
+
COLORS,
|
|
17
|
+
CONFIG_FILES,
|
|
18
|
+
DEFAULT_OUTPUT,
|
|
19
|
+
MAX_LOG_STRING
|
|
20
|
+
};
|
package/lib/format.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const { COLORS, MAX_LOG_STRING } = require('./constants');
|
|
2
|
+
|
|
3
|
+
function isMediaField(key) {
|
|
4
|
+
const name = (key || '').toLowerCase();
|
|
5
|
+
return name.endsWith('media') || name.endsWith('files');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function contentTypeDisplayName(s) {
|
|
9
|
+
if (!s || typeof s !== 'string') return s;
|
|
10
|
+
return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function formatContentForLog(data, indent = ' ') {
|
|
14
|
+
if (data === null || data === undefined) return '';
|
|
15
|
+
if (typeof data !== 'object') return String(data);
|
|
16
|
+
const lines = [];
|
|
17
|
+
for (const [key, value] of Object.entries(data)) {
|
|
18
|
+
if (isMediaField(key)) continue;
|
|
19
|
+
if (Array.isArray(value)) {
|
|
20
|
+
lines.push(`${indent}${key}: [${value.length} item(s)]`);
|
|
21
|
+
value.slice(0, 3).forEach((item, i) => {
|
|
22
|
+
if (item && typeof item === 'object') {
|
|
23
|
+
const str = JSON.stringify(item);
|
|
24
|
+
lines.push(`${indent} [${i}]: ${str.slice(0, MAX_LOG_STRING)}${str.length > MAX_LOG_STRING ? 'ā¦' : ''}`);
|
|
25
|
+
} else {
|
|
26
|
+
lines.push(`${indent} [${i}]: ${String(item).slice(0, MAX_LOG_STRING)}`);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
if (value.length > 3) lines.push(`${indent} ... and ${value.length - 3} more`);
|
|
30
|
+
} else if (value !== null && typeof value === 'object') {
|
|
31
|
+
lines.push(`${indent}${key}:`);
|
|
32
|
+
lines.push(formatContentForLog(value, indent + ' '));
|
|
33
|
+
} else {
|
|
34
|
+
const str = String(value);
|
|
35
|
+
lines.push(`${indent}${key}: ${str.length > MAX_LOG_STRING ? str.slice(0, MAX_LOG_STRING) + 'ā¦' : str}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return lines.join('\n');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = {
|
|
42
|
+
COLORS,
|
|
43
|
+
isMediaField,
|
|
44
|
+
contentTypeDisplayName,
|
|
45
|
+
formatContentForLog
|
|
46
|
+
};
|
package/lib/preview.js
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
const { COLORS } = require('./format');
|
|
2
|
+
const {
|
|
3
|
+
simplifyAttributeType,
|
|
4
|
+
entryDataDiffers,
|
|
5
|
+
entryDataDiff,
|
|
6
|
+
getEntryLocale,
|
|
7
|
+
getSchemaAttrsFromSnapshot,
|
|
8
|
+
getComparableSchemaAttrs,
|
|
9
|
+
isLocalizedSnapshotBlock,
|
|
10
|
+
hasSnapshotEntries
|
|
11
|
+
} = require('./snapshot-utils');
|
|
12
|
+
|
|
13
|
+
function collectItemsToShow(typeObjects) {
|
|
14
|
+
const forContent = typeObjects.filter((o) => o.source !== 'schema-only');
|
|
15
|
+
const items = [];
|
|
16
|
+
for (const obj of forContent) {
|
|
17
|
+
const loc = obj.locale;
|
|
18
|
+
if (Array.isArray(obj.data)) {
|
|
19
|
+
for (const item of obj.data) {
|
|
20
|
+
if (item && typeof item === 'object') items.push({ data: item, locale: loc });
|
|
21
|
+
}
|
|
22
|
+
} else if (obj.data && typeof obj.data === 'object') {
|
|
23
|
+
items.push({ data: obj.data, locale: loc });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return items;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function findExistingEntry(item, existingEntries, singleType, entryLoc) {
|
|
30
|
+
const { data, locale } = item;
|
|
31
|
+
if (!data) return [];
|
|
32
|
+
let existing = existingEntries.filter((e) => {
|
|
33
|
+
if (data.id == null || e.id == null) return false;
|
|
34
|
+
const matchId = String(data.id) === String(e.id);
|
|
35
|
+
if (locale != null && locale !== '') return matchId && entryLoc(e) === locale;
|
|
36
|
+
return matchId;
|
|
37
|
+
});
|
|
38
|
+
if (singleType && existing.length === 0 && existingEntries.length > 0 && (!locale || locale === '')) {
|
|
39
|
+
existing = existingEntries.length > 0 ? [existingEntries[0]] : existing;
|
|
40
|
+
}
|
|
41
|
+
return existing;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function buildToAddToUpdateOnlyInStrapi(contentType, typeObjects, existingEntries, snapshotContentTypes, singleType) {
|
|
45
|
+
const itemsToShow = collectItemsToShow(typeObjects);
|
|
46
|
+
const snapshotAttrs = getSchemaAttrsFromSnapshot(snapshotContentTypes, contentType);
|
|
47
|
+
const schemaKeysForCompare =
|
|
48
|
+
Object.keys(snapshotAttrs).length > 0
|
|
49
|
+
? getComparableSchemaAttrs(snapshotAttrs)
|
|
50
|
+
: Object.keys(typeObjects[0]?.data || {}).filter((k) => k !== 'id');
|
|
51
|
+
|
|
52
|
+
const toAdd = [];
|
|
53
|
+
const toUpdate = [];
|
|
54
|
+
const matchedExistingIds = new Set();
|
|
55
|
+
|
|
56
|
+
for (const item of itemsToShow) {
|
|
57
|
+
const { data } = item;
|
|
58
|
+
if (!data) continue;
|
|
59
|
+
const existing = findExistingEntry(item, existingEntries, singleType, getEntryLocale);
|
|
60
|
+
const isCreate = existing.length === 0 && !singleType;
|
|
61
|
+
const needsUpdate = existing.length > 0 && entryDataDiffers(data, existing[0], schemaKeysForCompare);
|
|
62
|
+
const singleTypeApply = singleType && existing.length === 0;
|
|
63
|
+
|
|
64
|
+
if (existing.length > 0 && existing[0].id != null) {
|
|
65
|
+
matchedExistingIds.add(`${existing[0].id}_${getEntryLocale(existing[0])}`);
|
|
66
|
+
}
|
|
67
|
+
if (isCreate) toAdd.push({ data, itemLocale: item.locale });
|
|
68
|
+
else if (needsUpdate) toUpdate.push({ data, itemLocale: item.locale, existing: existing[0] });
|
|
69
|
+
else if (singleTypeApply) toUpdate.push({ data, itemLocale: item.locale, existing: {} });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const onlyInStrapi = !singleType
|
|
73
|
+
? existingEntries.filter((e) => e.id == null || !matchedExistingIds.has(`${e.id}_${getEntryLocale(e)}`))
|
|
74
|
+
: [];
|
|
75
|
+
|
|
76
|
+
return { toAdd, toUpdate, onlyInStrapi, schemaKeysForCompare };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function attrEqual(a, b) {
|
|
80
|
+
if (Array.isArray(a) && Array.isArray(b)) return JSON.stringify(a) === JSON.stringify(b);
|
|
81
|
+
if (Array.isArray(a) || Array.isArray(b)) return false;
|
|
82
|
+
return a === b;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function attrDisplay(v) {
|
|
86
|
+
if (Array.isArray(v)) return `[${v.join(', ')}]`;
|
|
87
|
+
return v;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function schemaDiffers(snapshotAttrs, strapiSimpleAttrs) {
|
|
91
|
+
const snapKeys = Object.keys(snapshotAttrs);
|
|
92
|
+
const strapiKeys = Object.keys(strapiSimpleAttrs);
|
|
93
|
+
return (
|
|
94
|
+
snapKeys.length !== strapiKeys.length ||
|
|
95
|
+
snapKeys.some((k) => !attrEqual(strapiSimpleAttrs[k], snapshotAttrs[k])) ||
|
|
96
|
+
strapiKeys.some((k) => !attrEqual(snapshotAttrs[k], strapiSimpleAttrs[k]))
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function getStrapiSimpleAttrs(schema) {
|
|
101
|
+
const raw = schema?.data?.contentType?.attributes ?? schema?.data?.schema?.attributes ?? {};
|
|
102
|
+
return Object.fromEntries(Object.entries(raw).map(([k, v]) => [k, simplifyAttributeType(v)]));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function getStrapiLocalization(schema) {
|
|
106
|
+
const ct = schema?.data?.contentType ?? schema?.data?.schema ?? schema?.data;
|
|
107
|
+
return !!(ct?.pluginOptions?.i18n?.localized);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function getStrapiSingleType(schema) {
|
|
111
|
+
const ct = schema?.data?.contentType ?? schema?.data?.schema ?? schema?.data;
|
|
112
|
+
return ct?.kind === 'singleType';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function computeChangeMaps(groupedObjects, snapshotContentTypes, existingSchemas, existingEntriesByType, getContentTypeId) {
|
|
116
|
+
const schemaChangesByType = new Map();
|
|
117
|
+
const contentChangesByType = new Map();
|
|
118
|
+
const localizationChangesByType = new Map();
|
|
119
|
+
const singleTypeChangesByType = new Map();
|
|
120
|
+
let previewCount = 0;
|
|
121
|
+
|
|
122
|
+
for (const [contentType, typeObjects] of Object.entries(groupedObjects)) {
|
|
123
|
+
const singleType = typeObjects.some((o) => o.singleType);
|
|
124
|
+
const existingEntries = existingEntriesByType[contentType] || [];
|
|
125
|
+
const itemsToShow = collectItemsToShow(typeObjects);
|
|
126
|
+
const snapshotAttrsForCompare = getSchemaAttrsFromSnapshot(snapshotContentTypes, contentType);
|
|
127
|
+
const schemaKeysForCompare =
|
|
128
|
+
Object.keys(snapshotAttrsForCompare).length > 0
|
|
129
|
+
? getComparableSchemaAttrs(snapshotAttrsForCompare)
|
|
130
|
+
: Object.keys(typeObjects[0]?.data || {}).filter((k) => k !== 'id');
|
|
131
|
+
|
|
132
|
+
let needSyncCount = 0;
|
|
133
|
+
let onlyInStrapiCount = 0;
|
|
134
|
+
if (!singleType && existingEntries.length > 0 && itemsToShow.length > 0) {
|
|
135
|
+
const matchedKeys = new Set(
|
|
136
|
+
itemsToShow.filter((i) => i.data?.id != null).map((i) => `${String(i.data.id)}_${i.locale || ''}`)
|
|
137
|
+
);
|
|
138
|
+
onlyInStrapiCount = existingEntries.filter(
|
|
139
|
+
(e) => e.id == null || !matchedKeys.has(`${String(e.id)}_${getEntryLocale(e)}`)
|
|
140
|
+
).length;
|
|
141
|
+
}
|
|
142
|
+
for (const item of itemsToShow) {
|
|
143
|
+
const existing = findExistingEntry(item, existingEntries, singleType, getEntryLocale);
|
|
144
|
+
if (existing.length === 0) needSyncCount++;
|
|
145
|
+
else if (entryDataDiffers(item.data, existing[0], schemaKeysForCompare)) needSyncCount++;
|
|
146
|
+
}
|
|
147
|
+
const hasContentToShow = needSyncCount > 0 || onlyInStrapiCount > 0;
|
|
148
|
+
contentChangesByType.set(contentType, hasContentToShow);
|
|
149
|
+
|
|
150
|
+
const schema = existingSchemas[contentType];
|
|
151
|
+
const strapiSimpleAttrs = getStrapiSimpleAttrs(schema);
|
|
152
|
+
const snapshotAttrs = snapshotContentTypes[contentType]?.attributes ?? {};
|
|
153
|
+
const strapiLocalized = getStrapiLocalization(schema);
|
|
154
|
+
const snapshotBlock = snapshotContentTypes[contentType];
|
|
155
|
+
const snapshotLocalized = isLocalizedSnapshotBlock(snapshotBlock);
|
|
156
|
+
const strapiSingleType = getStrapiSingleType(schema);
|
|
157
|
+
const snapshotSingleType = !!snapshotBlock?.singleType;
|
|
158
|
+
const localizationDiff = schema != null && hasSnapshotEntries(snapshotBlock) && strapiLocalized !== snapshotLocalized;
|
|
159
|
+
if (localizationDiff) localizationChangesByType.set(contentType, { from: strapiLocalized, to: snapshotLocalized });
|
|
160
|
+
const singleTypeDiff = schema != null && strapiSingleType !== snapshotSingleType;
|
|
161
|
+
if (singleTypeDiff) singleTypeChangesByType.set(contentType, { from: strapiSingleType, to: snapshotSingleType });
|
|
162
|
+
const schemaDiff = schemaDiffers(snapshotAttrs, strapiSimpleAttrs) || localizationDiff || singleTypeDiff;
|
|
163
|
+
schemaChangesByType.set(contentType, schemaDiff);
|
|
164
|
+
|
|
165
|
+
if (hasContentToShow || schemaDiff) previewCount++;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { schemaChangesByType, contentChangesByType, localizationChangesByType, singleTypeChangesByType, previewCount };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function printSchemaDiff(contentType, existingSchemas, snapshotContentTypes, contentTypeDisplayName, localizationChange, singleTypeChange) {
|
|
172
|
+
const schema = existingSchemas[contentType];
|
|
173
|
+
const strapiSimpleAttrs = getStrapiSimpleAttrs(schema);
|
|
174
|
+
const snapshotAttrs = snapshotContentTypes[contentType]?.attributes ?? {};
|
|
175
|
+
const strapiHasType = schema != null && Object.keys(strapiSimpleAttrs).length > 0;
|
|
176
|
+
|
|
177
|
+
if (!strapiHasType) {
|
|
178
|
+
const attrLines = Object.entries(snapshotAttrs).map(
|
|
179
|
+
([k, v], i, arr) => ` ${k}: ${attrDisplay(v)}${i < arr.length - 1 ? ',' : ''}`
|
|
180
|
+
);
|
|
181
|
+
console.log(`${COLORS.GREEN}${attrLines.join('\n')}${COLORS.RESET}`);
|
|
182
|
+
} else {
|
|
183
|
+
const added = Object.keys(snapshotAttrs).filter((k) => !(k in strapiSimpleAttrs));
|
|
184
|
+
const removed = Object.keys(strapiSimpleAttrs).filter((k) => !(k in snapshotAttrs));
|
|
185
|
+
const typeChanged = Object.keys(snapshotAttrs).filter(
|
|
186
|
+
(k) => strapiSimpleAttrs[k] !== undefined && !attrEqual(strapiSimpleAttrs[k], snapshotAttrs[k])
|
|
187
|
+
);
|
|
188
|
+
const parts = [
|
|
189
|
+
...(singleTypeChange
|
|
190
|
+
? [{
|
|
191
|
+
line: ` ~ kind: ${singleTypeChange.from ? 'singleType' : 'collectionType'} ā ${singleTypeChange.to ? 'singleType' : 'collectionType'}`,
|
|
192
|
+
color: COLORS.YELLOW
|
|
193
|
+
}]
|
|
194
|
+
: []),
|
|
195
|
+
...(localizationChange
|
|
196
|
+
? [{
|
|
197
|
+
line: ` ~ localization: ${localizationChange.from ? 'enabled' : 'disabled'} ā ${localizationChange.to ? 'enabled' : 'disabled'}`,
|
|
198
|
+
color: COLORS.YELLOW
|
|
199
|
+
}]
|
|
200
|
+
: []),
|
|
201
|
+
...added.map((k) => ({ line: ` + ${k}: ${attrDisplay(snapshotAttrs[k])}`, color: COLORS.GREEN })),
|
|
202
|
+
...removed.map((k) => ({ line: ` ā ${k}`, color: COLORS.RED })),
|
|
203
|
+
...typeChanged.map((k) => ({
|
|
204
|
+
line: ` ~ ${k}: ${attrDisplay(strapiSimpleAttrs[k])} ā ${attrDisplay(snapshotAttrs[k])}`,
|
|
205
|
+
color: COLORS.YELLOW
|
|
206
|
+
}))
|
|
207
|
+
];
|
|
208
|
+
parts.forEach((p, i) => {
|
|
209
|
+
console.log(`${p.color}${p.line}${i < parts.length - 1 ? ',' : ''}${COLORS.RESET}`);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function printPreview(
|
|
215
|
+
groupedObjects,
|
|
216
|
+
snapshotContentTypes,
|
|
217
|
+
existingSchemas,
|
|
218
|
+
existingEntriesByType,
|
|
219
|
+
onlyInStrapiContentTypes,
|
|
220
|
+
schemaChangesByType,
|
|
221
|
+
contentChangesByType,
|
|
222
|
+
contentTypeDisplayName,
|
|
223
|
+
formatContentForLog,
|
|
224
|
+
onlyInStrapiComponents = [],
|
|
225
|
+
onlyInSnapshotComponents = [],
|
|
226
|
+
componentSchemaChangesByUid = new Map(),
|
|
227
|
+
localizationChangesByType = new Map(),
|
|
228
|
+
singleTypeChangesByType = new Map()
|
|
229
|
+
) {
|
|
230
|
+
console.log('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
231
|
+
console.log(' PREVIEW: Content and schema changes');
|
|
232
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
|
|
233
|
+
|
|
234
|
+
const strapiSystemKeys = new Set([
|
|
235
|
+
'id',
|
|
236
|
+
'documentId',
|
|
237
|
+
'createdAt',
|
|
238
|
+
'updatedAt',
|
|
239
|
+
'publishedAt',
|
|
240
|
+
'locale',
|
|
241
|
+
'localizations',
|
|
242
|
+
'createdBy',
|
|
243
|
+
'updatedBy'
|
|
244
|
+
]);
|
|
245
|
+
|
|
246
|
+
const sortedContentTypes = Object.keys(groupedObjects).sort((a, b) => {
|
|
247
|
+
const aSchema = schemaChangesByType.get(a) ? 1 : 0;
|
|
248
|
+
const bSchema = schemaChangesByType.get(b) ? 1 : 0;
|
|
249
|
+
return bSchema - aSchema;
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
for (const contentType of sortedContentTypes) {
|
|
253
|
+
const typeObjects = groupedObjects[contentType];
|
|
254
|
+
const singleType = typeObjects.some((o) => o.singleType);
|
|
255
|
+
const existingEntries = existingEntriesByType[contentType] || [];
|
|
256
|
+
if (!contentChangesByType.get(contentType) && !schemaChangesByType.get(contentType)) continue;
|
|
257
|
+
|
|
258
|
+
const { toAdd, toUpdate, onlyInStrapi, schemaKeysForCompare } = buildToAddToUpdateOnlyInStrapi(
|
|
259
|
+
contentType,
|
|
260
|
+
typeObjects,
|
|
261
|
+
existingEntries,
|
|
262
|
+
snapshotContentTypes,
|
|
263
|
+
singleType
|
|
264
|
+
);
|
|
265
|
+
const hasSchemaChange = schemaChangesByType.get(contentType);
|
|
266
|
+
if (toAdd.length === 0 && toUpdate.length === 0 && onlyInStrapi.length === 0 && !hasSchemaChange) continue;
|
|
267
|
+
|
|
268
|
+
const schemaForNew = existingSchemas[contentType];
|
|
269
|
+
const rawForNew =
|
|
270
|
+
schemaForNew?.data?.contentType?.attributes ?? schemaForNew?.data?.schema?.attributes ?? {};
|
|
271
|
+
const isNewSchema = hasSchemaChange && (schemaForNew == null || Object.keys(rawForNew).length === 0);
|
|
272
|
+
const isUpdatedSchema = hasSchemaChange && schemaForNew != null && Object.keys(rawForNew).length > 0;
|
|
273
|
+
|
|
274
|
+
if (isNewSchema) console.log(`${COLORS.GREEN}š ${contentTypeDisplayName(contentType)} (new)${COLORS.RESET}`);
|
|
275
|
+
else if (isUpdatedSchema)
|
|
276
|
+
console.log(`${COLORS.YELLOW}š ${contentTypeDisplayName(contentType)} (edited)${COLORS.RESET}`);
|
|
277
|
+
else console.log(`š ${contentTypeDisplayName(contentType)}`);
|
|
278
|
+
|
|
279
|
+
if (hasSchemaChange) {
|
|
280
|
+
console.log(` Schema:`);
|
|
281
|
+
printSchemaDiff(
|
|
282
|
+
contentType,
|
|
283
|
+
existingSchemas,
|
|
284
|
+
snapshotContentTypes,
|
|
285
|
+
contentTypeDisplayName,
|
|
286
|
+
localizationChangesByType.get(contentType),
|
|
287
|
+
singleTypeChangesByType.get(contentType)
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const contentColor = isNewSchema ? COLORS.GREEN : COLORS.YELLOW;
|
|
292
|
+
const summary = [
|
|
293
|
+
toAdd.length && `${toAdd.length} to add`,
|
|
294
|
+
toUpdate.length && `${toUpdate.length} to update`,
|
|
295
|
+
onlyInStrapi.length > 0 && `${onlyInStrapi.length} to delete`
|
|
296
|
+
]
|
|
297
|
+
.filter(Boolean)
|
|
298
|
+
.join(', ');
|
|
299
|
+
if (summary) console.log(` Content: ${summary}`);
|
|
300
|
+
|
|
301
|
+
toAdd.forEach(({ data, itemLocale }, i) => {
|
|
302
|
+
const localeSuffix = itemLocale ? ` [${itemLocale}]` : '';
|
|
303
|
+
console.log(` ${COLORS.GREEN}+ Entry ${i + 1}${localeSuffix}${COLORS.RESET}`);
|
|
304
|
+
console.log(`${COLORS.GREEN}${formatContentForLog(data, ' ')}${COLORS.RESET}`);
|
|
305
|
+
});
|
|
306
|
+
toUpdate.forEach(({ data, itemLocale, existing }) => {
|
|
307
|
+
const localeSuffix = itemLocale ? ` [${itemLocale}]` : '';
|
|
308
|
+
console.log(` ${contentColor}~ Update:${data.id ? ` ${data.id}` : ''}${localeSuffix}${COLORS.RESET}`);
|
|
309
|
+
const diff = entryDataDiff(data, existing, schemaKeysForCompare);
|
|
310
|
+
console.log(`${contentColor}${formatContentForLog(diff, ' ')}${COLORS.RESET}`);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const contentAttrKeys = Object.keys(snapshotContentTypes[contentType]?.attributes || {});
|
|
314
|
+
onlyInStrapi.forEach((e, i) => {
|
|
315
|
+
const localeSuffix = getEntryLocale(e) ? ` [${getEntryLocale(e)}]` : '';
|
|
316
|
+
console.log(` ${COLORS.RED}ā Entry ${i + 1}: ${localeSuffix}${COLORS.RESET}`);
|
|
317
|
+
const raw = e.attributes ? { ...e.attributes } : { ...e };
|
|
318
|
+
const displayData =
|
|
319
|
+
contentAttrKeys.length > 0
|
|
320
|
+
? Object.fromEntries(contentAttrKeys.filter((k) => raw[k] !== undefined).map((k) => [k, raw[k]]))
|
|
321
|
+
: Object.fromEntries(Object.entries(raw).filter(([k]) => !strapiSystemKeys.has(k)));
|
|
322
|
+
console.log(`${COLORS.RED}${formatContentForLog(displayData, ' ')}${COLORS.RESET}`);
|
|
323
|
+
});
|
|
324
|
+
console.log('');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
for (const contentType of onlyInStrapiContentTypes) {
|
|
328
|
+
const schema = existingSchemas[contentType];
|
|
329
|
+
const strapiSimpleAttrs = getStrapiSimpleAttrs(schema);
|
|
330
|
+
console.log(`${COLORS.RED}š ${contentTypeDisplayName(contentType)} (removed)${COLORS.RESET}`);
|
|
331
|
+
console.log(` Schema:`);
|
|
332
|
+
if (Object.keys(strapiSimpleAttrs).length > 0) {
|
|
333
|
+
const attrLines = Object.entries(strapiSimpleAttrs).map(
|
|
334
|
+
([k, v], i, arr) => ` ${k}: ${v}${i < arr.length - 1 ? ',' : ''}`
|
|
335
|
+
);
|
|
336
|
+
console.log(`${COLORS.RED}${attrLines.join('\n')}${COLORS.RESET}`);
|
|
337
|
+
}
|
|
338
|
+
console.log('');
|
|
339
|
+
}
|
|
340
|
+
for (const componentUid of onlyInSnapshotComponents) {
|
|
341
|
+
console.log(`${COLORS.GREEN}š§© component ${componentUid} (new)${COLORS.RESET}`);
|
|
342
|
+
}
|
|
343
|
+
for (const componentUid of onlyInStrapiComponents) {
|
|
344
|
+
console.log(`${COLORS.RED}š§© component ${componentUid} (removed)${COLORS.RESET}`);
|
|
345
|
+
}
|
|
346
|
+
for (const [componentUid, { strapiSimpleAttrs, snapshotAttrs }] of componentSchemaChangesByUid) {
|
|
347
|
+
console.log(`${COLORS.YELLOW}š§© component ${componentUid} (edited)${COLORS.RESET}`);
|
|
348
|
+
const added = Object.keys(snapshotAttrs).filter((k) => !(k in strapiSimpleAttrs));
|
|
349
|
+
const removed = Object.keys(strapiSimpleAttrs).filter((k) => !(k in snapshotAttrs));
|
|
350
|
+
const typeChanged = Object.keys(snapshotAttrs).filter(
|
|
351
|
+
(k) => strapiSimpleAttrs[k] !== undefined && JSON.stringify(strapiSimpleAttrs[k]) !== JSON.stringify(snapshotAttrs[k])
|
|
352
|
+
);
|
|
353
|
+
const parts = [
|
|
354
|
+
...added.map((k) => ({ line: ` + ${k}: ${attrDisplay(snapshotAttrs[k])}`, color: COLORS.GREEN })),
|
|
355
|
+
...removed.map((k) => ({ line: ` ā ${k}`, color: COLORS.RED })),
|
|
356
|
+
...typeChanged.map((k) => ({
|
|
357
|
+
line: ` ~ ${k}: ${attrDisplay(strapiSimpleAttrs[k])} ā ${attrDisplay(snapshotAttrs[k])}`,
|
|
358
|
+
color: COLORS.YELLOW
|
|
359
|
+
}))
|
|
360
|
+
];
|
|
361
|
+
parts.forEach((p) => console.log(`${p.color}${p.line}${COLORS.RESET}`));
|
|
362
|
+
}
|
|
363
|
+
console.log('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
module.exports = {
|
|
367
|
+
collectItemsToShow,
|
|
368
|
+
findExistingEntry,
|
|
369
|
+
buildToAddToUpdateOnlyInStrapi,
|
|
370
|
+
computeChangeMaps,
|
|
371
|
+
printPreview,
|
|
372
|
+
getStrapiSimpleAttrs
|
|
373
|
+
};
|