@arkebcacy/beacon-cli-temp 0.1.3 → 0.1.5
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/entries/getEntryLocales.d.ts.map +1 -1
- package/dist/cs/entries/getEntryLocales.js +23 -10
- package/dist/cs/entries/getEntryLocales.js.map +1 -1
- package/dist/cs/labels/getAllLabels.d.ts +3 -0
- package/dist/cs/labels/getAllLabels.d.ts.map +1 -0
- package/dist/cs/labels/getAllLabels.js +17 -0
- package/dist/cs/labels/getAllLabels.js.map +1 -0
- package/dist/dto/entry/beaconReplacer/lib/mapItemPathToAsset.d.ts.map +1 -1
- package/dist/dto/entry/beaconReplacer/lib/mapItemPathToAsset.js +5 -1
- package/dist/dto/entry/beaconReplacer/lib/mapItemPathToAsset.js.map +1 -1
- package/dist/schema/entries/toFilesystem.d.ts.map +1 -1
- package/dist/schema/entries/toFilesystem.js +24 -6
- package/dist/schema/entries/toFilesystem.js.map +1 -1
- package/dist/schema/labels/toContentstack.d.ts +4 -0
- package/dist/schema/labels/toContentstack.d.ts.map +1 -0
- package/dist/schema/labels/toContentstack.js +113 -0
- package/dist/schema/labels/toContentstack.js.map +1 -0
- package/dist/schema/labels/toFilesystem.d.ts +4 -0
- package/dist/schema/labels/toFilesystem.d.ts.map +1 -0
- package/dist/schema/labels/toFilesystem.js +20 -0
- package/dist/schema/labels/toFilesystem.js.map +1 -0
- package/dist/schema/lib/createProgressBar.d.ts +1 -1
- package/dist/schema/lib/createProgressBar.d.ts.map +1 -1
- package/dist/schema/lib/createProgressBar.js +11 -1
- package/dist/schema/lib/createProgressBar.js.map +1 -1
- package/dist/schema/lib/pullModules.d.ts.map +1 -1
- package/dist/schema/lib/pullModules.js +2 -0
- package/dist/schema/lib/pullModules.js.map +1 -1
- package/dist/schema/normalize.d.ts +3 -2
- package/dist/schema/normalize.d.ts.map +1 -1
- package/dist/schema/normalize.js +10 -1
- package/dist/schema/normalize.js.map +1 -1
- package/dist/schema/push.d.ts.map +1 -1
- package/dist/schema/push.js +2 -0
- package/dist/schema/push.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/cs/entries/getEntryLocales.ts +28 -19
- package/src/cs/labels/getAllLabels.ts +18 -0
- package/src/dto/entry/beaconReplacer/lib/mapItemPathToAsset.ts +9 -1
- package/src/schema/entries/toFilesystem.ts +26 -6
- package/src/schema/isEquivalentSchema.test.ts +84 -0
- package/src/schema/labels/toContentstack.ts +139 -0
- package/src/schema/labels/toFilesystem.ts +24 -0
- package/src/schema/lib/createProgressBar.ts +10 -2
- package/src/schema/lib/pullModules.ts +2 -0
- package/src/schema/normalize.test.ts +98 -0
- package/src/schema/normalize.ts +11 -0
- package/src/schema/push.ts +2 -0
package/package.json
CHANGED
|
@@ -11,23 +11,8 @@ export interface LocaleInfo {
|
|
|
11
11
|
readonly uid: string;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function isLocaleInfo(o: unknown): o is LocaleInfo {
|
|
19
|
-
return (
|
|
20
|
-
isRecord(o) &&
|
|
21
|
-
typeof o.code === 'string' &&
|
|
22
|
-
typeof o.name === 'string' &&
|
|
23
|
-
typeof o.uid === 'string'
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function isLocalesResponse(o: unknown): o is LocalesResponse {
|
|
28
|
-
return (
|
|
29
|
-
isRecord(o) && Array.isArray(o.locales) && o.locales.every(isLocaleInfo)
|
|
30
|
-
);
|
|
14
|
+
function isLocaleInfoRecord(o: unknown): o is Record<string, unknown> {
|
|
15
|
+
return isRecord(o) && typeof o.code === 'string';
|
|
31
16
|
}
|
|
32
17
|
|
|
33
18
|
/**
|
|
@@ -73,9 +58,33 @@ export default async function getEntryLocales(
|
|
|
73
58
|
|
|
74
59
|
const result = data as unknown;
|
|
75
60
|
|
|
76
|
-
if (!
|
|
61
|
+
if (!isRecord(result)) {
|
|
77
62
|
throw new Error(msg);
|
|
78
63
|
}
|
|
79
64
|
|
|
80
|
-
|
|
65
|
+
const raw = result.locales;
|
|
66
|
+
if (!Array.isArray(raw)) {
|
|
67
|
+
throw new Error(msg);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const normalized: LocaleInfo[] = raw.map((item) => {
|
|
71
|
+
const rec = isLocaleInfoRecord(item)
|
|
72
|
+
? item
|
|
73
|
+
: ({ code: '' } as Record<string, unknown>);
|
|
74
|
+
const code = typeof rec.code === 'string' ? rec.code : '';
|
|
75
|
+
const name = typeof rec.name === 'string' ? rec.name : code;
|
|
76
|
+
const uid = typeof rec.uid === 'string' ? rec.uid : code;
|
|
77
|
+
|
|
78
|
+
// Only include `fallback_locale` when present as a string. Construct
|
|
79
|
+
// the object with the correct shape so TypeScript can verify it against
|
|
80
|
+
// `LocaleInfo` without unsafe casts.
|
|
81
|
+
const locale =
|
|
82
|
+
typeof rec.fallback_locale === 'string'
|
|
83
|
+
? { code, fallback_locale: rec.fallback_locale, name, uid }
|
|
84
|
+
: { code, name, uid };
|
|
85
|
+
|
|
86
|
+
return locale;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return normalized;
|
|
81
90
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import ContentstackError from '../api/ContentstackError.js';
|
|
2
|
+
import type Client from '../api/Client.js';
|
|
3
|
+
import isRecord from '#cli/util/isRecord.js';
|
|
4
|
+
|
|
5
|
+
export default async function getAllLabels(client: Client): Promise<unknown[]> {
|
|
6
|
+
const res = (await client.GET('/v3/labels')) as unknown;
|
|
7
|
+
const data = (res as { data?: unknown } | undefined)?.data;
|
|
8
|
+
const error = (res as { error?: unknown } | undefined)?.error;
|
|
9
|
+
const msg = `Failed to fetch labels`;
|
|
10
|
+
ContentstackError.throwIfError(error, msg);
|
|
11
|
+
if (isRecord(data) && Array.isArray(data.labels)) {
|
|
12
|
+
return data.labels as unknown[];
|
|
13
|
+
}
|
|
14
|
+
if (Array.isArray(data)) {
|
|
15
|
+
return data as unknown[];
|
|
16
|
+
}
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
@@ -6,7 +6,15 @@ export default function mapItemPathToAsset(
|
|
|
6
6
|
this: BeaconReplacer,
|
|
7
7
|
itemPath: string,
|
|
8
8
|
) {
|
|
9
|
-
const
|
|
9
|
+
const normalized = itemPath
|
|
10
|
+
.split('/')
|
|
11
|
+
.map((s) => s.replace(/\s+/gu, ' ').trim())
|
|
12
|
+
.join('/');
|
|
13
|
+
|
|
14
|
+
const result = resolveRawAssetItem(
|
|
15
|
+
this.ctx.cs.assets.byParentUid,
|
|
16
|
+
normalized,
|
|
17
|
+
);
|
|
10
18
|
|
|
11
19
|
if (!result) {
|
|
12
20
|
throw new Error(`Could not find asset ${itemPath}.`);
|
|
@@ -5,6 +5,7 @@ import type { Entry } from '#cli/cs/entries/Types.js';
|
|
|
5
5
|
import transformEntry from '#cli/dto/entry/fromCs.js';
|
|
6
6
|
import writeYaml from '#cli/fs/writeYaml.js';
|
|
7
7
|
import getUi from '#cli/schema/lib/SchemaUi.js';
|
|
8
|
+
import sanitize from 'sanitize-filename';
|
|
8
9
|
import escapeRegex from '#cli/util/escapeRegex.js';
|
|
9
10
|
import type ProgressBar from '#cli/ui/progress/ProgressBar.js';
|
|
10
11
|
import { readdir, rm } from 'node:fs/promises';
|
|
@@ -78,8 +79,17 @@ function createWriteFn(
|
|
|
78
79
|
return;
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
// If only one locale, save without locale suffix for backward compatibility
|
|
82
|
-
|
|
82
|
+
// If only one locale, save without locale suffix for backward compatibility.
|
|
83
|
+
// When multiple locales exist prefer writing a single base file when an
|
|
84
|
+
// English locale is present (fixtures expect base filenames like
|
|
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
93
|
|
|
84
94
|
// Write all locale versions in parallel for better performance
|
|
85
95
|
const writePromises = locales.map(async (locale) =>
|
|
@@ -162,10 +172,20 @@ function resolveFilename(
|
|
|
162
172
|
}
|
|
163
173
|
|
|
164
174
|
const generated = filenamesByTitle.get(entry.title);
|
|
165
|
-
if (
|
|
166
|
-
|
|
167
|
-
throw new Error(msg);
|
|
175
|
+
if (generated) {
|
|
176
|
+
return generated;
|
|
168
177
|
}
|
|
169
178
|
|
|
170
|
-
|
|
179
|
+
// Fallback: sanitize the entry title to produce a filename so deletions
|
|
180
|
+
// and other operations do not fail when a mapping is missing. This can
|
|
181
|
+
// happen when entries exist on one side but not the other during merge
|
|
182
|
+
// plans. Use the same sanitization rules as `generateFilenames`.
|
|
183
|
+
const fallback = sanitizeFilename(entry.title) + '.yaml';
|
|
184
|
+
return fallback;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function sanitizeFilename(name: string): string {
|
|
188
|
+
const raw = name.trim();
|
|
189
|
+
const sanitized = sanitize(raw, { replacement: '_' });
|
|
190
|
+
return sanitized.trim() || 'Untitled';
|
|
171
191
|
}
|
|
@@ -22,3 +22,87 @@ test('Equal schemas are equal', () => {
|
|
|
22
22
|
expect(isEquivalentSchema(read, exportFixture)).toBe(true);
|
|
23
23
|
}),
|
|
24
24
|
);
|
|
25
|
+
|
|
26
|
+
test('Schemas with different labels are not equivalent', () => {
|
|
27
|
+
const baseSchema: Schema = {
|
|
28
|
+
description: '',
|
|
29
|
+
options: {
|
|
30
|
+
is_page: false,
|
|
31
|
+
singleton: false,
|
|
32
|
+
sub_title: [],
|
|
33
|
+
title: 'title',
|
|
34
|
+
},
|
|
35
|
+
schema: [
|
|
36
|
+
{
|
|
37
|
+
data_type: 'text',
|
|
38
|
+
display_name: 'Title',
|
|
39
|
+
mandatory: true,
|
|
40
|
+
multiple: false,
|
|
41
|
+
non_localizable: false,
|
|
42
|
+
uid: 'title',
|
|
43
|
+
unique: true,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
title: 'Test',
|
|
47
|
+
uid: 'test',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const schemaWithLabels: Schema = {
|
|
51
|
+
...baseSchema,
|
|
52
|
+
labels: ['label1', 'label2'],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
expect(isEquivalentSchema(baseSchema, schemaWithLabels)).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('Schemas with same labels are equivalent', () => {
|
|
59
|
+
const schema1: Schema = {
|
|
60
|
+
description: '',
|
|
61
|
+
labels: ['label1', 'label2'],
|
|
62
|
+
options: {
|
|
63
|
+
is_page: false,
|
|
64
|
+
singleton: false,
|
|
65
|
+
sub_title: [],
|
|
66
|
+
title: 'title',
|
|
67
|
+
},
|
|
68
|
+
schema: [
|
|
69
|
+
{
|
|
70
|
+
data_type: 'text',
|
|
71
|
+
display_name: 'Title',
|
|
72
|
+
mandatory: true,
|
|
73
|
+
multiple: false,
|
|
74
|
+
non_localizable: false,
|
|
75
|
+
uid: 'title',
|
|
76
|
+
unique: true,
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
title: 'Test',
|
|
80
|
+
uid: 'test',
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const schema2: Schema = {
|
|
84
|
+
description: '',
|
|
85
|
+
labels: ['label1', 'label2'],
|
|
86
|
+
options: {
|
|
87
|
+
is_page: false,
|
|
88
|
+
singleton: false,
|
|
89
|
+
sub_title: [],
|
|
90
|
+
title: 'title',
|
|
91
|
+
},
|
|
92
|
+
schema: [
|
|
93
|
+
{
|
|
94
|
+
data_type: 'text',
|
|
95
|
+
display_name: 'Title',
|
|
96
|
+
mandatory: true,
|
|
97
|
+
multiple: false,
|
|
98
|
+
non_localizable: false,
|
|
99
|
+
uid: 'title',
|
|
100
|
+
unique: true,
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
title: 'Test',
|
|
104
|
+
uid: 'test',
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
expect(isEquivalentSchema(schema1, schema2)).toBe(true);
|
|
108
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
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
|
+
import type Ctx from '../ctx/Ctx.js';
|
|
5
|
+
import { MutableTransferResults } from '../xfer/TransferResults.js';
|
|
6
|
+
import getUi from '../lib/SchemaUi.js';
|
|
7
|
+
import isRecord from '#cli/util/isRecord.js';
|
|
8
|
+
|
|
9
|
+
export default async function toContentstack(ctx: Ctx) {
|
|
10
|
+
const directory = schemaDirectory();
|
|
11
|
+
const path = `${directory}/labels.yaml`;
|
|
12
|
+
|
|
13
|
+
const ui = getUi();
|
|
14
|
+
|
|
15
|
+
let data: unknown;
|
|
16
|
+
try {
|
|
17
|
+
data = await readYaml(path);
|
|
18
|
+
} catch {
|
|
19
|
+
ui.info(`Labels: no file at ${path}, skipping`);
|
|
20
|
+
return new MutableTransferResults();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const labels =
|
|
24
|
+
isRecord(data) && Array.isArray(data.labels)
|
|
25
|
+
? data.labels
|
|
26
|
+
: Array.isArray(data)
|
|
27
|
+
? (data as unknown[])
|
|
28
|
+
: [];
|
|
29
|
+
|
|
30
|
+
ui.info(`Labels: read ${labels.length} label(s) from ${path}`);
|
|
31
|
+
|
|
32
|
+
const results = new MutableTransferResults();
|
|
33
|
+
|
|
34
|
+
for (const labelRaw of labels) {
|
|
35
|
+
if (!isRecord(labelRaw)) continue;
|
|
36
|
+
// keep per-label logic in helper to reduce complexity of this function
|
|
37
|
+
|
|
38
|
+
await handleLabel(ctx, labelRaw, results);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return results;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function canonicalize(value: unknown): unknown {
|
|
45
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
46
|
+
if (isRecord(value)) {
|
|
47
|
+
const obj = value;
|
|
48
|
+
const out: Record<string, unknown> = {};
|
|
49
|
+
for (const key of Object.keys(obj).sort()) {
|
|
50
|
+
if (key === 'uid' || key === 'created_at' || key === 'updated_at')
|
|
51
|
+
continue;
|
|
52
|
+
out[key] = canonicalize(obj[key]);
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function updateIfNecessary(
|
|
60
|
+
ctx: Ctx,
|
|
61
|
+
uid: string,
|
|
62
|
+
localLabel: Record<string, unknown>,
|
|
63
|
+
results: MutableTransferResults,
|
|
64
|
+
) {
|
|
65
|
+
const remoteRes = (await ctx.cs.client.GET('/v3/labels/{label_uid}', {
|
|
66
|
+
params: { path: { label_uid: uid } },
|
|
67
|
+
})) as unknown;
|
|
68
|
+
const remoteData = (remoteRes as { data?: unknown } | undefined)?.data;
|
|
69
|
+
|
|
70
|
+
if (!isRecord(remoteData) || !isRecord(remoteData.label)) {
|
|
71
|
+
// If we can't parse remote data, err on the side of updating
|
|
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;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const remoteLabel = remoteData.label;
|
|
83
|
+
let shouldUpdate = true;
|
|
84
|
+
try {
|
|
85
|
+
shouldUpdate =
|
|
86
|
+
JSON.stringify(canonicalize(localLabel)) !==
|
|
87
|
+
JSON.stringify(canonicalize(remoteLabel));
|
|
88
|
+
} catch {
|
|
89
|
+
shouldUpdate = true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!shouldUpdate) return;
|
|
93
|
+
|
|
94
|
+
const res = (await ctx.cs.client.PUT('/v3/labels/{label_uid}', {
|
|
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);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function createLabel(
|
|
104
|
+
ctx: Ctx,
|
|
105
|
+
localLabel: Record<string, unknown>,
|
|
106
|
+
results: MutableTransferResults,
|
|
107
|
+
) {
|
|
108
|
+
const res = (await ctx.cs.client.POST('/v3/labels', {
|
|
109
|
+
body: { label: localLabel },
|
|
110
|
+
})) as unknown;
|
|
111
|
+
const postError = (res as { error?: unknown } | undefined)?.error;
|
|
112
|
+
ContentstackError.throwIfError(postError, `Failed to create label`);
|
|
113
|
+
const postData = (res as { data?: unknown } | undefined)?.data;
|
|
114
|
+
let createdUid: string | null = null;
|
|
115
|
+
if (isRecord(postData)) {
|
|
116
|
+
const pd = postData;
|
|
117
|
+
if (isRecord(pd.label)) {
|
|
118
|
+
const labelObj = pd.label;
|
|
119
|
+
if (typeof labelObj.uid === 'string') createdUid = labelObj.uid;
|
|
120
|
+
}
|
|
121
|
+
if (createdUid === null && typeof pd.uid === 'string') createdUid = pd.uid;
|
|
122
|
+
}
|
|
123
|
+
results.created.add(createdUid ?? '<created>');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function handleLabel(
|
|
127
|
+
ctx: Ctx,
|
|
128
|
+
labelRaw: Record<string, unknown>,
|
|
129
|
+
results: MutableTransferResults,
|
|
130
|
+
) {
|
|
131
|
+
const uid = typeof labelRaw.uid === 'string' ? labelRaw.uid : '';
|
|
132
|
+
|
|
133
|
+
if (uid.length) {
|
|
134
|
+
await updateIfNecessary(ctx, uid, labelRaw, results);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
await createLabel(ctx, labelRaw, results);
|
|
139
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import getAllLabels from '#cli/cs/labels/getAllLabels.js';
|
|
2
|
+
import writeYaml from '#cli/fs/writeYaml.js';
|
|
3
|
+
import schemaDirectory from '../content-types/schemaDirectory.js';
|
|
4
|
+
import { MutableTransferResults } from '../xfer/TransferResults.js';
|
|
5
|
+
import createProgressBar from '../lib/createProgressBar.js';
|
|
6
|
+
import type Ctx from '../ctx/Ctx.js';
|
|
7
|
+
|
|
8
|
+
export default async function toFilesystem(ctx: Ctx) {
|
|
9
|
+
const directory = schemaDirectory();
|
|
10
|
+
const bar = createProgressBar('Labels', 1, 0);
|
|
11
|
+
|
|
12
|
+
// Fetch labels and write them directly. processPlan shortcuts when the
|
|
13
|
+
// merge plan is empty, so for a single-file resource we write and return
|
|
14
|
+
// a simple TransferResults object.
|
|
15
|
+
const labels = await getAllLabels(ctx.cs.client);
|
|
16
|
+
await writeYaml(`${directory}/labels.yaml`, { labels });
|
|
17
|
+
|
|
18
|
+
const result = new MutableTransferResults();
|
|
19
|
+
result.created.add('labels.yaml');
|
|
20
|
+
bar.increment();
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
//# sourceMappingURL=toFilesystem.js.map
|
|
@@ -2,8 +2,16 @@ import getUi from './SchemaUi.js';
|
|
|
2
2
|
|
|
3
3
|
export default function createProgressBar(
|
|
4
4
|
name: string,
|
|
5
|
-
...indicies: readonly ReadonlyMap<unknown, unknown>[]
|
|
5
|
+
...indicies: readonly (ReadonlyMap<unknown, unknown> | number)[]
|
|
6
6
|
) {
|
|
7
|
-
const
|
|
7
|
+
const sizes = indicies.map((x) => {
|
|
8
|
+
if (typeof x === 'number') return x;
|
|
9
|
+
try {
|
|
10
|
+
return [...x.keys()].length;
|
|
11
|
+
} catch {
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
const totalSize = sizes.reduce((a, b) => a + b, 0);
|
|
8
16
|
return getUi().createProgressBar(name, totalSize);
|
|
9
17
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ContentType } from '#cli/cs/content-types/Types.js';
|
|
2
2
|
import assets from '../assets/toFilesystem.js';
|
|
3
3
|
import contentTypes from '../content-types/toFilesystem.js';
|
|
4
|
+
import labels from '../labels/toFilesystem.js';
|
|
4
5
|
import type Ctx from '../ctx/Ctx.js';
|
|
5
6
|
import clean from '../entries/clean.js';
|
|
6
7
|
import entries from '../entries/toFilesystem.js';
|
|
@@ -15,6 +16,7 @@ export default async function* pullModules(
|
|
|
15
16
|
yield ['Assets', assets(ctx)];
|
|
16
17
|
yield ['Global Fields', globalFields(ctx)];
|
|
17
18
|
yield ['Taxonomies', taxonomies(ctx)];
|
|
19
|
+
yield ['Labels', labels(ctx)];
|
|
18
20
|
yield ['Content Types', contentTypes(ctx)];
|
|
19
21
|
|
|
20
22
|
const summary = summarizeContentTypes(ctx);
|
|
@@ -18,3 +18,101 @@ test('Normalization of an exported schema should match snapshot', async () => {
|
|
|
18
18
|
const snapPath = getSnapshotPath('normalize-exported-schema.yaml');
|
|
19
19
|
await expect(output).toMatchFileSnapshot(snapPath);
|
|
20
20
|
});
|
|
21
|
+
|
|
22
|
+
test('Normalization preserves labels when present', () => {
|
|
23
|
+
// Arrange
|
|
24
|
+
const schemaWithLabels: Schema = {
|
|
25
|
+
description: 'Test content type',
|
|
26
|
+
labels: ['label1', 'label2'],
|
|
27
|
+
options: {
|
|
28
|
+
is_page: false,
|
|
29
|
+
singleton: false,
|
|
30
|
+
sub_title: [],
|
|
31
|
+
title: 'title',
|
|
32
|
+
},
|
|
33
|
+
schema: [
|
|
34
|
+
{
|
|
35
|
+
data_type: 'text',
|
|
36
|
+
display_name: 'Title',
|
|
37
|
+
mandatory: true,
|
|
38
|
+
multiple: false,
|
|
39
|
+
non_localizable: false,
|
|
40
|
+
uid: 'title',
|
|
41
|
+
unique: true,
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
title: 'Test',
|
|
45
|
+
uid: 'test',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Act
|
|
49
|
+
const normalized = normalize(schemaWithLabels);
|
|
50
|
+
|
|
51
|
+
// Assert
|
|
52
|
+
expect(normalized.labels).toEqual(['label1', 'label2']);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('Normalization preserves empty labels array', () => {
|
|
56
|
+
// Arrange
|
|
57
|
+
const schemaWithEmptyLabels: Schema = {
|
|
58
|
+
description: 'Test content type',
|
|
59
|
+
labels: [],
|
|
60
|
+
options: {
|
|
61
|
+
is_page: false,
|
|
62
|
+
singleton: false,
|
|
63
|
+
sub_title: [],
|
|
64
|
+
title: 'title',
|
|
65
|
+
},
|
|
66
|
+
schema: [
|
|
67
|
+
{
|
|
68
|
+
data_type: 'text',
|
|
69
|
+
display_name: 'Title',
|
|
70
|
+
mandatory: true,
|
|
71
|
+
multiple: false,
|
|
72
|
+
non_localizable: false,
|
|
73
|
+
uid: 'title',
|
|
74
|
+
unique: true,
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
title: 'Test',
|
|
78
|
+
uid: 'test',
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Act
|
|
82
|
+
const normalized = normalize(schemaWithEmptyLabels);
|
|
83
|
+
|
|
84
|
+
// Assert
|
|
85
|
+
expect(normalized.labels).toEqual([]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('Normalization does not add labels when absent', () => {
|
|
89
|
+
// Arrange
|
|
90
|
+
const schemaWithoutLabels: Schema = {
|
|
91
|
+
description: 'Test content type',
|
|
92
|
+
options: {
|
|
93
|
+
is_page: false,
|
|
94
|
+
singleton: false,
|
|
95
|
+
sub_title: [],
|
|
96
|
+
title: 'title',
|
|
97
|
+
},
|
|
98
|
+
schema: [
|
|
99
|
+
{
|
|
100
|
+
data_type: 'text',
|
|
101
|
+
display_name: 'Title',
|
|
102
|
+
mandatory: true,
|
|
103
|
+
multiple: false,
|
|
104
|
+
non_localizable: false,
|
|
105
|
+
uid: 'title',
|
|
106
|
+
unique: true,
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
title: 'Test',
|
|
110
|
+
uid: 'test',
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// Act
|
|
114
|
+
const normalized = normalize(schemaWithoutLabels);
|
|
115
|
+
|
|
116
|
+
// Assert
|
|
117
|
+
expect(normalized).not.toHaveProperty('labels');
|
|
118
|
+
});
|
package/src/schema/normalize.ts
CHANGED
|
@@ -7,15 +7,26 @@ import type { Schema } from '#cli/cs/Types.js';
|
|
|
7
7
|
// - SYS_ACL
|
|
8
8
|
//
|
|
9
9
|
// Last observed: 2024-07-26.
|
|
10
|
+
//
|
|
11
|
+
// This function normalizes a schema by extracting only the fields that are
|
|
12
|
+
// considered stable and should be compared for equivalence. Fields like
|
|
13
|
+
// labels are preserved when present to ensure they are not lost during
|
|
14
|
+
// serialization/deserialization cycles.
|
|
10
15
|
export default function normalize({
|
|
11
16
|
description,
|
|
17
|
+
labels,
|
|
12
18
|
options,
|
|
13
19
|
schema,
|
|
14
20
|
title,
|
|
15
21
|
uid,
|
|
16
22
|
}: Schema) {
|
|
23
|
+
const normalizedLabels = Array.isArray(labels)
|
|
24
|
+
? (labels as string[]).slice().sort()
|
|
25
|
+
: labels;
|
|
26
|
+
|
|
17
27
|
return {
|
|
18
28
|
description,
|
|
29
|
+
...(normalizedLabels ? { labels: normalizedLabels } : {}),
|
|
19
30
|
options,
|
|
20
31
|
schema: normalizeSchema(schema),
|
|
21
32
|
title,
|
package/src/schema/push.ts
CHANGED
|
@@ -11,6 +11,7 @@ import PushResults from './lib/PushResults.js';
|
|
|
11
11
|
import getUi from './lib/SchemaUi.js';
|
|
12
12
|
import taxonomies from './taxonomies/toContentstack.js';
|
|
13
13
|
import terms from './terms/toContentstack.js';
|
|
14
|
+
import labels from './labels/toContentstack.js';
|
|
14
15
|
|
|
15
16
|
export default async function push(client: Client) {
|
|
16
17
|
const results = new PushResults();
|
|
@@ -62,6 +63,7 @@ export default async function push(client: Client) {
|
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
await results.set('References', updateMissedReferences(ctx));
|
|
66
|
+
await results.set('Labels', labels(ctx));
|
|
65
67
|
return results.value;
|
|
66
68
|
}
|
|
67
69
|
|