@arkebcacy/beacon-cli-temp 0.1.5 → 0.1.7
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/dist/cs/labels/Label.d.ts +8 -0
- package/dist/cs/labels/Label.d.ts.map +1 -0
- package/dist/cs/labels/Label.js +5 -0
- package/dist/cs/labels/Label.js.map +1 -0
- package/dist/cs/labels/getAllLabels.d.ts +2 -1
- package/dist/cs/labels/getAllLabels.d.ts.map +1 -1
- package/dist/cs/labels/getAllLabels.js +24 -4
- package/dist/cs/labels/getAllLabels.js.map +1 -1
- package/dist/cs/locales/addLocale.d.ts +11 -0
- package/dist/cs/locales/addLocale.d.ts.map +1 -0
- package/dist/cs/locales/addLocale.js +36 -0
- package/dist/cs/locales/addLocale.js.map +1 -0
- package/dist/cs/locales/ensureLocaleExists.d.ts +11 -0
- package/dist/cs/locales/ensureLocaleExists.d.ts.map +1 -0
- package/dist/cs/locales/ensureLocaleExists.js +38 -0
- package/dist/cs/locales/ensureLocaleExists.js.map +1 -0
- package/dist/cs/locales/getLocales.d.ts +12 -0
- package/dist/cs/locales/getLocales.d.ts.map +1 -0
- package/dist/cs/locales/getLocales.js +23 -0
- package/dist/cs/locales/getLocales.js.map +1 -0
- package/dist/dto/labels/NormalizedLabels.d.ts +13 -0
- package/dist/dto/labels/NormalizedLabels.d.ts.map +1 -0
- package/dist/dto/labels/NormalizedLabels.js +25 -0
- package/dist/dto/labels/NormalizedLabels.js.map +1 -0
- package/dist/dto/labels/flatten.d.ts +4 -0
- package/dist/dto/labels/flatten.d.ts.map +1 -0
- package/dist/dto/labels/flatten.js +18 -0
- package/dist/dto/labels/flatten.js.map +1 -0
- package/dist/dto/labels/organize.d.ts +4 -0
- package/dist/dto/labels/organize.d.ts.map +1 -0
- package/dist/dto/labels/organize.js +41 -0
- package/dist/dto/labels/organize.js.map +1 -0
- package/dist/schema/entries/toContentstack.d.ts.map +1 -1
- package/dist/schema/entries/toContentstack.js +30 -5
- package/dist/schema/entries/toContentstack.js.map +1 -1
- package/dist/schema/entries/toFilesystem.js +23 -10
- package/dist/schema/entries/toFilesystem.js.map +1 -1
- package/dist/schema/labels/lib/labelHelpers.d.ts +3 -0
- package/dist/schema/labels/lib/labelHelpers.d.ts.map +1 -0
- package/dist/schema/labels/lib/labelHelpers.js +34 -0
- package/dist/schema/labels/lib/labelHelpers.js.map +1 -0
- package/dist/schema/labels/lib/labelOperations.d.ts +6 -0
- package/dist/schema/labels/lib/labelOperations.d.ts.map +1 -0
- package/dist/schema/labels/lib/labelOperations.js +57 -0
- package/dist/schema/labels/lib/labelOperations.js.map +1 -0
- package/dist/schema/labels/toContentstack.d.ts.map +1 -1
- package/dist/schema/labels/toContentstack.js +145 -85
- package/dist/schema/labels/toContentstack.js.map +1 -1
- package/dist/schema/labels/toFilesystem.d.ts.map +1 -1
- package/dist/schema/labels/toFilesystem.js +10 -8
- package/dist/schema/labels/toFilesystem.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/cs/labels/Label.ts +12 -0
- package/src/cs/labels/getAllLabels.ts +30 -5
- package/src/cs/locales/addLocale.ts +59 -0
- package/src/cs/locales/ensureLocaleExists.ts +51 -0
- package/src/cs/locales/getLocales.ts +38 -0
- package/src/dto/labels/NormalizedLabels.ts +47 -0
- package/src/dto/labels/flatten.test.ts +118 -0
- package/src/dto/labels/flatten.ts +27 -0
- package/src/dto/labels/organize.test.ts +131 -0
- package/src/dto/labels/organize.ts +58 -0
- package/src/schema/entries/toContentstack.ts +75 -6
- package/src/schema/entries/toFilesystem.ts +29 -14
- package/src/schema/labels/lib/labelHelpers.ts +36 -0
- package/src/schema/labels/lib/labelOperations.ts +74 -0
- package/src/schema/labels/toContentstack.ts +208 -91
- package/src/schema/labels/toFilesystem.ts +15 -9
|
@@ -3,6 +3,7 @@ import deleteEntry from '#cli/cs/entries/delete.js';
|
|
|
3
3
|
import getEntryLocales from '#cli/cs/entries/getEntryLocales.js';
|
|
4
4
|
import importEntry from '#cli/cs/entries/import.js';
|
|
5
5
|
import type { Entry } from '#cli/cs/entries/Types.js';
|
|
6
|
+
import { ensureLocaleExists } from '#cli/cs/locales/ensureLocaleExists.js';
|
|
6
7
|
import BeaconReplacer from '#cli/dto/entry/BeaconReplacer.js';
|
|
7
8
|
import type ProgressBar from '#cli/ui/progress/ProgressBar.js';
|
|
8
9
|
import type Ctx from '../ctx/Ctx.js';
|
|
@@ -45,7 +46,45 @@ export default async function toContentstack(
|
|
|
45
46
|
update,
|
|
46
47
|
});
|
|
47
48
|
|
|
48
|
-
|
|
49
|
+
// Process unmodified entries to ensure all locale versions are synced
|
|
50
|
+
await processUnmodifiedEntries(
|
|
51
|
+
result.unmodified,
|
|
52
|
+
ctx,
|
|
53
|
+
contentType,
|
|
54
|
+
csEntriesByTitle,
|
|
55
|
+
fsEntriesByTitle,
|
|
56
|
+
transformer,
|
|
57
|
+
filenamesByTitle,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function shouldSyncLocales(
|
|
64
|
+
fsLocaleVersions: Awaited<ReturnType<typeof loadFsLocaleVersions>>,
|
|
65
|
+
csLocaleSet: Set<string>,
|
|
66
|
+
): boolean {
|
|
67
|
+
// Check if we have new locale versions that don't exist in Contentstack
|
|
68
|
+
const fsLocaleSet = new Set(
|
|
69
|
+
fsLocaleVersions.map((lv) => (lv.locale === 'default' ? null : lv.locale)),
|
|
70
|
+
);
|
|
71
|
+
return Array.from(fsLocaleSet).some(
|
|
72
|
+
(locale) => locale !== null && !csLocaleSet.has(locale),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function processUnmodifiedEntries(
|
|
77
|
+
unmodified: Iterable<string>,
|
|
78
|
+
ctx: Ctx,
|
|
79
|
+
contentType: ContentType,
|
|
80
|
+
csEntriesByTitle: ReadonlyMap<string, Entry>,
|
|
81
|
+
fsEntriesByTitle: ReadonlyMap<string, Entry>,
|
|
82
|
+
transformer: BeaconReplacer,
|
|
83
|
+
filenamesByTitle: ReadonlyMap<Entry['uid'], string>,
|
|
84
|
+
) {
|
|
85
|
+
const ui = getUi();
|
|
86
|
+
|
|
87
|
+
for (const title of unmodified) {
|
|
49
88
|
const cs = csEntriesByTitle.get(title);
|
|
50
89
|
const fs = fsEntriesByTitle.get(title);
|
|
51
90
|
|
|
@@ -60,11 +99,33 @@ export default async function toContentstack(
|
|
|
60
99
|
throw new Error(`No matching entry found for ${title}.`);
|
|
61
100
|
}
|
|
62
101
|
|
|
102
|
+
// Load all locale versions from filesystem
|
|
103
|
+
const fsLocaleVersions = await loadFsLocaleVersions(
|
|
104
|
+
fs,
|
|
105
|
+
contentType.uid,
|
|
106
|
+
filenamesByTitle,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const csLocaleSet = await getExistingLocales(ctx, contentType, cs.uid);
|
|
110
|
+
|
|
111
|
+
// Only push if there are new locale versions to sync
|
|
112
|
+
if (!shouldSyncLocales(fsLocaleVersions, csLocaleSet)) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Push all locale versions (including new locales like zh-cn)
|
|
117
|
+
await updateAllLocales(
|
|
118
|
+
ctx,
|
|
119
|
+
transformer,
|
|
120
|
+
contentType,
|
|
121
|
+
fsLocaleVersions,
|
|
122
|
+
cs.uid,
|
|
123
|
+
csLocaleSet,
|
|
124
|
+
);
|
|
125
|
+
|
|
63
126
|
const entry = { ...fs, uid: cs.uid };
|
|
64
127
|
ctx.references.recordEntryForReferences(contentType.uid, entry);
|
|
65
128
|
}
|
|
66
|
-
|
|
67
|
-
return result;
|
|
68
129
|
}
|
|
69
130
|
|
|
70
131
|
function buildUpdateFn(
|
|
@@ -141,6 +202,13 @@ async function updateAllLocales(
|
|
|
141
202
|
entryUid: string,
|
|
142
203
|
csLocaleSet: Set<string>,
|
|
143
204
|
) {
|
|
205
|
+
// Ensure all required locales exist in the target stack before pushing entries
|
|
206
|
+
const localeEnsurePromises = fsLocaleVersions
|
|
207
|
+
.filter((lv) => lv.locale !== 'default')
|
|
208
|
+
.map(async (lv) => ensureLocaleExists(ctx.cs.client, lv.locale));
|
|
209
|
+
|
|
210
|
+
await Promise.all(localeEnsurePromises);
|
|
211
|
+
|
|
144
212
|
// Import all locale versions in parallel for better performance
|
|
145
213
|
const importPromises = fsLocaleVersions.map(async (localeVersion) => {
|
|
146
214
|
const transformed = transformer.process(localeVersion.entry);
|
|
@@ -148,9 +216,10 @@ async function updateAllLocales(
|
|
|
148
216
|
// Pass undefined for 'default' locale (single-locale backward compat)
|
|
149
217
|
const locale =
|
|
150
218
|
localeVersion.locale === 'default' ? undefined : localeVersion.locale;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
219
|
+
|
|
220
|
+
// Always use overwrite=true for locale-specific versions since the entry exists.
|
|
221
|
+
// For 'default' locale (single-locale backward compat), only overwrite if entry has locales.
|
|
222
|
+
const overwrite = locale ? true : csLocaleSet.size > 0;
|
|
154
223
|
|
|
155
224
|
return importEntry(
|
|
156
225
|
ctx.cs.client,
|
|
@@ -80,28 +80,24 @@ function createWriteFn(
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
// If only one locale, save without locale suffix for backward compatibility.
|
|
83
|
-
// When multiple locales exist
|
|
84
|
-
//
|
|
85
|
-
// `Autumn Feast and Social.yaml`). Only use locale suffix when no
|
|
86
|
-
// English locale is available.
|
|
87
|
-
const hasEnglish = locales.some((l) => /^en(?:[-_]|$)/iu.test(l.code));
|
|
88
|
-
const useLocaleSuffix = locales.length > 1 && !hasEnglish;
|
|
89
|
-
|
|
90
|
-
// Log locale details for debugging why specific locales (e.g. Chinese)
|
|
91
|
-
// may not be written to the filesystem.
|
|
92
|
-
// Debug logging removed to avoid unsafe-call lint errors.
|
|
83
|
+
// When multiple locales exist, write English as base file and other locales
|
|
84
|
+
// with locale suffixes (e.g., Entry.yaml for English, Entry.zh-chs.yaml for Chinese)
|
|
93
85
|
|
|
94
86
|
// Write all locale versions in parallel for better performance
|
|
95
|
-
const writePromises = locales.map(async (locale) =>
|
|
96
|
-
|
|
87
|
+
const writePromises = locales.map(async (locale) => {
|
|
88
|
+
const isEnglish = /^en(?:[-_]|$)/iu.test(locale.code);
|
|
89
|
+
// Use locale suffix for non-English locales when multiple locales exist
|
|
90
|
+
const useLocaleSuffix = locales.length > 1 && !isEnglish;
|
|
91
|
+
|
|
92
|
+
return writeLocaleVersion(
|
|
97
93
|
ctx,
|
|
98
94
|
contentType,
|
|
99
95
|
entry,
|
|
100
96
|
locale.code,
|
|
101
97
|
getBasePath,
|
|
102
98
|
useLocaleSuffix,
|
|
103
|
-
)
|
|
104
|
-
);
|
|
99
|
+
);
|
|
100
|
+
});
|
|
105
101
|
|
|
106
102
|
await Promise.all(writePromises);
|
|
107
103
|
};
|
|
@@ -122,8 +118,27 @@ async function writeLocaleVersion(
|
|
|
122
118
|
localeCode,
|
|
123
119
|
);
|
|
124
120
|
|
|
121
|
+
// Skip writing this locale version if it's a fallback (locale doesn't match requested)
|
|
122
|
+
// This happens when Contentstack returns the default locale content because
|
|
123
|
+
// no localized version exists for the requested locale
|
|
124
|
+
if (
|
|
125
|
+
useLocaleSuffix &&
|
|
126
|
+
exported.locale &&
|
|
127
|
+
typeof exported.locale === 'string' &&
|
|
128
|
+
exported.locale !== localeCode
|
|
129
|
+
) {
|
|
130
|
+
// This is a fallback locale, skip writing it
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
125
134
|
const { uid, ...transformed } = transformEntry(ctx, contentType, exported);
|
|
126
135
|
|
|
136
|
+
// Preserve the actual locale code from the source stack
|
|
137
|
+
// This ensures we maintain the exact language-region configuration
|
|
138
|
+
if ('locale' in transformed) {
|
|
139
|
+
transformed.locale = localeCode;
|
|
140
|
+
}
|
|
141
|
+
|
|
127
142
|
const basePath = getBasePath(entry);
|
|
128
143
|
const filePath = useLocaleSuffix
|
|
129
144
|
? basePath.replace(/\.yaml$/u, `.${localeCode}.yaml`)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import isRecord from '#cli/util/isRecord.js';
|
|
2
|
+
|
|
3
|
+
export function canonicalize(value: unknown): unknown {
|
|
4
|
+
if (Array.isArray(value)) {
|
|
5
|
+
const result: unknown[] = [];
|
|
6
|
+
for (const item of value) {
|
|
7
|
+
result.push(canonicalize(item));
|
|
8
|
+
}
|
|
9
|
+
return result;
|
|
10
|
+
}
|
|
11
|
+
if (isRecord(value)) {
|
|
12
|
+
const obj: Record<string, unknown> = value;
|
|
13
|
+
const out: Record<string, unknown> = {};
|
|
14
|
+
const objKeys: string[] = Object.keys(obj);
|
|
15
|
+
objKeys.sort();
|
|
16
|
+
for (const key of objKeys) {
|
|
17
|
+
if (key === 'uid' || key === 'created_at' || key === 'updated_at')
|
|
18
|
+
continue;
|
|
19
|
+
// Normalize parent: skip if empty array
|
|
20
|
+
const val: unknown = obj[key];
|
|
21
|
+
if (key === 'parent' && Array.isArray(val) && val.length === 0) continue;
|
|
22
|
+
const canonicalizedVal: unknown = canonicalize(val);
|
|
23
|
+
out[key] = canonicalizedVal;
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function prepareLabel(
|
|
31
|
+
label: Record<string, unknown>,
|
|
32
|
+
): Record<string, unknown> {
|
|
33
|
+
// Labels are sent to the API with all fields intact
|
|
34
|
+
// The API expects the parent field as an array
|
|
35
|
+
return label;
|
|
36
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import ContentstackError from '#cli/cs/api/ContentstackError.js';
|
|
2
|
+
import isRecord from '#cli/util/isRecord.js';
|
|
3
|
+
import type Ctx from '../../ctx/Ctx.js';
|
|
4
|
+
import type { MutableTransferResults } from '../../xfer/TransferResults.js';
|
|
5
|
+
import type Label from '#cli/cs/labels/Label.js';
|
|
6
|
+
import { canonicalize, prepareLabel } from './labelHelpers.js';
|
|
7
|
+
|
|
8
|
+
export async function updateIfNecessary(
|
|
9
|
+
ctx: Ctx,
|
|
10
|
+
uid: string,
|
|
11
|
+
localLabel: Record<string, unknown>,
|
|
12
|
+
remoteLabel: Label | undefined,
|
|
13
|
+
results: MutableTransferResults,
|
|
14
|
+
) {
|
|
15
|
+
if (!remoteLabel) {
|
|
16
|
+
// If we don't have remote data, err on the side of updating
|
|
17
|
+
const res = (await ctx.cs.client.PUT('/v3/labels/{label_uid}', {
|
|
18
|
+
body: { label: prepareLabel(localLabel) },
|
|
19
|
+
params: { path: { label_uid: uid } },
|
|
20
|
+
})) as unknown;
|
|
21
|
+
const putError = (res as { error?: unknown } | undefined)?.error;
|
|
22
|
+
ContentstackError.throwIfError(putError, `Failed to update label: ${uid}`);
|
|
23
|
+
results.updated.add(uid);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let shouldUpdate = true;
|
|
28
|
+
try {
|
|
29
|
+
shouldUpdate =
|
|
30
|
+
JSON.stringify(canonicalize(localLabel)) !==
|
|
31
|
+
JSON.stringify(canonicalize(remoteLabel));
|
|
32
|
+
} catch {
|
|
33
|
+
shouldUpdate = true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!shouldUpdate) return;
|
|
37
|
+
|
|
38
|
+
const res = (await ctx.cs.client.PUT('/v3/labels/{label_uid}', {
|
|
39
|
+
body: { label: prepareLabel(localLabel) },
|
|
40
|
+
params: { path: { label_uid: uid } },
|
|
41
|
+
})) as unknown;
|
|
42
|
+
const putError = (res as { error?: unknown } | undefined)?.error;
|
|
43
|
+
ContentstackError.throwIfError(putError, `Failed to update label: ${uid}`);
|
|
44
|
+
results.updated.add(uid);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function createLabel(
|
|
48
|
+
ctx: Ctx,
|
|
49
|
+
localLabel: Record<string, unknown>,
|
|
50
|
+
results: MutableTransferResults,
|
|
51
|
+
): Promise<string | null> {
|
|
52
|
+
const res = (await ctx.cs.client.POST('/v3/labels', {
|
|
53
|
+
body: { label: prepareLabel(localLabel) },
|
|
54
|
+
})) as unknown;
|
|
55
|
+
|
|
56
|
+
const postError = (res as { error?: unknown } | undefined)?.error;
|
|
57
|
+
const labelName = typeof localLabel.name === 'string' ? localLabel.name : '';
|
|
58
|
+
ContentstackError.throwIfError(
|
|
59
|
+
postError,
|
|
60
|
+
`Failed to create label: ${labelName}`,
|
|
61
|
+
);
|
|
62
|
+
const postData = (res as { data?: unknown } | undefined)?.data;
|
|
63
|
+
let createdUid: string | null = null;
|
|
64
|
+
if (isRecord(postData)) {
|
|
65
|
+
const pd = postData;
|
|
66
|
+
if (isRecord(pd.label)) {
|
|
67
|
+
const labelObj = pd.label;
|
|
68
|
+
if (typeof labelObj.uid === 'string') createdUid = labelObj.uid;
|
|
69
|
+
}
|
|
70
|
+
if (createdUid === null && typeof pd.uid === 'string') createdUid = pd.uid;
|
|
71
|
+
}
|
|
72
|
+
results.created.add(createdUid ?? '<created>');
|
|
73
|
+
return createdUid;
|
|
74
|
+
}
|
|
@@ -1,139 +1,256 @@
|
|
|
1
1
|
import readYaml from '#cli/fs/readYaml.js';
|
|
2
|
-
import schemaDirectory from '../content-types/schemaDirectory.js';
|
|
3
|
-
import ContentstackError from '#cli/cs/api/ContentstackError.js';
|
|
4
2
|
import type Ctx from '../ctx/Ctx.js';
|
|
5
3
|
import { MutableTransferResults } from '../xfer/TransferResults.js';
|
|
6
4
|
import getUi from '../lib/SchemaUi.js';
|
|
7
5
|
import isRecord from '#cli/util/isRecord.js';
|
|
6
|
+
import flatten from '#cli/dto/labels/flatten.js';
|
|
7
|
+
import { isNormalizedLabels } from '#cli/dto/labels/NormalizedLabels.js';
|
|
8
|
+
import type Label from '#cli/cs/labels/Label.js';
|
|
9
|
+
import getAllLabels from '#cli/cs/labels/getAllLabels.js';
|
|
10
|
+
import { createLabel, updateIfNecessary } from './lib/labelOperations.js';
|
|
11
|
+
import { resolve } from 'node:path';
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Sort labels to ensure parents are processed before children.
|
|
15
|
+
* Labels without parents come first, then labels are sorted by dependency depth.
|
|
16
|
+
*/
|
|
17
|
+
function sortLabelsByDependency(labels: Label[]): Label[] {
|
|
18
|
+
const labelsByUid = new Map<string, Label>();
|
|
19
|
+
const sorted: Label[] = [];
|
|
20
|
+
const processed = new Set<string>();
|
|
21
|
+
|
|
22
|
+
// Build a map for quick lookup
|
|
23
|
+
for (const label of labels) {
|
|
24
|
+
if (isRecord(label) && typeof label.uid === 'string') {
|
|
25
|
+
labelsByUid.set(label.uid, label as Label);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
12
28
|
|
|
29
|
+
// Recursive function to add a label and its dependencies
|
|
30
|
+
function addLabel(label: Label) {
|
|
31
|
+
const uid = typeof label.uid === 'string' ? label.uid : '';
|
|
32
|
+
if (!uid || processed.has(uid)) return;
|
|
33
|
+
|
|
34
|
+
// First, process parent if it exists in our label set
|
|
35
|
+
const parentArray = label.parent;
|
|
36
|
+
if (Array.isArray(parentArray) && parentArray.length > 0) {
|
|
37
|
+
// Use destructuring to satisfy eslint prefer-destructuring rule
|
|
38
|
+
const [parentValue] = parentArray as unknown as readonly [
|
|
39
|
+
string,
|
|
40
|
+
...string[],
|
|
41
|
+
];
|
|
42
|
+
if (labelsByUid.has(parentValue) && !processed.has(parentValue)) {
|
|
43
|
+
const parentLabel = labelsByUid.get(parentValue);
|
|
44
|
+
if (parentLabel) addLabel(parentLabel);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Now add this label
|
|
49
|
+
sorted.push(label);
|
|
50
|
+
processed.add(uid);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Process all labels
|
|
54
|
+
for (const label of labels) {
|
|
55
|
+
if (isRecord(label)) {
|
|
56
|
+
addLabel(label as Label);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return sorted;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default async function toContentstack(ctx: Ctx) {
|
|
13
64
|
const ui = getUi();
|
|
65
|
+
const path = resolve(ui.options.schema.schemaPath, 'labels.yaml');
|
|
14
66
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
data = await readYaml(path);
|
|
18
|
-
} catch {
|
|
19
|
-
ui.info(`Labels: no file at ${path}, skipping`);
|
|
67
|
+
const data = await loadLabelsData(path, ui);
|
|
68
|
+
if (!data) {
|
|
20
69
|
return new MutableTransferResults();
|
|
21
70
|
}
|
|
22
71
|
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
? data.labels
|
|
26
|
-
: Array.isArray(data)
|
|
27
|
-
? (data as unknown[])
|
|
28
|
-
: [];
|
|
72
|
+
const flatLabels = extractFlatLabels(data);
|
|
73
|
+
ui.info(`Labels: read ${flatLabels.length} label(s) from ${path}`);
|
|
29
74
|
|
|
30
|
-
|
|
75
|
+
const {
|
|
76
|
+
localUidToName,
|
|
77
|
+
nameToRemoteUid,
|
|
78
|
+
remoteLabelsByName,
|
|
79
|
+
remoteLabelsByUid,
|
|
80
|
+
} = await prepareLabelMappings(ctx, flatLabels);
|
|
31
81
|
|
|
82
|
+
const sortedLabels = sortLabelsByDependency(flatLabels);
|
|
32
83
|
const results = new MutableTransferResults();
|
|
33
84
|
|
|
34
|
-
for (const labelRaw of
|
|
85
|
+
for (const labelRaw of sortedLabels) {
|
|
35
86
|
if (!isRecord(labelRaw)) continue;
|
|
36
|
-
// keep per-label logic in helper to reduce complexity of this function
|
|
37
87
|
|
|
38
|
-
await handleLabel(
|
|
88
|
+
await handleLabel(
|
|
89
|
+
ctx,
|
|
90
|
+
labelRaw,
|
|
91
|
+
remoteLabelsByUid,
|
|
92
|
+
remoteLabelsByName,
|
|
93
|
+
nameToRemoteUid,
|
|
94
|
+
localUidToName,
|
|
95
|
+
results,
|
|
96
|
+
);
|
|
39
97
|
}
|
|
40
98
|
|
|
41
99
|
return results;
|
|
42
100
|
}
|
|
43
101
|
|
|
44
|
-
function
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
return out;
|
|
102
|
+
async function loadLabelsData(
|
|
103
|
+
path: string,
|
|
104
|
+
ui: ReturnType<typeof getUi>,
|
|
105
|
+
): Promise<unknown> {
|
|
106
|
+
try {
|
|
107
|
+
return await readYaml(path);
|
|
108
|
+
} catch {
|
|
109
|
+
ui.info(`Labels: no file at ${path}, skipping`);
|
|
110
|
+
return undefined;
|
|
55
111
|
}
|
|
56
|
-
return value;
|
|
57
112
|
}
|
|
58
113
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const res = (await ctx.cs.client.PUT('/v3/labels/{label_uid}', {
|
|
73
|
-
body: { label: localLabel },
|
|
74
|
-
params: { path: { label_uid: uid } },
|
|
75
|
-
})) as unknown;
|
|
76
|
-
const putError = (res as { error?: unknown } | undefined)?.error;
|
|
77
|
-
ContentstackError.throwIfError(putError, `Failed to update label: ${uid}`);
|
|
78
|
-
results.updated.add(uid);
|
|
79
|
-
return;
|
|
114
|
+
function extractFlatLabels(data: unknown): Label[] {
|
|
115
|
+
if (isNormalizedLabels(data)) {
|
|
116
|
+
interface LabelWithChildren {
|
|
117
|
+
children?: unknown;
|
|
118
|
+
}
|
|
119
|
+
const hasChildren = (data.labels as unknown as LabelWithChildren[]).some(
|
|
120
|
+
(l) => 'children' in l && l.children !== undefined,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
if (hasChildren) {
|
|
124
|
+
return [...flatten(data.labels)];
|
|
125
|
+
}
|
|
126
|
+
return [...data.labels] as unknown as Label[];
|
|
80
127
|
}
|
|
81
128
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
shouldUpdate =
|
|
86
|
-
JSON.stringify(canonicalize(localLabel)) !==
|
|
87
|
-
JSON.stringify(canonicalize(remoteLabel));
|
|
88
|
-
} catch {
|
|
89
|
-
shouldUpdate = true;
|
|
129
|
+
if (isRecord(data) && Array.isArray(data.labels)) {
|
|
130
|
+
return data.labels as Label[];
|
|
90
131
|
}
|
|
91
132
|
|
|
92
|
-
if (
|
|
133
|
+
if (Array.isArray(data)) {
|
|
134
|
+
return data as Label[];
|
|
135
|
+
}
|
|
93
136
|
|
|
94
|
-
|
|
95
|
-
body: { label: localLabel },
|
|
96
|
-
params: { path: { label_uid: uid } },
|
|
97
|
-
})) as unknown;
|
|
98
|
-
const putError = (res as { error?: unknown } | undefined)?.error;
|
|
99
|
-
ContentstackError.throwIfError(putError, `Failed to update label: ${uid}`);
|
|
100
|
-
results.updated.add(uid);
|
|
137
|
+
return [];
|
|
101
138
|
}
|
|
102
139
|
|
|
103
|
-
async function
|
|
104
|
-
ctx
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
140
|
+
async function prepareLabelMappings(ctx: Ctx, flatLabels: Label[]) {
|
|
141
|
+
const remoteLabels = await getAllLabels(ctx.cs.client);
|
|
142
|
+
const remoteLabelsByUid = new Map<string, Label>();
|
|
143
|
+
const remoteLabelsByName = new Map<string, Label>();
|
|
144
|
+
|
|
145
|
+
for (const label of remoteLabels) {
|
|
146
|
+
remoteLabelsByUid.set(label.uid, label);
|
|
147
|
+
remoteLabelsByName.set(label.name, label);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const nameToRemoteUid = new Map<string, string>();
|
|
151
|
+
for (const [name, label] of remoteLabelsByName) {
|
|
152
|
+
nameToRemoteUid.set(name, label.uid);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const localUidToName = new Map<string, string>();
|
|
156
|
+
for (const label of flatLabels) {
|
|
157
|
+
if (
|
|
158
|
+
isRecord(label) &&
|
|
159
|
+
typeof label.uid === 'string' &&
|
|
160
|
+
typeof label.name === 'string'
|
|
161
|
+
) {
|
|
162
|
+
localUidToName.set(label.uid, label.name);
|
|
120
163
|
}
|
|
121
|
-
if (createdUid === null && typeof pd.uid === 'string') createdUid = pd.uid;
|
|
122
164
|
}
|
|
123
|
-
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
localUidToName,
|
|
168
|
+
nameToRemoteUid,
|
|
169
|
+
remoteLabelsByName,
|
|
170
|
+
remoteLabelsByUid,
|
|
171
|
+
};
|
|
124
172
|
}
|
|
125
173
|
|
|
126
174
|
async function handleLabel(
|
|
127
175
|
ctx: Ctx,
|
|
128
176
|
labelRaw: Record<string, unknown>,
|
|
177
|
+
remoteLabelsByUid: Map<string, Label>,
|
|
178
|
+
remoteLabelsByName: Map<string, Label>,
|
|
179
|
+
nameToRemoteUid: Map<string, string>,
|
|
180
|
+
localUidToName: Map<string, string>,
|
|
129
181
|
results: MutableTransferResults,
|
|
130
182
|
) {
|
|
131
183
|
const uid = typeof labelRaw.uid === 'string' ? labelRaw.uid : '';
|
|
184
|
+
const name = typeof labelRaw.name === 'string' ? labelRaw.name : '';
|
|
185
|
+
|
|
186
|
+
const preparedLabel = translateParentReference(
|
|
187
|
+
labelRaw,
|
|
188
|
+
localUidToName,
|
|
189
|
+
nameToRemoteUid,
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// First try to find by UID (exact match from same stack)
|
|
193
|
+
if (uid.length > 0 && remoteLabelsByUid.has(uid)) {
|
|
194
|
+
const remoteLabel = remoteLabelsByUid.get(uid);
|
|
195
|
+
await updateIfNecessary(ctx, uid, preparedLabel, remoteLabel, results);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
132
198
|
|
|
133
|
-
|
|
134
|
-
|
|
199
|
+
// Then try to find by name (for cross-stack scenarios)
|
|
200
|
+
if (name.length > 0 && remoteLabelsByName.has(name)) {
|
|
201
|
+
const remoteLabel = remoteLabelsByName.get(name);
|
|
202
|
+
if (!remoteLabel) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
await updateIfNecessary(
|
|
206
|
+
ctx,
|
|
207
|
+
remoteLabel.uid,
|
|
208
|
+
preparedLabel,
|
|
209
|
+
remoteLabel,
|
|
210
|
+
results,
|
|
211
|
+
);
|
|
212
|
+
nameToRemoteUid.set(name, remoteLabel.uid);
|
|
135
213
|
return;
|
|
136
214
|
}
|
|
137
215
|
|
|
138
|
-
|
|
216
|
+
// Label doesn't exist remotely, create it
|
|
217
|
+
const createdUid = await createLabel(ctx, preparedLabel, results);
|
|
218
|
+
|
|
219
|
+
if (createdUid && name) {
|
|
220
|
+
nameToRemoteUid.set(name, createdUid);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function translateParentReference(
|
|
225
|
+
labelRaw: Record<string, unknown>,
|
|
226
|
+
localUidToName: Map<string, string>,
|
|
227
|
+
nameToRemoteUid: Map<string, string>,
|
|
228
|
+
): Record<string, unknown> {
|
|
229
|
+
const parentField = labelRaw.parent;
|
|
230
|
+
if (!Array.isArray(parentField) || parentField.length === 0) {
|
|
231
|
+
return { ...labelRaw };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// TypeScript knows parentField is an array, but we need to type it properly
|
|
235
|
+
const [firstElement] = parentField as unknown[];
|
|
236
|
+
if (typeof firstElement !== 'string') {
|
|
237
|
+
return { ...labelRaw };
|
|
238
|
+
}
|
|
239
|
+
const firstParent: string = firstElement;
|
|
240
|
+
|
|
241
|
+
const parentName = localUidToName.get(firstParent);
|
|
242
|
+
|
|
243
|
+
if (!parentName || !nameToRemoteUid.has(parentName)) {
|
|
244
|
+
return { ...labelRaw };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const remoteParentUid = nameToRemoteUid.get(parentName);
|
|
248
|
+
if (!remoteParentUid) {
|
|
249
|
+
return { ...labelRaw };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
...labelRaw,
|
|
254
|
+
parent: [remoteParentUid],
|
|
255
|
+
};
|
|
139
256
|
}
|
|
@@ -1,24 +1,30 @@
|
|
|
1
1
|
import getAllLabels from '#cli/cs/labels/getAllLabels.js';
|
|
2
2
|
import writeYaml from '#cli/fs/writeYaml.js';
|
|
3
|
-
import
|
|
3
|
+
import organize from '#cli/dto/labels/organize.js';
|
|
4
4
|
import { MutableTransferResults } from '../xfer/TransferResults.js';
|
|
5
5
|
import createProgressBar from '../lib/createProgressBar.js';
|
|
6
6
|
import type Ctx from '../ctx/Ctx.js';
|
|
7
|
+
import getUi from '../lib/SchemaUi.js';
|
|
8
|
+
import { resolve } from 'node:path';
|
|
7
9
|
|
|
8
10
|
export default async function toFilesystem(ctx: Ctx) {
|
|
9
|
-
const
|
|
11
|
+
const ui = getUi();
|
|
12
|
+
const {
|
|
13
|
+
options: {
|
|
14
|
+
schema: { schemaPath },
|
|
15
|
+
},
|
|
16
|
+
} = ui;
|
|
17
|
+
const labelsPath = resolve(schemaPath, 'labels.yaml');
|
|
10
18
|
const bar = createProgressBar('Labels', 1, 0);
|
|
11
19
|
|
|
12
|
-
// Fetch labels and
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
await writeYaml(
|
|
20
|
+
// Fetch labels and organize them into a hierarchical structure
|
|
21
|
+
const flatLabels = await getAllLabels(ctx.cs.client);
|
|
22
|
+
const hierarchicalLabels = organize(flatLabels);
|
|
23
|
+
|
|
24
|
+
await writeYaml(labelsPath, { labels: hierarchicalLabels });
|
|
17
25
|
|
|
18
26
|
const result = new MutableTransferResults();
|
|
19
27
|
result.created.add('labels.yaml');
|
|
20
28
|
bar.increment();
|
|
21
29
|
return result;
|
|
22
30
|
}
|
|
23
|
-
|
|
24
|
-
//# sourceMappingURL=toFilesystem.js.map
|