@storyblok/migrations 0.1.0
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/LICENSE +8 -0
- package/README.md +73 -0
- package/dist/index.cjs +445 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +203 -0
- package/dist/index.d.mts +203 -0
- package/dist/index.mjs +420 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +71 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
Copyright © 2025 Storyblok GmbH
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
5
|
+
|
|
6
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
7
|
+
|
|
8
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
<h1 align="center">@storyblok/migrations</h1>
|
|
6
|
+
<p>
|
|
7
|
+
Pure utility helpers to migrate content into <a href="https://www.storyblok.com?utm_source=github.com&utm_medium=readme&utm_campaign=storyblok-migrations" target="_blank">Storyblok</a>.
|
|
8
|
+
</p>
|
|
9
|
+
<br />
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="https://npmjs.com/package/@storyblok/migrations">
|
|
14
|
+
<img src="https://img.shields.io/npm/v/@storyblok/migrations/latest.svg?style=flat-square&color=8d60ff" alt="@storyblok/migrations" />
|
|
15
|
+
</a>
|
|
16
|
+
<a href="https://npmjs.com/package/@storyblok/migrations" rel="nofollow">
|
|
17
|
+
<img src="https://img.shields.io/npm/dt/@storyblok/migrations.svg?style=appveyor&color=8d60ff" alt="npm">
|
|
18
|
+
</a>
|
|
19
|
+
<a href="https://storyblok.com/join-discord">
|
|
20
|
+
<img src="https://img.shields.io/discord/700316478792138842?label=Join%20Our%20Discord%20Community&style=appveyor&logo=discord&color=8d60ff">
|
|
21
|
+
</a>
|
|
22
|
+
<a href="https://twitter.com/intent/follow?screen_name=storyblok">
|
|
23
|
+
<img src="https://img.shields.io/badge/Follow-%40storyblok-8d60ff?style=appveyor&logo=twitter" alt="Follow @Storyblok" />
|
|
24
|
+
</a><br/>
|
|
25
|
+
<a href="https://app.storyblok.com/#!/signup?utm_source=github.com&utm_medium=readme&utm_campaign=storyblok-migrations">
|
|
26
|
+
<img src="https://img.shields.io/badge/Try%20Storyblok-Free-8d60ff?style=appveyor&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAABGdBTUEAALGPC/xhBQAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAHqADAAQAAAABAAAAHgAAAADpiRU/AAACRElEQVRIDWNgGGmAEd3D3Js3LPrP8D8WXZwSPiMjw6qvPoHhyGYwIXNAbGpbCjbzP0MYuj0YFqMroBV/wCxmIeSju64eDNzMBJUxvP/9i2Hnq5cM1devMnz984eQsQwETeRhYWHgIcJiXqC6VHlFBjUeXgav40cIWkz1oLYXFmGwFBImaDFBHyObcOzdW4aSq5eRhRiE2dgYlpuYoYSKJi8vw3GgWnyAJIs/AuPu4scPGObd/fqVQZ+PHy7+6udPOBsXgySLDfn5GRYYmaKYJcXBgWLpsx8/GPa8foWiBhuHJIsl2DkYQqWksZkDFgP5PObcKYYff//iVAOTIDlx/QPqRMb/YSYBaWlOToZIaVkGZmAZSQiQ5OPtwHwacuo4iplMQEu6tXUZMhSUGDiYmBjylFQYvv/7x9B04xqKOnQOyT5GN+Df//8M59ASXKyMHLoyDD5JPtbj42OYrm+EYgg70JfuYuIoYmLs7AwMjIzA+uY/zjAnyWJpDk6GOFnCvrn86SOwmsNtKciVFAc1ileBHFDC67lzG10Yg0+SjzF0ownsf/OaofvOLYaDQJoQIGix94ljv1gIZI8Pv38zPvj2lQWYf3HGKbpDCFp85v07NnRN1OBTPY6JdRSGxcCw2k6sZuLVMZ5AV4s1TozPnGGFKbz+/PE7IJsHmC//MDMyhXBw8e6FyRFLv3Z0/IKuFqvFyIqAzd1PwBzJw8jAGPfVx38JshwlbIygxmYY43/GQmpais0ODDHuzevLMARHBcgIAQAbOJHZW0/EyQAAAABJRU5ErkJggg==" alt="Follow @Storyblok" />
|
|
27
|
+
</a>
|
|
28
|
+
</p>
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
- **Content Converters**: Convert HTML and Markdown to Storyblok rich text format.
|
|
33
|
+
- **Link and Asset Helpers**: Transform URLs into Storyblok link and asset objects.
|
|
34
|
+
- **Local File Workflows**: Read and update locally pulled stories, assets, components, and datasources.
|
|
35
|
+
- **Reference Mapping**: Remap story and asset references when migrating between spaces.
|
|
36
|
+
- **Schema Cleanup**: Remove out-of-schema fields and rename datasource values.
|
|
37
|
+
|
|
38
|
+
## Documentation
|
|
39
|
+
|
|
40
|
+
For complete documentation, please visit the [package reference](https://www.storyblok.com/docs/libraries/js/migrations).
|
|
41
|
+
|
|
42
|
+
## Contributing
|
|
43
|
+
|
|
44
|
+
If you're interested in contributing, please read our [contributing docs](https://github.com/storyblok/.github/blob/main/contributing.md) before submitting a pull request.
|
|
45
|
+
|
|
46
|
+
## Community
|
|
47
|
+
|
|
48
|
+
For help, discussion about best practices, or any other conversation that would benefit from being searchable:
|
|
49
|
+
|
|
50
|
+
- [Discuss Storyblok on Github Discussions](https://github.com/storyblok/monoblok/discussions)
|
|
51
|
+
|
|
52
|
+
For community support, chatting with other users, please visit:
|
|
53
|
+
|
|
54
|
+
- [Discuss Storyblok on Discord](https://storyblok.com/join-discord)
|
|
55
|
+
|
|
56
|
+
## Support
|
|
57
|
+
|
|
58
|
+
For bugs or feature requests, please [submit an issue](https://github.com/storyblok/monoblok/issues/new/choose).
|
|
59
|
+
|
|
60
|
+
> [!IMPORTANT]
|
|
61
|
+
> Please search existing issues before submitting a new one. Issues without a minimal reproducible example will be closed. [Why reproductions are Required](https://antfu.me/posts/why-reproductions-are-required).
|
|
62
|
+
|
|
63
|
+
### I can't share my company project code
|
|
64
|
+
|
|
65
|
+
We understand that you might not be able to share your company's project code. Please provide a minimal reproducible example that demonstrates the issue by using tools like [Stackblitz](https://stackblitz.com) or a link to a Github Repo lease make sure you include a README file with the instructions to build and run the project, important not to include any access token, password or personal information of any kind.
|
|
66
|
+
|
|
67
|
+
### Feedback
|
|
68
|
+
|
|
69
|
+
If you have a question, please ask in the [Discuss Storyblok on Discord](https://storyblok.com/join-discord) channel.
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
|
|
73
|
+
[License](/LICENSE)
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
let node_fs_promises = require("node:fs/promises");
|
|
3
|
+
let pathe = require("pathe");
|
|
4
|
+
let _storyblok_richtext_html_parser = require("@storyblok/richtext/html-parser");
|
|
5
|
+
let _storyblok_richtext_markdown_parser = require("@storyblok/richtext/markdown-parser");
|
|
6
|
+
|
|
7
|
+
//#region src/delete-out-of-schema-fields.ts
|
|
8
|
+
const SYSTEM_FIELDS = new Set([
|
|
9
|
+
"_uid",
|
|
10
|
+
"component",
|
|
11
|
+
"_editable"
|
|
12
|
+
]);
|
|
13
|
+
function cleanBlok(blok, schemaDefinition, removedFields) {
|
|
14
|
+
const componentName = blok.component;
|
|
15
|
+
if (!componentName) return blok;
|
|
16
|
+
const schema = schemaDefinition[componentName];
|
|
17
|
+
if (!schema) return blok;
|
|
18
|
+
const validFieldSet = new Set(Object.keys(schema));
|
|
19
|
+
const result = {};
|
|
20
|
+
for (const [key, value] of Object.entries(blok)) {
|
|
21
|
+
if (SYSTEM_FIELDS.has(key)) {
|
|
22
|
+
result[key] = value;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const baseFieldName = key.replace(/__i18n__.*/, "");
|
|
26
|
+
if (!validFieldSet.has(baseFieldName)) {
|
|
27
|
+
removedFields.push({
|
|
28
|
+
component: componentName,
|
|
29
|
+
field: key
|
|
30
|
+
});
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (Array.isArray(value) && value.some((item) => item && typeof item === "object" && !Array.isArray(item) && item.component)) result[key] = value.map((item) => {
|
|
34
|
+
if (item && typeof item === "object" && !Array.isArray(item) && item.component) return cleanBlok(item, schemaDefinition, removedFields);
|
|
35
|
+
return item;
|
|
36
|
+
});
|
|
37
|
+
else result[key] = value;
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
function deleteOutOfSchemaFields(story, schemaDefinition) {
|
|
42
|
+
const removedFields = [];
|
|
43
|
+
if (!story.content) return {
|
|
44
|
+
story,
|
|
45
|
+
removedFields
|
|
46
|
+
};
|
|
47
|
+
const newContent = cleanBlok(story.content, schemaDefinition, removedFields);
|
|
48
|
+
return {
|
|
49
|
+
story: {
|
|
50
|
+
...story,
|
|
51
|
+
content: newContent
|
|
52
|
+
},
|
|
53
|
+
removedFields
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region src/local-utils.ts
|
|
59
|
+
async function readLocalJsonFiles(dir) {
|
|
60
|
+
let files;
|
|
61
|
+
try {
|
|
62
|
+
files = await (0, node_fs_promises.readdir)(dir);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (error.code === "ENOENT") return [];
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
const jsonFiles = files.filter((f) => (0, pathe.extname)(f) === ".json");
|
|
68
|
+
return await Promise.all(jsonFiles.map(async (file) => {
|
|
69
|
+
const filePath = (0, pathe.join)(dir, file);
|
|
70
|
+
const content = await (0, node_fs_promises.readFile)(filePath, "utf8");
|
|
71
|
+
try {
|
|
72
|
+
return JSON.parse(content);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
throw new Error(`Failed to parse ${filePath}: ${error.message}`);
|
|
75
|
+
}
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
async function writeLocalJsonFile(dir, filename, data) {
|
|
79
|
+
await (0, node_fs_promises.mkdir)(dir, { recursive: true });
|
|
80
|
+
await (0, node_fs_promises.writeFile)((0, pathe.join)(dir, filename), JSON.stringify(data, void 0, 2), "utf8");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
//#endregion
|
|
84
|
+
//#region src/local-assets.ts
|
|
85
|
+
function getAssetFilename(asset) {
|
|
86
|
+
return `${(asset.short_filename || String(asset.id)).replace(/\.[^.]+$/, "")}_${asset.id}.json`;
|
|
87
|
+
}
|
|
88
|
+
async function getLocalAssets(dir) {
|
|
89
|
+
return readLocalJsonFiles(dir);
|
|
90
|
+
}
|
|
91
|
+
async function updateLocalAsset(dir, asset) {
|
|
92
|
+
await writeLocalJsonFile(dir, getAssetFilename(asset), asset);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
//#endregion
|
|
96
|
+
//#region src/local-components.ts
|
|
97
|
+
function getComponentFilename(component) {
|
|
98
|
+
return `${component.name}.json`;
|
|
99
|
+
}
|
|
100
|
+
async function getLocalComponents(dir) {
|
|
101
|
+
return readLocalJsonFiles(dir);
|
|
102
|
+
}
|
|
103
|
+
async function updateLocalComponent(dir, component) {
|
|
104
|
+
await writeLocalJsonFile(dir, getComponentFilename(component), component);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
//#endregion
|
|
108
|
+
//#region src/local-datasources.ts
|
|
109
|
+
function getDatasourceFilename(datasource) {
|
|
110
|
+
return `${datasource.slug}_${datasource.id}.json`;
|
|
111
|
+
}
|
|
112
|
+
async function getLocalDatasources(dir) {
|
|
113
|
+
return readLocalJsonFiles(dir);
|
|
114
|
+
}
|
|
115
|
+
async function updateLocalDatasource(dir, datasource) {
|
|
116
|
+
await writeLocalJsonFile(dir, getDatasourceFilename(datasource), datasource);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
//#endregion
|
|
120
|
+
//#region src/local-stories.ts
|
|
121
|
+
function getStoryFilename(story) {
|
|
122
|
+
return `${story.slug}_${story.uuid}.json`;
|
|
123
|
+
}
|
|
124
|
+
async function getLocalStories(dir) {
|
|
125
|
+
return readLocalJsonFiles(dir);
|
|
126
|
+
}
|
|
127
|
+
async function updateLocalStory(dir, story) {
|
|
128
|
+
await writeLocalJsonFile(dir, getStoryFilename(story), story);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region src/map-refs.ts
|
|
133
|
+
function isRecord(value) {
|
|
134
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
135
|
+
}
|
|
136
|
+
function asRecord(value) {
|
|
137
|
+
return isRecord(value) ? value : {};
|
|
138
|
+
}
|
|
139
|
+
function asArray(value) {
|
|
140
|
+
return Array.isArray(value) ? value : [];
|
|
141
|
+
}
|
|
142
|
+
const traverseAndMapBySchema = (data, { schemas, maps, fieldRefMappers, processedFields, missingSchemas }) => {
|
|
143
|
+
if (!isRecord(data) || typeof data.component !== "string") return data ?? {};
|
|
144
|
+
const schema = schemas[data.component];
|
|
145
|
+
if (!schema) {
|
|
146
|
+
missingSchemas.add(data.component);
|
|
147
|
+
return data;
|
|
148
|
+
}
|
|
149
|
+
const dataNew = { ...data };
|
|
150
|
+
for (const [fieldName, fieldValue] of Object.entries(data)) {
|
|
151
|
+
const fieldSchema = schema[fieldName.replace(/__i18n__.*/, "")];
|
|
152
|
+
const fieldType = fieldSchema && typeof fieldSchema === "object" && "type" in fieldSchema && fieldSchema.type;
|
|
153
|
+
const fieldRefMapper = typeof fieldType === "string" && fieldRefMappers[fieldType];
|
|
154
|
+
if (fieldSchema) processedFields.add(fieldSchema);
|
|
155
|
+
if (fieldRefMapper) dataNew[fieldName] = fieldRefMapper(fieldValue, {
|
|
156
|
+
schema: fieldSchema,
|
|
157
|
+
schemas,
|
|
158
|
+
maps,
|
|
159
|
+
fieldRefMappers,
|
|
160
|
+
processedFields,
|
|
161
|
+
missingSchemas
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return dataNew;
|
|
165
|
+
};
|
|
166
|
+
const traverseAndMapRichtextDoc = (data, { schemas, maps, fieldRefMappers, processedFields, missingSchemas }) => {
|
|
167
|
+
if (Array.isArray(data)) return data.map((item) => traverseAndMapRichtextDoc(item, {
|
|
168
|
+
schemas,
|
|
169
|
+
maps,
|
|
170
|
+
fieldRefMappers,
|
|
171
|
+
processedFields,
|
|
172
|
+
missingSchemas
|
|
173
|
+
}));
|
|
174
|
+
if (isRecord(data)) {
|
|
175
|
+
if (data.type === "link" && asRecord(data.attrs).linktype === "story") return {
|
|
176
|
+
...data,
|
|
177
|
+
attrs: {
|
|
178
|
+
...asRecord(data.attrs),
|
|
179
|
+
uuid: maps.stories?.get(asRecord(data.attrs).uuid) ?? asRecord(data.attrs).uuid
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
if (data.type === "blok") return {
|
|
183
|
+
...data,
|
|
184
|
+
attrs: {
|
|
185
|
+
...asRecord(data.attrs),
|
|
186
|
+
body: asArray(asRecord(data.attrs).body).map((d) => traverseAndMapBySchema(d, {
|
|
187
|
+
schemas,
|
|
188
|
+
maps,
|
|
189
|
+
fieldRefMappers,
|
|
190
|
+
processedFields,
|
|
191
|
+
missingSchemas
|
|
192
|
+
}))
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
const newData = {};
|
|
196
|
+
for (const [k, value] of Object.entries(data)) newData[k] = traverseAndMapRichtextDoc(value, {
|
|
197
|
+
schemas,
|
|
198
|
+
maps,
|
|
199
|
+
fieldRefMappers,
|
|
200
|
+
processedFields,
|
|
201
|
+
missingSchemas
|
|
202
|
+
});
|
|
203
|
+
return newData;
|
|
204
|
+
}
|
|
205
|
+
return data;
|
|
206
|
+
};
|
|
207
|
+
const richtextFieldRefMapper = (data, { schemas, maps, fieldRefMappers, processedFields, missingSchemas }) => traverseAndMapRichtextDoc(data, {
|
|
208
|
+
schemas,
|
|
209
|
+
maps,
|
|
210
|
+
fieldRefMappers,
|
|
211
|
+
processedFields,
|
|
212
|
+
missingSchemas
|
|
213
|
+
});
|
|
214
|
+
const multilinkFieldRefMapper = (data, { maps }) => {
|
|
215
|
+
if (!isRecord(data) || data.linktype !== "story") return data;
|
|
216
|
+
return {
|
|
217
|
+
...data,
|
|
218
|
+
id: maps.stories?.get(data.id) ?? data.id
|
|
219
|
+
};
|
|
220
|
+
};
|
|
221
|
+
const bloksFieldRefMapper = (data, { schemas, maps, fieldRefMappers, processedFields, missingSchemas }) => {
|
|
222
|
+
if (!Array.isArray(data)) throw new TypeError(`Invalid bloks field: expected an array, but received ${JSON.stringify(data)}.`);
|
|
223
|
+
return data.map((d) => traverseAndMapBySchema(d, {
|
|
224
|
+
schemas,
|
|
225
|
+
maps,
|
|
226
|
+
fieldRefMappers,
|
|
227
|
+
processedFields,
|
|
228
|
+
missingSchemas
|
|
229
|
+
}));
|
|
230
|
+
};
|
|
231
|
+
const assetFieldRefMapper = (data, { maps }) => {
|
|
232
|
+
if (!isRecord(data)) return data;
|
|
233
|
+
const newId = typeof data.id === "number" ? maps.assets?.get(data.id) : void 0;
|
|
234
|
+
return newId === void 0 ? data : {
|
|
235
|
+
...data,
|
|
236
|
+
id: newId
|
|
237
|
+
};
|
|
238
|
+
};
|
|
239
|
+
const multiassetFieldRefMapper = (data, options) => {
|
|
240
|
+
if (!Array.isArray(data)) throw new TypeError(`Invalid multiasset field: expected an array, but received ${JSON.stringify(data)}.`);
|
|
241
|
+
return data.map((d) => assetFieldRefMapper(d, options));
|
|
242
|
+
};
|
|
243
|
+
const optionsFieldRefMapper = (data, { schema, maps }) => {
|
|
244
|
+
if (!Array.isArray(data)) return data;
|
|
245
|
+
const sourceMap = {
|
|
246
|
+
internal_stories: maps.stories,
|
|
247
|
+
internal_users: maps.users,
|
|
248
|
+
internal_tags: maps.tags,
|
|
249
|
+
internal_datasources: maps.datasources
|
|
250
|
+
}[schema.source ?? ""];
|
|
251
|
+
if (!sourceMap) return data;
|
|
252
|
+
return data.map((d) => sourceMap.get(d) ?? d);
|
|
253
|
+
};
|
|
254
|
+
const fieldRefMappers = {
|
|
255
|
+
asset: assetFieldRefMapper,
|
|
256
|
+
bloks: bloksFieldRefMapper,
|
|
257
|
+
multiasset: multiassetFieldRefMapper,
|
|
258
|
+
multilink: multilinkFieldRefMapper,
|
|
259
|
+
options: optionsFieldRefMapper,
|
|
260
|
+
richtext: richtextFieldRefMapper
|
|
261
|
+
};
|
|
262
|
+
function mapRefs(story, options) {
|
|
263
|
+
const { schemas, maps } = options;
|
|
264
|
+
const processedFields = /* @__PURE__ */ new Set();
|
|
265
|
+
const missingSchemas = /* @__PURE__ */ new Set();
|
|
266
|
+
const alternates = story.alternates ? story.alternates.map((alternate) => {
|
|
267
|
+
const mappedAlternate = asRecord(alternate);
|
|
268
|
+
return {
|
|
269
|
+
...mappedAlternate,
|
|
270
|
+
id: maps.stories?.get(mappedAlternate.id) ?? mappedAlternate.id,
|
|
271
|
+
parent_id: maps.stories?.get(mappedAlternate.parent_id) ?? mappedAlternate.parent_id
|
|
272
|
+
};
|
|
273
|
+
}) : story.alternates;
|
|
274
|
+
const parentId = maps.stories?.get(story.parent_id) ?? story.parent_id;
|
|
275
|
+
const mappedContentRaw = story.content?.component ? traverseAndMapBySchema(story.content, {
|
|
276
|
+
schemas,
|
|
277
|
+
maps,
|
|
278
|
+
fieldRefMappers,
|
|
279
|
+
processedFields,
|
|
280
|
+
missingSchemas
|
|
281
|
+
}) : story.content;
|
|
282
|
+
return {
|
|
283
|
+
mappedStory: {
|
|
284
|
+
...story,
|
|
285
|
+
content: mappedContentRaw,
|
|
286
|
+
id: Number(maps.stories?.get(story.id) ?? story.id),
|
|
287
|
+
uuid: String(maps.stories?.get(story.uuid) ?? story.uuid),
|
|
288
|
+
parent_id: parentId != null ? Number(parentId) : null,
|
|
289
|
+
alternates
|
|
290
|
+
},
|
|
291
|
+
processedFields,
|
|
292
|
+
missingSchemas
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
//#endregion
|
|
297
|
+
//#region src/rename-datasource-value.ts
|
|
298
|
+
function traverseRichtext(data, componentsToUpdate, oldValue, newValue, changes, path) {
|
|
299
|
+
if (Array.isArray(data)) return data.map((item, index) => traverseRichtext(item, componentsToUpdate, oldValue, newValue, changes, `${path}[${index}]`));
|
|
300
|
+
if (data && typeof data === "object" && !Array.isArray(data)) {
|
|
301
|
+
const record = data;
|
|
302
|
+
if (record.type === "blok" && record.attrs && typeof record.attrs === "object") {
|
|
303
|
+
const attrs = record.attrs;
|
|
304
|
+
if (Array.isArray(attrs.body)) return {
|
|
305
|
+
...record,
|
|
306
|
+
attrs: {
|
|
307
|
+
...attrs,
|
|
308
|
+
body: attrs.body.map((item, index) => {
|
|
309
|
+
if (item && typeof item === "object" && !Array.isArray(item)) return traverseObject(item, componentsToUpdate, oldValue, newValue, changes, `${path}.attrs.body[${index}]`);
|
|
310
|
+
return item;
|
|
311
|
+
})
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
const result = {};
|
|
316
|
+
for (const [key, value] of Object.entries(record)) result[key] = traverseRichtext(value, componentsToUpdate, oldValue, newValue, changes, `${path}.${key}`);
|
|
317
|
+
return result;
|
|
318
|
+
}
|
|
319
|
+
return data;
|
|
320
|
+
}
|
|
321
|
+
function traverseObject(obj, componentsToUpdate, oldValue, newValue, changes, path) {
|
|
322
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj)) return obj;
|
|
323
|
+
const result = { ...obj };
|
|
324
|
+
const componentName = obj.component;
|
|
325
|
+
if (componentName) for (const { field, name } of componentsToUpdate) {
|
|
326
|
+
if (name !== componentName) continue;
|
|
327
|
+
const fieldValue = result[field];
|
|
328
|
+
if (typeof fieldValue === "string" && fieldValue === oldValue) {
|
|
329
|
+
result[field] = newValue;
|
|
330
|
+
changes.push({
|
|
331
|
+
component: componentName,
|
|
332
|
+
field,
|
|
333
|
+
path: `${path}.${field}`
|
|
334
|
+
});
|
|
335
|
+
} else if (Array.isArray(fieldValue)) result[field] = fieldValue.map((item) => {
|
|
336
|
+
if (item === oldValue) {
|
|
337
|
+
changes.push({
|
|
338
|
+
component: componentName,
|
|
339
|
+
field,
|
|
340
|
+
path: `${path}.${field}[]`
|
|
341
|
+
});
|
|
342
|
+
return newValue;
|
|
343
|
+
}
|
|
344
|
+
return item;
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
for (const [key, value] of Object.entries(result)) if (Array.isArray(value)) result[key] = value.map((item, index) => {
|
|
348
|
+
if (item && typeof item === "object" && !Array.isArray(item)) return traverseObject(item, componentsToUpdate, oldValue, newValue, changes, `${path}.${key}[${index}]`);
|
|
349
|
+
return item;
|
|
350
|
+
});
|
|
351
|
+
else if (value && typeof value === "object" && !Array.isArray(value) && value.type === "doc" && Array.isArray(value.content)) result[key] = traverseRichtext(value, componentsToUpdate, oldValue, newValue, changes, `${path}.${key}`);
|
|
352
|
+
return result;
|
|
353
|
+
}
|
|
354
|
+
function renameDataSourceValue(story, componentsToUpdate, oldValue, newValue) {
|
|
355
|
+
const changes = [];
|
|
356
|
+
const newContent = traverseObject(story.content, componentsToUpdate, oldValue, newValue, changes, "content");
|
|
357
|
+
return {
|
|
358
|
+
story: {
|
|
359
|
+
...story,
|
|
360
|
+
content: newContent
|
|
361
|
+
},
|
|
362
|
+
changes
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
//#endregion
|
|
367
|
+
//#region src/url-to-asset.ts
|
|
368
|
+
function urlToAsset(url, options) {
|
|
369
|
+
return {
|
|
370
|
+
fieldtype: "asset",
|
|
371
|
+
id: 0,
|
|
372
|
+
filename: url,
|
|
373
|
+
src: url,
|
|
374
|
+
name: url.split("/").at(-1) || url,
|
|
375
|
+
alt: null,
|
|
376
|
+
title: null,
|
|
377
|
+
copyright: null,
|
|
378
|
+
focus: null,
|
|
379
|
+
meta_data: {},
|
|
380
|
+
source: null,
|
|
381
|
+
is_external_url: true,
|
|
382
|
+
is_private: false,
|
|
383
|
+
updated_at: "",
|
|
384
|
+
width: null,
|
|
385
|
+
height: null,
|
|
386
|
+
aspect_ratio: null,
|
|
387
|
+
public_id: null,
|
|
388
|
+
content_type: "",
|
|
389
|
+
...options
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
//#endregion
|
|
394
|
+
//#region src/url-to-link.ts
|
|
395
|
+
function urlToLink(url, options) {
|
|
396
|
+
if (url.startsWith("mailto:")) return {
|
|
397
|
+
fieldtype: "multilink",
|
|
398
|
+
id: "",
|
|
399
|
+
url: "",
|
|
400
|
+
cached_url: "",
|
|
401
|
+
linktype: "email",
|
|
402
|
+
email: url.slice(7),
|
|
403
|
+
...options
|
|
404
|
+
};
|
|
405
|
+
const hashIndex = url.indexOf("#");
|
|
406
|
+
const anchor = hashIndex === -1 ? void 0 : url.slice(hashIndex + 1);
|
|
407
|
+
const cleanUrl = hashIndex === -1 ? url : url.slice(0, hashIndex);
|
|
408
|
+
return {
|
|
409
|
+
fieldtype: "multilink",
|
|
410
|
+
id: "",
|
|
411
|
+
url: cleanUrl,
|
|
412
|
+
cached_url: cleanUrl,
|
|
413
|
+
linktype: "url",
|
|
414
|
+
...anchor ? { anchor } : {},
|
|
415
|
+
...options
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
//#endregion
|
|
420
|
+
exports.deleteOutOfSchemaFields = deleteOutOfSchemaFields;
|
|
421
|
+
exports.getLocalAssets = getLocalAssets;
|
|
422
|
+
exports.getLocalComponents = getLocalComponents;
|
|
423
|
+
exports.getLocalDatasources = getLocalDatasources;
|
|
424
|
+
exports.getLocalStories = getLocalStories;
|
|
425
|
+
Object.defineProperty(exports, 'htmlToStoryblokRichtext', {
|
|
426
|
+
enumerable: true,
|
|
427
|
+
get: function () {
|
|
428
|
+
return _storyblok_richtext_html_parser.htmlToStoryblokRichtext;
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
exports.mapRefs = mapRefs;
|
|
432
|
+
Object.defineProperty(exports, 'markdownToStoryblokRichtext', {
|
|
433
|
+
enumerable: true,
|
|
434
|
+
get: function () {
|
|
435
|
+
return _storyblok_richtext_markdown_parser.markdownToStoryblokRichtext;
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
exports.renameDataSourceValue = renameDataSourceValue;
|
|
439
|
+
exports.updateLocalAsset = updateLocalAsset;
|
|
440
|
+
exports.updateLocalComponent = updateLocalComponent;
|
|
441
|
+
exports.updateLocalDatasource = updateLocalDatasource;
|
|
442
|
+
exports.updateLocalStory = updateLocalStory;
|
|
443
|
+
exports.urlToAsset = urlToAsset;
|
|
444
|
+
exports.urlToLink = urlToLink;
|
|
445
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../src/delete-out-of-schema-fields.ts","../src/local-utils.ts","../src/local-assets.ts","../src/local-components.ts","../src/local-datasources.ts","../src/local-stories.ts","../src/map-refs.ts","../src/rename-datasource-value.ts","../src/url-to-asset.ts","../src/url-to-link.ts"],"sourcesContent":["import type { Story } from '@storyblok/management-api-client/resources/stories';\n\nimport type { ComponentSchemas } from './map-refs';\n\nconst SYSTEM_FIELDS = new Set(['_uid', 'component', '_editable']);\n\ninterface RemovedField {\n component: string;\n field: string;\n}\n\nfunction cleanBlok(\n blok: Record<string, unknown>,\n schemaDefinition: ComponentSchemas,\n removedFields: RemovedField[],\n): Record<string, unknown> {\n const componentName = blok.component as string | undefined;\n if (!componentName) {\n return blok;\n }\n\n const schema = schemaDefinition[componentName];\n if (!schema) {\n // Unknown component — leave untouched\n return blok;\n }\n\n const validFieldSet = new Set(Object.keys(schema));\n const result: Record<string, unknown> = {};\n\n for (const [key, value] of Object.entries(blok)) {\n // Always keep system fields\n if (SYSTEM_FIELDS.has(key)) {\n result[key] = value;\n continue;\n }\n\n // Strip i18n suffix to get base field name\n const baseFieldName = key.replace(/__i18n__.*/, '');\n\n // Field not in schema — remove it\n if (!validFieldSet.has(baseFieldName)) {\n removedFields.push({ component: componentName, field: key });\n continue;\n }\n\n // Check if this is an array of bloks (objects with component property)\n const isBlokArray\n = Array.isArray(value)\n && value.some(\n item =>\n item\n && typeof item === 'object'\n && !Array.isArray(item)\n && (item as Record<string, unknown>).component,\n );\n\n if (isBlokArray) {\n // Traverse blok arrays and process nested bloks recursively\n result[key] = (value as unknown[]).map((item) => {\n if (\n item\n && typeof item === 'object'\n && !Array.isArray(item)\n && (item as Record<string, unknown>).component\n ) {\n return cleanBlok(\n item as Record<string, unknown>,\n schemaDefinition,\n removedFields,\n );\n }\n return item;\n });\n }\n else {\n // Valid field — keep it\n result[key] = value;\n }\n }\n\n return result;\n}\n\nexport function deleteOutOfSchemaFields(\n story: Story,\n schemaDefinition: ComponentSchemas,\n): { story: Story; removedFields: RemovedField[] } {\n const removedFields: RemovedField[] = [];\n\n if (!story.content) {\n return { story, removedFields };\n }\n\n const newContent = cleanBlok(\n story.content as Record<string, unknown>,\n schemaDefinition,\n removedFields,\n );\n\n return {\n // cleanBlok returns Record<string,unknown>; runtime shape satisfies Blok\n story: { ...story, content: newContent as unknown as Story['content'] },\n removedFields,\n };\n}\n","import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';\nimport { extname, join } from 'pathe';\n\nexport async function readLocalJsonFiles<T>(dir: string): Promise<T[]> {\n let files: string[];\n try {\n files = await readdir(dir);\n }\n catch (error: unknown) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') {\n return [];\n }\n throw error;\n }\n\n const jsonFiles = files.filter(f => extname(f) === '.json');\n\n const items = await Promise.all(\n jsonFiles.map(async (file) => {\n const filePath = join(dir, file);\n const content = await readFile(filePath, 'utf8');\n try {\n return JSON.parse(content) as T;\n }\n catch (error: unknown) {\n throw new Error(\n `Failed to parse ${filePath}: ${(error as Error).message}`,\n );\n }\n }),\n );\n\n return items;\n}\n\nexport async function writeLocalJsonFile(\n dir: string,\n filename: string,\n data: unknown,\n): Promise<void> {\n await mkdir(dir, { recursive: true });\n const filePath = join(dir, filename);\n await writeFile(filePath, JSON.stringify(data, undefined, 2), 'utf8');\n}\n","import type { Asset } from '@storyblok/management-api-client/resources/assets';\n\nimport { readLocalJsonFiles, writeLocalJsonFile } from './local-utils';\n\nfunction getAssetFilename(asset: Pick<Asset, 'id' | 'short_filename'>): string {\n const name = (asset.short_filename || String(asset.id)).replace(\n /\\.[^.]+$/,\n '',\n ); // strip extension\n return `${name}_${asset.id}.json`;\n}\n\nexport async function getLocalAssets(dir: string): Promise<Asset[]> {\n return readLocalJsonFiles<Asset>(dir);\n}\n\nexport async function updateLocalAsset(\n dir: string,\n asset: Asset,\n): Promise<void> {\n await writeLocalJsonFile(dir, getAssetFilename(asset), asset);\n}\n","import type { Component } from '@storyblok/management-api-client/resources/components';\n\nimport { readLocalJsonFiles, writeLocalJsonFile } from './local-utils';\n\nfunction getComponentFilename(component: Pick<Component, 'name'>): string {\n return `${component.name}.json`;\n}\n\nexport async function getLocalComponents(dir: string): Promise<Component[]> {\n return readLocalJsonFiles<Component>(dir);\n}\n\nexport async function updateLocalComponent(\n dir: string,\n component: Component,\n): Promise<void> {\n await writeLocalJsonFile(dir, getComponentFilename(component), component);\n}\n","import type { Datasource } from '@storyblok/management-api-client/resources/datasources';\n\nimport { readLocalJsonFiles, writeLocalJsonFile } from './local-utils';\n\nfunction getDatasourceFilename(\n datasource: Pick<Datasource, 'slug' | 'id'>,\n): string {\n return `${datasource.slug}_${datasource.id}.json`;\n}\n\nexport async function getLocalDatasources(dir: string): Promise<Datasource[]> {\n return readLocalJsonFiles<Datasource>(dir);\n}\n\nexport async function updateLocalDatasource(\n dir: string,\n datasource: Datasource,\n): Promise<void> {\n await writeLocalJsonFile(dir, getDatasourceFilename(datasource), datasource);\n}\n\nexport type { Datasource };\n","import type { Story } from '@storyblok/management-api-client/resources/stories';\n\nimport { readLocalJsonFiles, writeLocalJsonFile } from './local-utils';\n\nfunction getStoryFilename(story: Pick<Story, 'slug' | 'uuid'>): string {\n return `${story.slug}_${story.uuid}.json`;\n}\n\nexport async function getLocalStories(dir: string): Promise<Story[]> {\n return readLocalJsonFiles<Story>(dir);\n}\n\nexport async function updateLocalStory(\n dir: string,\n story: Story,\n): Promise<void> {\n await writeLocalJsonFile(dir, getStoryFilename(story), story);\n}\n","import type { Component } from '@storyblok/management-api-client/resources/components';\nimport type { Story } from '@storyblok/management-api-client/resources/stories';\n\nexport interface RefMaps {\n assets?: Map<unknown, string | number>;\n stories?: Map<unknown, string | number>;\n users?: Map<unknown, string | number>;\n tags?: Map<unknown, string | number>;\n datasources?: Map<unknown, string | number>;\n}\n\nexport type ComponentSchemas = Record<\n string,\n Record<string, { type: string; source?: string }>\n>;\n\nexport interface MapRefsOptions {\n schemas: ComponentSchemas;\n maps: RefMaps;\n}\n\ntype ProcessedFields = Set<Component['schema']>;\ntype MissingSchemas = Set<Component['name']>;\ntype UnknownRecord = Record<string, unknown>;\n\ntype RefMapper = (\n data: unknown,\n options: {\n schema: Component['schema'];\n schemas: ComponentSchemas;\n maps: RefMaps;\n fieldRefMappers: FieldRefMappers;\n processedFields: ProcessedFields;\n missingSchemas: MissingSchemas;\n },\n) => unknown;\n\ntype FieldRefMappers = Record<string, RefMapper>;\n\nfunction isRecord(value: unknown): value is UnknownRecord {\n return value !== null && typeof value === 'object' && !Array.isArray(value);\n}\n\nfunction asRecord(value: unknown): UnknownRecord {\n return isRecord(value) ? value : {};\n}\n\nfunction asArray(value: unknown): unknown[] {\n return Array.isArray(value) ? value : [];\n}\n\nconst traverseAndMapBySchema = (\n data: unknown,\n {\n schemas,\n maps,\n fieldRefMappers,\n processedFields,\n missingSchemas,\n }: {\n schemas: ComponentSchemas;\n maps: RefMaps;\n fieldRefMappers: FieldRefMappers;\n processedFields: ProcessedFields;\n missingSchemas: MissingSchemas;\n },\n): unknown => {\n if (!isRecord(data) || typeof data.component !== 'string') {\n return data ?? {};\n }\n\n const schema = schemas[data.component];\n if (!schema) {\n missingSchemas.add(data.component);\n return data;\n }\n\n const dataNew: UnknownRecord = { ...data };\n\n for (const [fieldName, fieldValue] of Object.entries(data)) {\n const fieldSchema = schema[\n fieldName.replace(/__i18n__.*/, '')\n ] as Component['schema'];\n const fieldType\n = fieldSchema\n && typeof fieldSchema === 'object'\n && 'type' in fieldSchema\n && fieldSchema.type;\n const fieldRefMapper\n = typeof fieldType === 'string' && fieldRefMappers[fieldType];\n\n if (fieldSchema) {\n processedFields.add(fieldSchema);\n }\n\n if (fieldRefMapper) {\n dataNew[fieldName] = fieldRefMapper(fieldValue, {\n schema: fieldSchema,\n schemas,\n maps,\n fieldRefMappers,\n processedFields,\n missingSchemas,\n });\n }\n }\n\n return dataNew;\n};\n\nconst traverseAndMapRichtextDoc = (\n data: unknown,\n {\n schemas,\n maps,\n fieldRefMappers,\n processedFields,\n missingSchemas,\n }: {\n schemas: ComponentSchemas;\n maps: RefMaps;\n fieldRefMappers: FieldRefMappers;\n processedFields: ProcessedFields;\n missingSchemas: MissingSchemas;\n },\n): unknown => {\n if (Array.isArray(data)) {\n return data.map(item =>\n traverseAndMapRichtextDoc(item, {\n schemas,\n maps,\n fieldRefMappers,\n processedFields,\n missingSchemas,\n }),\n );\n }\n\n if (isRecord(data)) {\n if (data.type === 'link' && asRecord(data.attrs).linktype === 'story') {\n return {\n ...data,\n attrs: {\n ...asRecord(data.attrs),\n uuid:\n maps.stories?.get(asRecord(data.attrs).uuid)\n ?? asRecord(data.attrs).uuid,\n },\n };\n }\n\n if (data.type === 'blok') {\n return {\n ...data,\n attrs: {\n ...asRecord(data.attrs),\n body: asArray(asRecord(data.attrs).body).map(d =>\n traverseAndMapBySchema(d, {\n schemas,\n maps,\n fieldRefMappers,\n processedFields,\n missingSchemas,\n }),\n ),\n },\n };\n }\n\n const newData: UnknownRecord = {};\n for (const [k, value] of Object.entries(data)) {\n newData[k] = traverseAndMapRichtextDoc(value, {\n schemas,\n maps,\n fieldRefMappers,\n processedFields,\n missingSchemas,\n });\n }\n\n return newData;\n }\n\n return data;\n};\n\nconst richtextFieldRefMapper: RefMapper = (\n data,\n { schemas, maps, fieldRefMappers, processedFields, missingSchemas },\n) =>\n traverseAndMapRichtextDoc(data, {\n schemas,\n maps,\n fieldRefMappers,\n processedFields,\n missingSchemas,\n });\n\nconst multilinkFieldRefMapper: RefMapper = (data, { maps }) => {\n if (!isRecord(data) || data.linktype !== 'story') {\n return data;\n }\n\n return {\n ...data,\n id: maps.stories?.get(data.id) ?? data.id,\n };\n};\n\nconst bloksFieldRefMapper: RefMapper = (\n data,\n { schemas, maps, fieldRefMappers, processedFields, missingSchemas },\n) => {\n if (!Array.isArray(data)) {\n throw new TypeError(\n `Invalid bloks field: expected an array, but received ${JSON.stringify(data)}.`,\n );\n }\n\n return data.map(d =>\n traverseAndMapBySchema(d, {\n schemas,\n maps,\n fieldRefMappers,\n processedFields,\n missingSchemas,\n }),\n );\n};\n\nconst assetFieldRefMapper: RefMapper = (data, { maps }) => {\n if (!isRecord(data)) {\n return data;\n }\n\n const newId\n = typeof data.id === 'number' ? maps.assets?.get(data.id) : undefined;\n return newId === undefined ? data : { ...data, id: newId };\n};\n\nconst multiassetFieldRefMapper: RefMapper = (data, options) => {\n if (!Array.isArray(data)) {\n throw new TypeError(\n `Invalid multiasset field: expected an array, but received ${JSON.stringify(data)}.`,\n );\n }\n\n return data.map(d => assetFieldRefMapper(d, options));\n};\n\nconst optionsFieldRefMapper: RefMapper = (data, { schema, maps }) => {\n if (!Array.isArray(data)) {\n return data;\n }\n\n const sourceMapBySchema: Record<\n string,\n Map<unknown, string | number> | undefined\n > = {\n internal_stories: maps.stories,\n internal_users: maps.users,\n internal_tags: maps.tags,\n internal_datasources: maps.datasources,\n };\n\n const sourceMap\n = sourceMapBySchema[(schema as { source?: string }).source ?? ''];\n if (!sourceMap) {\n return data;\n }\n\n return data.map(d => sourceMap.get(d) ?? d);\n};\n\nconst fieldRefMappers = {\n asset: assetFieldRefMapper,\n bloks: bloksFieldRefMapper,\n multiasset: multiassetFieldRefMapper,\n multilink: multilinkFieldRefMapper,\n options: optionsFieldRefMapper,\n richtext: richtextFieldRefMapper,\n} as const;\n\nexport function mapRefs(\n story: Story,\n options: MapRefsOptions,\n): {\n mappedStory: Story;\n processedFields: ProcessedFields;\n missingSchemas: MissingSchemas;\n } {\n const { schemas, maps } = options;\n const processedFields: ProcessedFields = new Set();\n const missingSchemas: MissingSchemas = new Set();\n\n const alternatesRaw = story.alternates\n ? (story.alternates as Required<Story>['alternates']).map((alternate) => {\n const mappedAlternate = asRecord(alternate);\n return {\n ...mappedAlternate,\n id: maps.stories?.get(mappedAlternate.id) ?? mappedAlternate.id,\n parent_id:\n maps.stories?.get(mappedAlternate.parent_id)\n ?? mappedAlternate.parent_id,\n };\n })\n : story.alternates;\n // mapped ids may be string|number at runtime but shape is compatible\n const alternates = alternatesRaw as Story['alternates'];\n\n const parentId = maps.stories?.get(story.parent_id) ?? story.parent_id;\n const mappedContentRaw = story.content?.component\n ? traverseAndMapBySchema(story.content, {\n schemas,\n maps,\n fieldRefMappers,\n processedFields,\n missingSchemas,\n })\n : story.content;\n\n const mappedStory = {\n ...story,\n // traverseAndMapBySchema returns unknown; runtime shape satisfies Blok\n content: mappedContentRaw as unknown as Story['content'],\n id: Number(maps.stories?.get(story.id) ?? story.id),\n uuid: String(maps.stories?.get(story.uuid) ?? story.uuid),\n parent_id: parentId != null ? Number(parentId) : null,\n alternates,\n } satisfies Story;\n\n return {\n mappedStory,\n processedFields,\n missingSchemas,\n };\n}\n","import type { Story } from '@storyblok/management-api-client/resources/stories';\n\ninterface ComponentToUpdate {\n field: string;\n name: string;\n}\n\ninterface Change {\n component: string;\n field: string;\n path: string;\n}\n\nfunction traverseRichtext(\n data: unknown,\n componentsToUpdate: ComponentToUpdate[],\n oldValue: string,\n newValue: string,\n changes: Change[],\n path: string,\n): unknown {\n if (Array.isArray(data)) {\n return data.map((item, index) =>\n traverseRichtext(item, componentsToUpdate, oldValue, newValue, changes, `${path}[${index}]`),\n );\n }\n\n if (data && typeof data === 'object' && !Array.isArray(data)) {\n const record = data as Record<string, unknown>;\n\n if (record.type === 'blok' && record.attrs && typeof record.attrs === 'object') {\n const attrs = record.attrs as Record<string, unknown>;\n if (Array.isArray(attrs.body)) {\n return {\n ...record,\n attrs: {\n ...attrs,\n body: attrs.body.map((item: unknown, index: number) => {\n if (item && typeof item === 'object' && !Array.isArray(item)) {\n return traverseObject(\n item as Record<string, unknown>,\n componentsToUpdate,\n oldValue,\n newValue,\n changes,\n `${path}.attrs.body[${index}]`,\n );\n }\n return item;\n }),\n },\n };\n }\n }\n\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(record)) {\n result[key] = traverseRichtext(value, componentsToUpdate, oldValue, newValue, changes, `${path}.${key}`);\n }\n return result;\n }\n\n return data;\n}\n\nfunction traverseObject(\n obj: Record<string, unknown>,\n componentsToUpdate: ComponentToUpdate[],\n oldValue: string,\n newValue: string,\n changes: Change[],\n path: string,\n): Record<string, unknown> {\n if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {\n return obj;\n }\n\n const result = { ...obj };\n const componentName = obj.component as string | undefined;\n\n if (componentName) {\n for (const { field, name } of componentsToUpdate) {\n if (name !== componentName) {\n continue;\n }\n\n const fieldValue = result[field];\n\n if (typeof fieldValue === 'string' && fieldValue === oldValue) {\n result[field] = newValue;\n changes.push({\n component: componentName,\n field,\n path: `${path}.${field}`,\n });\n }\n else if (Array.isArray(fieldValue)) {\n const newArray = fieldValue.map((item) => {\n if (item === oldValue) {\n changes.push({\n component: componentName,\n field,\n path: `${path}.${field}[]`,\n });\n return newValue;\n }\n return item;\n });\n result[field] = newArray;\n }\n }\n }\n\n // Traverse nested arrays (bloks fields) and richtext fields\n for (const [key, value] of Object.entries(result)) {\n if (Array.isArray(value)) {\n result[key] = value.map((item, index) => {\n if (item && typeof item === 'object' && !Array.isArray(item)) {\n return traverseObject(\n item as Record<string, unknown>,\n componentsToUpdate,\n oldValue,\n newValue,\n changes,\n `${path}.${key}[${index}]`,\n );\n }\n return item;\n });\n }\n else if (\n value\n && typeof value === 'object'\n && !Array.isArray(value)\n && (value as Record<string, unknown>).type === 'doc'\n && Array.isArray((value as Record<string, unknown>).content)\n ) {\n result[key] = traverseRichtext(value, componentsToUpdate, oldValue, newValue, changes, `${path}.${key}`);\n }\n }\n\n return result;\n}\n\nexport function renameDataSourceValue(\n story: Story,\n componentsToUpdate: ComponentToUpdate[],\n oldValue: string,\n newValue: string,\n): { story: Story; changes: Change[] } {\n const changes: Change[] = [];\n\n const newContent = traverseObject(\n story.content as Record<string, unknown>,\n componentsToUpdate,\n oldValue,\n newValue,\n changes,\n 'content',\n );\n\n return {\n // traverseObject returns Record<string,unknown>; runtime shape satisfies Blok\n story: { ...story, content: newContent as unknown as Story['content'] },\n changes,\n };\n}\n","import type { StoryblokAsset } from './types';\n\nexport interface UrlToAssetOptions {\n alt?: string | null;\n title?: string | null;\n copyright?: string | null;\n focus?: string | null;\n}\n\nexport function urlToAsset(\n url: string,\n options?: UrlToAssetOptions,\n): StoryblokAsset {\n // Derive name from last path segment\n const pathSegments = url.split('/');\n const name = pathSegments.at(-1) || url;\n\n return {\n fieldtype: 'asset',\n id: 0,\n filename: url,\n src: url,\n name,\n alt: null,\n title: null,\n copyright: null,\n focus: null,\n meta_data: {},\n source: null,\n is_external_url: true,\n is_private: false,\n updated_at: '',\n width: null,\n height: null,\n aspect_ratio: null,\n public_id: null,\n content_type: '',\n ...options,\n };\n}\n","import type { StoryblokMultilink } from './types';\n\nexport interface UrlToLinkOptions {\n target?: '_blank' | '_self';\n title?: string;\n rel?: string;\n anchor?: string;\n}\n\nexport function urlToLink(\n url: string,\n options?: UrlToLinkOptions,\n): StoryblokMultilink {\n // Detect mailto: links\n if (url.startsWith('mailto:')) {\n return {\n fieldtype: 'multilink',\n id: '',\n url: '',\n cached_url: '',\n linktype: 'email',\n email: url.slice('mailto:'.length),\n ...options,\n };\n }\n\n // Extract anchor from fragment\n const hashIndex = url.indexOf('#');\n const anchor = hashIndex === -1 ? undefined : url.slice(hashIndex + 1);\n const cleanUrl = hashIndex === -1 ? url : url.slice(0, hashIndex);\n\n return {\n fieldtype: 'multilink',\n id: '',\n url: cleanUrl,\n cached_url: cleanUrl,\n linktype: 'url',\n ...(anchor ? { anchor } : {}),\n ...options,\n };\n}\n"],"mappings":"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"}
|