@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/README.md
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# strapi-sync
|
|
2
|
+
|
|
3
|
+
Export Strapi schemas and content to a snapshot file and apply snapshot changes back to Strapi.
|
|
4
|
+
|
|
5
|
+
## Configuration
|
|
6
|
+
|
|
7
|
+
Optional config file in the project root: `.strapi-sync.json`, `strapi-sync.config.json`, or `strapi-sync.json`.
|
|
8
|
+
|
|
9
|
+
`strapi-sync` also loads `.env` from the project root before resolving environment fallback values.
|
|
10
|
+
|
|
11
|
+
`.env` example:
|
|
12
|
+
|
|
13
|
+
```env
|
|
14
|
+
STRAPI_URL=https://cms.example.com
|
|
15
|
+
STRAPI_API_TOKEN=your-api-token
|
|
16
|
+
STRAPI_PROJECT_PATH=/var/www/strapi-app
|
|
17
|
+
STRAPI_SSH_HOST=cms.example.com
|
|
18
|
+
STRAPI_SSH_USER=deploy
|
|
19
|
+
STRAPI_SSH_PORT=22
|
|
20
|
+
STRAPI_SSH_PASSWORD=your-password
|
|
21
|
+
STRAPI_SSH_PRIVATE_KEY_PATH=C:/Users/Ivan/.ssh/id_rsa
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Minimal config example:
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"strapiUrl": "https://your-strapi.example.com",
|
|
29
|
+
"apiToken": "your-api-token",
|
|
30
|
+
"strapiProjectPath": "/var/www/strapi-app",
|
|
31
|
+
"strapiSSHUser": "deploy",
|
|
32
|
+
"strapiSSHPassword": "your-password",
|
|
33
|
+
"strapiSSHPrivateKeyPath": "C:/Users/Ivan/.ssh/id_rsa"
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
For local schema writes, set `strapiProjectPath` to the local project path.
|
|
38
|
+
|
|
39
|
+
For remote schema writes, set `strapiProjectPath` to the remote project path and add `strapiSSHHost` with either `strapiSSHPassword` or `strapiSSHPrivateKeyPath`.
|
|
40
|
+
|
|
41
|
+
Config values are applied in this order, from lower priority to higher priority:
|
|
42
|
+
|
|
43
|
+
1. `.env` or process environment values for fallback-supported fields
|
|
44
|
+
2. Project json config file
|
|
45
|
+
3. CLI flags
|
|
46
|
+
|
|
47
|
+
Supported environment variables:
|
|
48
|
+
|
|
49
|
+
- `STRAPI_URL`
|
|
50
|
+
- `STRAPI_API_TOKEN`
|
|
51
|
+
- `STRAPI_PROJECT_PATH`
|
|
52
|
+
- `STRAPI_SSH_HOST`
|
|
53
|
+
- `STRAPI_SSH_USER`
|
|
54
|
+
- `STRAPI_SSH_PORT`
|
|
55
|
+
- `STRAPI_SSH_PASSWORD`
|
|
56
|
+
- `STRAPI_SSH_PRIVATE_KEY_PATH`
|
|
57
|
+
|
|
58
|
+
Only these exact environment variable names are read by the CLI.
|
|
59
|
+
|
|
60
|
+
Environment variables are used only when the corresponding CLI or config value is missing.
|
|
61
|
+
|
|
62
|
+
Supported config fields:
|
|
63
|
+
|
|
64
|
+
- `strapiUrl` - required
|
|
65
|
+
- `apiToken` - required
|
|
66
|
+
- `output` - optional, default `strapi-snapshot.json`
|
|
67
|
+
- `strapiProjectPath` - optional for export and content-only sync; required for local or remote schema writes
|
|
68
|
+
- `strapiSSHHost` - optional, enables remote SSH schema writes when set
|
|
69
|
+
- `updateSchema` - optional, default `true`
|
|
70
|
+
- `strapiSSHUser` - optional, default `root` for remote SSH writes
|
|
71
|
+
- `strapiSSHPort` - optional, default `22` for remote SSH writes
|
|
72
|
+
- `strapiSSHPassword` - optional, no default
|
|
73
|
+
- `strapiSSHPrivateKeyPath` - optional, no default
|
|
74
|
+
|
|
75
|
+
## Usage
|
|
76
|
+
|
|
77
|
+
Export a snapshot from Strapi:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
strapi-sync --export
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Apply the current snapshot:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
strapi-sync
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Show package version:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
strapi-sync --version
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Options
|
|
96
|
+
|
|
97
|
+
- `--export` – Optional. Export from Strapi to snapshot and exit. Default behavior without this flag is apply mode.
|
|
98
|
+
- `--output <path>` – Optional. Snapshot file path. Default `strapi-snapshot.json`.
|
|
99
|
+
- `-p, --project <path>` – Optional. Project directory used for config and snapshot resolution. Default is current working directory.
|
|
100
|
+
- `-u, --strapi-url <url>` – Optional if `strapiUrl` exists in config. Otherwise required.
|
|
101
|
+
- `--api-token <token>` – Optional if `apiToken` exists in config. Otherwise required.
|
|
102
|
+
- `--config <path>` – Optional. Use a custom JSON config file instead of the auto-discovered one.
|
|
103
|
+
- `--strapi-project <path>` – Optional. Override `strapiProjectPath` from config or `STRAPI_PROJECT_PATH`.
|
|
104
|
+
- `--strapi-ssh-host <host>` – Optional. Set the SSH host for remote schema writes.
|
|
105
|
+
- `--strapi-ssh-user <user>` – Optional. Override the SSH user for remote schema writes.
|
|
106
|
+
- `--strapi-ssh-port <port>` – Optional. Override the SSH port for remote schema writes.
|
|
107
|
+
- `--strapi-ssh-password <pwd>` – Optional. Use SSH password auth for remote schema writes.
|
|
108
|
+
- `--strapi-ssh-key <path>` – Optional. Use a private key file for remote schema writes.
|
|
109
|
+
- `-h, --help` – Optional. Show help and exit.
|
|
110
|
+
- `-v, --version` – Optional. Show the installed package version and exit.
|
|
111
|
+
|
|
112
|
+
## Behavior
|
|
113
|
+
|
|
114
|
+
- Running `strapi-sync` always prints a preview before confirmation.
|
|
115
|
+
- Running `strapi-sync --export` asks for confirmation before overwriting the snapshot file.
|
|
116
|
+
- Schema writes are controlled by `updateSchema` in config.
|
|
117
|
+
- When `strapiSSHHost` is set, `strapiProjectPath` is treated as a remote filesystem path and schema writes are executed over SSH.
|
|
118
|
+
- When `strapiSSHHost` is not set, `strapiProjectPath` is treated as a local filesystem path.
|
|
119
|
+
- Remote schema writes support password auth or private key auth.
|
|
120
|
+
- After any successful change, the snapshot is exported again from Strapi.
|
|
121
|
+
- Adding a field to schema without adding a value for that field in snapshot entries is treated as a schema-only change.
|
|
122
|
+
- Media fields are exported and shown in schema definitions, but content writes skip them.
|
|
123
|
+
- Unknown CLI arguments stop the command with an error.
|
|
124
|
+
- Invalid snapshot syntax stops the command with detailed validation errors.
|
|
125
|
+
|
|
126
|
+
## Snapshot Format
|
|
127
|
+
|
|
128
|
+
The snapshot file contains:
|
|
129
|
+
|
|
130
|
+
- `version`
|
|
131
|
+
- `contentTypes`
|
|
132
|
+
- `components`
|
|
133
|
+
|
|
134
|
+
Component definitions live in the top-level `components` block:
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
{
|
|
138
|
+
"components": {
|
|
139
|
+
"shared.seo": {
|
|
140
|
+
"attributes": {
|
|
141
|
+
"metaTitle": "string",
|
|
142
|
+
"metaDescription": "string"
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Each content type includes `singleType`, `attributes`, and `entries`.
|
|
150
|
+
|
|
151
|
+
Collection type example:
|
|
152
|
+
|
|
153
|
+
```json
|
|
154
|
+
{
|
|
155
|
+
"singleType": false,
|
|
156
|
+
"attributes": {
|
|
157
|
+
"title": "string"
|
|
158
|
+
},
|
|
159
|
+
"entries": [
|
|
160
|
+
{
|
|
161
|
+
"title": "Example"
|
|
162
|
+
}
|
|
163
|
+
]
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Localized collection type example:
|
|
168
|
+
|
|
169
|
+
```json
|
|
170
|
+
{
|
|
171
|
+
"singleType": false,
|
|
172
|
+
"attributes": {
|
|
173
|
+
"title": "string"
|
|
174
|
+
},
|
|
175
|
+
"entries": {
|
|
176
|
+
"en": [
|
|
177
|
+
{
|
|
178
|
+
"title": "Example"
|
|
179
|
+
}
|
|
180
|
+
],
|
|
181
|
+
"uk": [
|
|
182
|
+
{
|
|
183
|
+
"title": "Приклад"
|
|
184
|
+
}
|
|
185
|
+
]
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Single type example:
|
|
191
|
+
|
|
192
|
+
```json
|
|
193
|
+
{
|
|
194
|
+
"singleType": true,
|
|
195
|
+
"attributes": {
|
|
196
|
+
"title": "string"
|
|
197
|
+
},
|
|
198
|
+
"entries": {
|
|
199
|
+
"title": "Example"
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Localized single type example:
|
|
205
|
+
|
|
206
|
+
```json
|
|
207
|
+
{
|
|
208
|
+
"singleType": true,
|
|
209
|
+
"attributes": {
|
|
210
|
+
"title": "string"
|
|
211
|
+
},
|
|
212
|
+
"entries": {
|
|
213
|
+
"en": {
|
|
214
|
+
"title": "Example"
|
|
215
|
+
},
|
|
216
|
+
"uk": {
|
|
217
|
+
"title": "Приклад"
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
For `singleType: false`, `entries` must be an array or an object with locale keys.
|
|
224
|
+
|
|
225
|
+
For `singleType: true`, `entries` must be a single object or an object with locale keys that point to objects.
|
|
226
|
+
|
|
227
|
+
Supported attribute examples:
|
|
228
|
+
|
|
229
|
+
- `string`
|
|
230
|
+
- `number`
|
|
231
|
+
- `boolean`
|
|
232
|
+
- `date`
|
|
233
|
+
- `json`
|
|
234
|
+
- `media`
|
|
235
|
+
- `media[]`
|
|
236
|
+
- `component:shared.seo`
|
|
237
|
+
- `component:shared.gallery:repeatable`
|
|
238
|
+
- `["shared.media", "shared.rich-text"]` for dynamic zones
|
|
239
|
+
- `relation:article:oneToMany`
|
|
240
|
+
|
|
241
|
+
Component field example:
|
|
242
|
+
|
|
243
|
+
```json
|
|
244
|
+
{
|
|
245
|
+
"singleType": false,
|
|
246
|
+
"attributes": {
|
|
247
|
+
"seo": "component:shared.seo"
|
|
248
|
+
},
|
|
249
|
+
"entries": [
|
|
250
|
+
{
|
|
251
|
+
"seo": {
|
|
252
|
+
"metaTitle": "Example",
|
|
253
|
+
"metaDescription": "Example description"
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
]
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Repeatable component example:
|
|
261
|
+
|
|
262
|
+
```json
|
|
263
|
+
{
|
|
264
|
+
"singleType": false,
|
|
265
|
+
"attributes": {
|
|
266
|
+
"gallery": "component:shared.gallery:repeatable"
|
|
267
|
+
},
|
|
268
|
+
"entries": [
|
|
269
|
+
{
|
|
270
|
+
"gallery": [
|
|
271
|
+
{
|
|
272
|
+
"title": "Slide 1"
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
"title": "Slide 2"
|
|
276
|
+
}
|
|
277
|
+
]
|
|
278
|
+
}
|
|
279
|
+
]
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Dynamic zone example:
|
|
284
|
+
|
|
285
|
+
```json
|
|
286
|
+
{
|
|
287
|
+
"singleType": false,
|
|
288
|
+
"attributes": {
|
|
289
|
+
"blocks": [
|
|
290
|
+
"shared.media",
|
|
291
|
+
"shared.rich-text"
|
|
292
|
+
]
|
|
293
|
+
},
|
|
294
|
+
"entries": [
|
|
295
|
+
{
|
|
296
|
+
"blocks": [
|
|
297
|
+
{
|
|
298
|
+
"__component": "shared.rich-text",
|
|
299
|
+
"body": "Example text"
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
"__component": "shared.media",
|
|
303
|
+
"caption": "Example image"
|
|
304
|
+
}
|
|
305
|
+
]
|
|
306
|
+
}
|
|
307
|
+
]
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Relation attributes must use the short form like `relation:article:oneToMany`. During schema writes it is expanded to the full Strapi UID automatically.
|
|
312
|
+
|
|
313
|
+
Supported relation examples:
|
|
314
|
+
|
|
315
|
+
- `relation:article:oneToOne`
|
|
316
|
+
- `relation:article:oneToMany`
|
|
317
|
+
- `relation:article:manyToOne`
|
|
318
|
+
- `relation:article:manyToMany`
|
|
319
|
+
- `relation:article:oneWay`
|
|
320
|
+
- `relation:article:manyWay`
|
|
321
|
+
- `relation:article:morphOne`
|
|
322
|
+
- `relation:article:morphMany`
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const readline = require('readline');
|
|
5
|
+
const { version } = require('../package.json');
|
|
6
|
+
const { StrapiSyncClient } = require('../lib/strapi-client');
|
|
7
|
+
const { getSchemaUpdater } = require('../lib/schema-writer');
|
|
8
|
+
const { parseArgs } = require('../lib/config');
|
|
9
|
+
const { exportSnapshot, loadSnapshotFile } = require('../lib/snapshot-io');
|
|
10
|
+
const { contentTypeDisplayName, formatContentForLog } = require('../lib/format');
|
|
11
|
+
const { computeChangeMaps, printPreview } = require('../lib/preview');
|
|
12
|
+
const { getContentTypeId } = require('../lib/snapshot-utils');
|
|
13
|
+
const {
|
|
14
|
+
fetchStrapiState,
|
|
15
|
+
computeComponentChanges,
|
|
16
|
+
getPreviewCounts,
|
|
17
|
+
getNonActionableNotes,
|
|
18
|
+
applySchemaChanges,
|
|
19
|
+
waitForStrapiReload,
|
|
20
|
+
applyContentChanges
|
|
21
|
+
} = require('../lib/sync-engine');
|
|
22
|
+
|
|
23
|
+
function askConfirmation(question) {
|
|
24
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
rl.question(question, (answer) => {
|
|
27
|
+
rl.close();
|
|
28
|
+
resolve(/^y(es)?$/i.test(answer.trim()));
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function printHelp() {
|
|
34
|
+
console.log(`
|
|
35
|
+
Usage: strapi-sync [options]
|
|
36
|
+
|
|
37
|
+
Commands:
|
|
38
|
+
strapi-sync Preview and apply snapshot changes
|
|
39
|
+
strapi-sync --export Export Strapi data to snapshot and exit
|
|
40
|
+
|
|
41
|
+
Options:
|
|
42
|
+
--output <path> Snapshot file path (default: strapi-snapshot.json)
|
|
43
|
+
-p, --project <path> Project directory used for config and snapshot paths
|
|
44
|
+
-u, --strapi-url <url> Strapi server URL
|
|
45
|
+
--api-token <token> Strapi API token
|
|
46
|
+
--config <path> Custom JSON config file
|
|
47
|
+
--strapi-project <path> Strapi project path for schema updates
|
|
48
|
+
--strapi-ssh-host <host> SSH host for remote schema updates
|
|
49
|
+
--strapi-ssh-user <user> SSH user for remote schema updates
|
|
50
|
+
--strapi-ssh-port <port> SSH port for remote schema updates
|
|
51
|
+
--strapi-ssh-password <pwd> SSH password for remote schema updates
|
|
52
|
+
--strapi-ssh-key <path> SSH private key for remote schema updates
|
|
53
|
+
-h, --help Show this help
|
|
54
|
+
-v, --version Show package version
|
|
55
|
+
|
|
56
|
+
Notes:
|
|
57
|
+
Media fields are exported but not applied back to Strapi.
|
|
58
|
+
`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function resolveOutputPath(output, projectPath) {
|
|
62
|
+
return path.isAbsolute(output) ? output : path.resolve(projectPath, output);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function main() {
|
|
66
|
+
const options = parseArgs();
|
|
67
|
+
if (options.help) {
|
|
68
|
+
printHelp();
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
if (options.version) {
|
|
72
|
+
console.log(version);
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
75
|
+
if (!options.apiToken) {
|
|
76
|
+
console.error('❌ Error: API token not specified. Set it in config file or pass via parameters.');
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
if (!options.strapiUrl) {
|
|
80
|
+
console.error('❌ Error: Strapi URL not specified. Set it in config file or use --strapi-url.');
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const client = new StrapiSyncClient({
|
|
85
|
+
baseURL: options.strapiUrl,
|
|
86
|
+
apiToken: options.apiToken
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (options.export) {
|
|
90
|
+
try {
|
|
91
|
+
const outputPath = resolveOutputPath(options.output, options.projectPath);
|
|
92
|
+
console.log('');
|
|
93
|
+
const confirmed = await askConfirmation(`Export from Strapi and overwrite ${path.basename(outputPath)}? (y/N) `);
|
|
94
|
+
if (!confirmed) {
|
|
95
|
+
console.log('Aborted.');
|
|
96
|
+
process.exit(0);
|
|
97
|
+
}
|
|
98
|
+
console.log('📥 Exporting schema and content from Strapi...');
|
|
99
|
+
await exportSnapshot(client, outputPath);
|
|
100
|
+
console.log(`✓ Snapshot written: ${outputPath}`);
|
|
101
|
+
process.exit(0);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error('❌', error.message);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let groupedObjects = {};
|
|
109
|
+
let snapshotContentTypes = {};
|
|
110
|
+
let snapshotComponents = {};
|
|
111
|
+
let snapshotPath;
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
snapshotPath = resolveOutputPath(options.output, options.projectPath);
|
|
115
|
+
console.log('📂 Loading snapshot...');
|
|
116
|
+
const loaded = loadSnapshotFile(snapshotPath);
|
|
117
|
+
groupedObjects = loaded.groupedObjects;
|
|
118
|
+
snapshotContentTypes = loaded.snapshotContentTypes ?? {};
|
|
119
|
+
snapshotComponents = loaded.snapshotComponents ?? {};
|
|
120
|
+
console.log(`✓ Loaded ${Object.keys(groupedObjects).length} content types`);
|
|
121
|
+
if (Object.keys(snapshotComponents).length > 0) {
|
|
122
|
+
console.log(`✓ Loaded ${Object.keys(snapshotComponents).length} components`);
|
|
123
|
+
}
|
|
124
|
+
if (Object.keys(groupedObjects).length === 0) {
|
|
125
|
+
console.log('⚠️ Snapshot has no content types.');
|
|
126
|
+
process.exit(0);
|
|
127
|
+
}
|
|
128
|
+
} catch (error) {
|
|
129
|
+
logCliError('❌', error.message);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
console.log('📥 Fetching content types and entries from Strapi...');
|
|
134
|
+
const { apiTypes, existingSchemas, existingEntriesByType, apiIdByType } = await fetchStrapiState(
|
|
135
|
+
client,
|
|
136
|
+
groupedObjects
|
|
137
|
+
);
|
|
138
|
+
console.log('✓ Fetched\n');
|
|
139
|
+
|
|
140
|
+
const { schemaChangesByType, contentChangesByType, localizationChangesByType, singleTypeChangesByType } = computeChangeMaps(
|
|
141
|
+
groupedObjects,
|
|
142
|
+
snapshotContentTypes,
|
|
143
|
+
existingSchemas,
|
|
144
|
+
existingEntriesByType,
|
|
145
|
+
getContentTypeId
|
|
146
|
+
);
|
|
147
|
+
const onlyInStrapiContentTypes = apiTypes.map(getContentTypeId).filter((ct) => !(ct in groupedObjects));
|
|
148
|
+
const schemaUpdater = options.updateSchema && getSchemaUpdater(options);
|
|
149
|
+
let componentChanges = {
|
|
150
|
+
onlyInStrapiComponents: [],
|
|
151
|
+
onlyInSnapshotComponents: [],
|
|
152
|
+
componentSchemaChangesByUid: new Map()
|
|
153
|
+
};
|
|
154
|
+
try {
|
|
155
|
+
componentChanges = await computeComponentChanges(client, snapshotComponents, Boolean(schemaUpdater));
|
|
156
|
+
} catch (_) {}
|
|
157
|
+
const previewCounts = getPreviewCounts(
|
|
158
|
+
groupedObjects,
|
|
159
|
+
schemaChangesByType,
|
|
160
|
+
contentChangesByType,
|
|
161
|
+
onlyInStrapiContentTypes,
|
|
162
|
+
componentChanges,
|
|
163
|
+
Boolean(schemaUpdater)
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
if (previewCounts.detected > 0) {
|
|
167
|
+
printPreview(
|
|
168
|
+
groupedObjects,
|
|
169
|
+
snapshotContentTypes,
|
|
170
|
+
existingSchemas,
|
|
171
|
+
existingEntriesByType,
|
|
172
|
+
onlyInStrapiContentTypes,
|
|
173
|
+
schemaChangesByType,
|
|
174
|
+
contentChangesByType,
|
|
175
|
+
contentTypeDisplayName,
|
|
176
|
+
formatContentForLog,
|
|
177
|
+
componentChanges.onlyInStrapiComponents,
|
|
178
|
+
componentChanges.onlyInSnapshotComponents,
|
|
179
|
+
componentChanges.componentSchemaChangesByUid,
|
|
180
|
+
localizationChangesByType,
|
|
181
|
+
singleTypeChangesByType
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
getNonActionableNotes(
|
|
186
|
+
groupedObjects,
|
|
187
|
+
schemaChangesByType,
|
|
188
|
+
onlyInStrapiContentTypes,
|
|
189
|
+
componentChanges,
|
|
190
|
+
Boolean(schemaUpdater)
|
|
191
|
+
).forEach((note) => console.log(`ℹ️ ${note}`));
|
|
192
|
+
|
|
193
|
+
if (previewCounts.actionable === 0) {
|
|
194
|
+
console.log('No actionable changes to apply.\n');
|
|
195
|
+
process.exit(0);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const confirmed = await askConfirmation('Proceed with actionable changes? (y/N) ');
|
|
199
|
+
if (!confirmed) {
|
|
200
|
+
console.log('Aborted.');
|
|
201
|
+
process.exit(0);
|
|
202
|
+
}
|
|
203
|
+
console.log('');
|
|
204
|
+
|
|
205
|
+
const schemaWasUpdated = await applySchemaChanges({
|
|
206
|
+
schemaUpdater,
|
|
207
|
+
snapshotComponents,
|
|
208
|
+
snapshotContentTypes,
|
|
209
|
+
onlyInSnapshotComponents: componentChanges.onlyInSnapshotComponents,
|
|
210
|
+
componentSchemaChangesByUid: componentChanges.componentSchemaChangesByUid,
|
|
211
|
+
schemaChangesByType,
|
|
212
|
+
localizationChangesByType,
|
|
213
|
+
singleTypeChangesByType,
|
|
214
|
+
existingSchemas,
|
|
215
|
+
onlyInStrapiContentTypes,
|
|
216
|
+
onlyInStrapiComponents: componentChanges.onlyInStrapiComponents
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
if (schemaWasUpdated) {
|
|
220
|
+
const ready = await waitForStrapiReload(client, options);
|
|
221
|
+
if (!ready) {
|
|
222
|
+
console.log('\n✅ Done (schema only).');
|
|
223
|
+
process.exit(0);
|
|
224
|
+
}
|
|
225
|
+
console.log('');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let activeSchemas = existingSchemas;
|
|
229
|
+
let activeEntriesByType = existingEntriesByType;
|
|
230
|
+
let activeApiIdByType = apiIdByType;
|
|
231
|
+
let activeContentChangesByType = contentChangesByType;
|
|
232
|
+
|
|
233
|
+
if (schemaWasUpdated) {
|
|
234
|
+
try {
|
|
235
|
+
const refetched = await fetchStrapiState(client, groupedObjects);
|
|
236
|
+
activeSchemas = refetched.existingSchemas;
|
|
237
|
+
activeEntriesByType = refetched.existingEntriesByType;
|
|
238
|
+
activeApiIdByType = refetched.apiIdByType;
|
|
239
|
+
const recomputed = computeChangeMaps(
|
|
240
|
+
groupedObjects,
|
|
241
|
+
snapshotContentTypes,
|
|
242
|
+
activeSchemas,
|
|
243
|
+
activeEntriesByType,
|
|
244
|
+
getContentTypeId
|
|
245
|
+
);
|
|
246
|
+
activeContentChangesByType = recomputed.contentChangesByType;
|
|
247
|
+
} catch (_) {}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const changesApplied = await applyContentChanges(
|
|
251
|
+
client,
|
|
252
|
+
groupedObjects,
|
|
253
|
+
snapshotContentTypes,
|
|
254
|
+
activeSchemas,
|
|
255
|
+
activeEntriesByType,
|
|
256
|
+
activeApiIdByType,
|
|
257
|
+
activeContentChangesByType,
|
|
258
|
+
schemaChangesByType
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
if (snapshotPath && (changesApplied || schemaWasUpdated)) {
|
|
262
|
+
console.log('\n📤 Updating snapshot from Strapi...');
|
|
263
|
+
await exportSnapshot(client, snapshotPath);
|
|
264
|
+
console.log(`✓ Snapshot updated: ${snapshotPath}`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
console.log('\n✅ Done.');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (require.main === module) {
|
|
271
|
+
main().catch((error) => {
|
|
272
|
+
console.error('❌', error.message);
|
|
273
|
+
process.exit(1);
|
|
274
|
+
});
|
|
275
|
+
}
|
package/index.js
ADDED
package/lib/async.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
async function mapWithConcurrency(items, limit, iteratee) {
|
|
2
|
+
const list = Array.isArray(items) ? items : [];
|
|
3
|
+
const size = Math.max(1, Number(limit) || 1);
|
|
4
|
+
const results = new Array(list.length);
|
|
5
|
+
let nextIndex = 0;
|
|
6
|
+
|
|
7
|
+
async function worker() {
|
|
8
|
+
while (nextIndex < list.length) {
|
|
9
|
+
const currentIndex = nextIndex;
|
|
10
|
+
nextIndex += 1;
|
|
11
|
+
results[currentIndex] = await iteratee(list[currentIndex], currentIndex);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const workerCount = Math.min(size, list.length || 1);
|
|
16
|
+
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
17
|
+
return results;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = {
|
|
21
|
+
mapWithConcurrency
|
|
22
|
+
};
|