@platforma-sdk/block-tools 2.5.91 → 2.6.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/README.md +38 -2
- package/dist/cli.js +5 -1
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +208 -129
- package/dist/cli.mjs.map +1 -1
- package/dist/cmd/index.d.ts +14 -10
- package/dist/cmd/list-overview-snapshots.d.ts +9 -0
- package/dist/cmd/restore-overview-from-snapshot.d.ts +10 -0
- package/dist/config-DjpRXRy9.js +3 -0
- package/dist/config-DjpRXRy9.js.map +1 -0
- package/dist/config-XBQ2O39y.mjs +2020 -0
- package/dist/config-XBQ2O39y.mjs.map +1 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/dist/registry_v1/config_schema.d.ts +5 -5
- package/dist/v2/model/block_description.d.ts +7329 -1422
- package/dist/v2/model/block_meta.d.ts +481 -88
- package/dist/v2/registry/registry.d.ts +16 -1
- package/dist/v2/registry/registry_reader.d.ts +2 -3
- package/dist/v2/registry/schema_internal.d.ts +7 -1
- package/package.json +5 -5
- package/src/cmd/index.ts +14 -10
- package/src/cmd/list-overview-snapshots.ts +46 -0
- package/src/cmd/restore-overview-from-snapshot.ts +78 -0
- package/src/v2/registry/registry.test.ts +176 -4
- package/src/v2/registry/registry.ts +146 -13
- package/src/v2/registry/registry_reader.ts +5 -6
- package/src/v2/registry/schema_internal.ts +16 -1
- package/src/v2/registry/schema_public.ts +5 -5
- package/dist/config-VnABe7ki.mjs +0 -1951
- package/dist/config-VnABe7ki.mjs.map +0 -1
- package/dist/config-t7F2nAAr.js +0 -3
- package/dist/config-t7F2nAAr.js.map +0 -1
- package/dist/v2/registry/schema_public.d.ts +0 -18475
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ConsoleLoggerAdapter, MiLogger } from '@milaboratories/ts-helpers';
|
|
2
2
|
import { compare as compareSemver, satisfies } from 'semver';
|
|
3
|
-
import { gzip } from 'node:zlib';
|
|
3
|
+
import { gzip, gunzip } from 'node:zlib';
|
|
4
4
|
import { promisify } from 'node:util';
|
|
5
5
|
import { RegistryStorage } from '../../io/storage';
|
|
6
6
|
import {
|
|
@@ -15,7 +15,13 @@ import {
|
|
|
15
15
|
GlobalUpdateSeedOutFile,
|
|
16
16
|
PackageUpdatePattern,
|
|
17
17
|
packageUpdateSeedPath,
|
|
18
|
-
VersionUpdatesPrefix
|
|
18
|
+
VersionUpdatesPrefix,
|
|
19
|
+
GlobalOverviewSnapshotPattern,
|
|
20
|
+
OverviewSnapshotsPrefix,
|
|
21
|
+
GlobalSnapshotsPrefix,
|
|
22
|
+
PackageSnapshotsPrefix,
|
|
23
|
+
globalOverviewSnapshotPath,
|
|
24
|
+
packageOverviewSnapshotPath
|
|
19
25
|
} from './schema_internal';
|
|
20
26
|
import {
|
|
21
27
|
GlobalOverviewReg,
|
|
@@ -36,6 +42,15 @@ import { BlockPackDescriptionManifestAddRelativePathPrefix, RelativeContentReade
|
|
|
36
42
|
import { randomUUID } from 'node:crypto';
|
|
37
43
|
import { calculateSha256 } from '../../util';
|
|
38
44
|
|
|
45
|
+
export interface BlockRegistrySettings {
|
|
46
|
+
skipSnapshotCreation?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface GlobalOverviewBackupDescription {
|
|
50
|
+
timestamp: string;
|
|
51
|
+
path: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
39
54
|
type PackageUpdateInfo = {
|
|
40
55
|
package: BlockPackIdNoVersion;
|
|
41
56
|
versions: Set<String>;
|
|
@@ -43,15 +58,59 @@ type PackageUpdateInfo = {
|
|
|
43
58
|
|
|
44
59
|
export class BlockRegistryV2 {
|
|
45
60
|
private readonly gzipAsync = promisify(gzip);
|
|
61
|
+
private readonly gunzipAsync = promisify(gunzip);
|
|
46
62
|
|
|
47
63
|
constructor(
|
|
48
64
|
private readonly storage: RegistryStorage,
|
|
49
|
-
private readonly logger: MiLogger = new ConsoleLoggerAdapter()
|
|
65
|
+
private readonly logger: MiLogger = new ConsoleLoggerAdapter(),
|
|
66
|
+
private readonly settings: BlockRegistrySettings = {}
|
|
50
67
|
) {}
|
|
51
68
|
|
|
69
|
+
private generateTimestamp(): string {
|
|
70
|
+
const timestamp = new Date().toISOString().replace(/:/g, '-').replace(/\.(\d{3})Z$/, '.$1Z');
|
|
71
|
+
const randomSuffix = Math.random().toString(36).substring(2, 6);
|
|
72
|
+
return `${timestamp}-${randomSuffix}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private generatePreWriteTimestamp(): string {
|
|
76
|
+
const timestamp = new Date().toISOString().replace(/:/g, '-').replace(/\.(\d{3})Z$/, '.$1Z');
|
|
77
|
+
const randomSuffix = Math.random().toString(36).substring(2, 6);
|
|
78
|
+
return `${timestamp}-prewrite-${randomSuffix}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private async createGlobalOverviewSnapshot(overviewData: string, timestamp: string): Promise<void> {
|
|
82
|
+
if (this.settings.skipSnapshotCreation) return;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const gzippedData = await this.gzipAsync(overviewData);
|
|
86
|
+
const snapshotPath = globalOverviewSnapshotPath(timestamp);
|
|
87
|
+
await this.storage.putFile(snapshotPath, Buffer.from(gzippedData));
|
|
88
|
+
this.logger.info(`Global overview snapshot created at ${snapshotPath}`);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
this.logger.warn(`Failed to create global overview snapshot: ${error}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private async createPackageOverviewSnapshot(pkg: BlockPackIdNoVersion, overview: PackageOverview, timestamp: string): Promise<void> {
|
|
95
|
+
if (this.settings.skipSnapshotCreation) return;
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const overviewData = JSON.stringify(overview);
|
|
99
|
+
const gzippedData = await this.gzipAsync(overviewData);
|
|
100
|
+
const snapshotPath = packageOverviewSnapshotPath(pkg, timestamp);
|
|
101
|
+
await this.storage.putFile(snapshotPath, Buffer.from(gzippedData));
|
|
102
|
+
this.logger.info(`Package overview snapshot created at ${snapshotPath} for ${pkg.organization}:${pkg.name}`);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
this.logger.warn(`Failed to create package overview snapshot for ${pkg.organization}:${pkg.name}: ${error}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
52
108
|
private async updateRegistry(mode: 'force' | 'normal' | 'dry-run' = 'normal') {
|
|
53
109
|
this.logger.info('Initiating registry refresh...');
|
|
54
110
|
|
|
111
|
+
// Generate timestamp for all snapshots in this run
|
|
112
|
+
const snapshotTimestamp = this.generateTimestamp();
|
|
113
|
+
|
|
55
114
|
// reading update requests
|
|
56
115
|
const packagesToUpdate = new Map<string, PackageUpdateInfo>();
|
|
57
116
|
const seedPaths: string[] = [];
|
|
@@ -97,24 +156,41 @@ export class BlockRegistryV2 {
|
|
|
97
156
|
|
|
98
157
|
// loading global overview
|
|
99
158
|
const overviewContent = await this.storage.getFile(GlobalOverviewPath);
|
|
100
|
-
|
|
101
|
-
|
|
159
|
+
|
|
160
|
+
// Create pre-write snapshot in force mode if overview exists
|
|
161
|
+
if (mode === 'force' && overviewContent !== undefined) {
|
|
162
|
+
const preWriteTimestamp = this.generatePreWriteTimestamp();
|
|
163
|
+
await this.createGlobalOverviewSnapshot(overviewContent.toString(), preWriteTimestamp);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const overview: GlobalOverviewReg = mode === 'force'
|
|
167
|
+
? { schema: 'v2', packages: [] }
|
|
168
|
+
: overviewContent === undefined
|
|
102
169
|
? { schema: 'v2', packages: [] }
|
|
103
170
|
: GlobalOverviewReg.parse(JSON.parse(overviewContent.toString()));
|
|
104
171
|
let overviewPackages = overview.packages;
|
|
105
|
-
this.logger.info(`Global overview loaded, ${overviewPackages.length} records`);
|
|
172
|
+
this.logger.info(`Global overview ${mode === 'force' ? 'starting empty (force mode)' : 'loaded'}, ${overviewPackages.length} records`);
|
|
106
173
|
|
|
107
174
|
// updating packages
|
|
108
175
|
for (const [, packageInfo] of packagesToUpdate.entries()) {
|
|
109
176
|
// reading existing overview
|
|
110
177
|
const overviewFile = packageOverviewPath(packageInfo.package);
|
|
111
178
|
const pOverviewContent = await this.storage.getFile(overviewFile);
|
|
112
|
-
|
|
113
|
-
|
|
179
|
+
|
|
180
|
+
// Create pre-write snapshot in force mode if package overview exists
|
|
181
|
+
if (mode === 'force' && pOverviewContent !== undefined) {
|
|
182
|
+
const preWriteTimestamp = this.generatePreWriteTimestamp();
|
|
183
|
+
const existingOverview = PackageOverview.parse(JSON.parse(pOverviewContent.toString()));
|
|
184
|
+
await this.createPackageOverviewSnapshot(packageInfo.package, existingOverview, preWriteTimestamp);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const packageOverview: PackageOverview = mode === 'force'
|
|
188
|
+
? { schema: 'v2', versions: [] }
|
|
189
|
+
: pOverviewContent === undefined
|
|
114
190
|
? { schema: 'v2', versions: [] }
|
|
115
191
|
: PackageOverview.parse(JSON.parse(pOverviewContent.toString()));
|
|
116
192
|
this.logger.info(
|
|
117
|
-
`Updating ${packageInfo.package.organization}:${packageInfo.package.name} overview, ${packageOverview.versions.length} records`
|
|
193
|
+
`Updating ${packageInfo.package.organization}:${packageInfo.package.name} overview${mode === 'force' ? ' (starting empty in force mode)' : ''}, ${packageOverview.versions.length} records`
|
|
118
194
|
);
|
|
119
195
|
|
|
120
196
|
// removing versions that we will update
|
|
@@ -158,13 +234,16 @@ export class BlockRegistryV2 {
|
|
|
158
234
|
);
|
|
159
235
|
|
|
160
236
|
// write package overview back
|
|
161
|
-
|
|
237
|
+
const packageOverviewData = { schema: 'v2', versions: newVersions } satisfies PackageOverview;
|
|
238
|
+
if (mode !== 'dry-run') {
|
|
162
239
|
await this.storage.putFile(
|
|
163
240
|
overviewFile,
|
|
164
|
-
Buffer.from(
|
|
165
|
-
JSON.stringify({ schema: 'v2', versions: newVersions } satisfies PackageOverview)
|
|
166
|
-
)
|
|
241
|
+
Buffer.from(JSON.stringify(packageOverviewData))
|
|
167
242
|
);
|
|
243
|
+
|
|
244
|
+
// Create snapshot after successful write
|
|
245
|
+
await this.createPackageOverviewSnapshot(packageInfo.package, packageOverviewData, snapshotTimestamp);
|
|
246
|
+
}
|
|
168
247
|
this.logger.info(`Done (${newVersions.length} records)`);
|
|
169
248
|
|
|
170
249
|
// calculating all channels
|
|
@@ -222,6 +301,9 @@ export class BlockRegistryV2 {
|
|
|
222
301
|
// Write gzipped overview file
|
|
223
302
|
const gzippedBuffer = await this.gzipAsync(overviewData);
|
|
224
303
|
await this.storage.putFile(GlobalOverviewGzPath, Buffer.from(gzippedBuffer));
|
|
304
|
+
|
|
305
|
+
// Create snapshot after successful writes
|
|
306
|
+
await this.createGlobalOverviewSnapshot(overviewData, snapshotTimestamp);
|
|
225
307
|
}
|
|
226
308
|
this.logger.info(`Global overview updated (${overviewPackages.length} records)`);
|
|
227
309
|
|
|
@@ -310,6 +392,57 @@ export class BlockRegistryV2 {
|
|
|
310
392
|
await this.marchChanged(id);
|
|
311
393
|
}
|
|
312
394
|
|
|
395
|
+
public async listGlobalOverviewSnapshots(): Promise<GlobalOverviewBackupDescription[]> {
|
|
396
|
+
const snapshotPaths = await this.storage.listFiles(GlobalSnapshotsPrefix);
|
|
397
|
+
const snapshots: GlobalOverviewBackupDescription[] = [];
|
|
398
|
+
|
|
399
|
+
for (const path of snapshotPaths) {
|
|
400
|
+
// Extract filename from path
|
|
401
|
+
const filename = path.indexOf('/') === -1 ? path : path.substring(path.lastIndexOf('/') + 1);
|
|
402
|
+
|
|
403
|
+
const match = filename.match(GlobalOverviewSnapshotPattern);
|
|
404
|
+
if (match) {
|
|
405
|
+
snapshots.push({
|
|
406
|
+
timestamp: match.groups!.timestamp,
|
|
407
|
+
path: GlobalSnapshotsPrefix + filename
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Sort by timestamp descending (newest first)
|
|
413
|
+
snapshots.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
414
|
+
return snapshots;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
public async restoreGlobalOverviewFromSnapshot(backupId: string): Promise<void> {
|
|
418
|
+
const snapshotPath = globalOverviewSnapshotPath(backupId);
|
|
419
|
+
|
|
420
|
+
// Read and decompress the snapshot
|
|
421
|
+
const snapshotData = await this.storage.getFile(snapshotPath);
|
|
422
|
+
if (!snapshotData) {
|
|
423
|
+
throw new Error(`Snapshot ${backupId} not found at ${snapshotPath}`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const decompressedData = await this.gunzipAsync(snapshotData);
|
|
427
|
+
const overviewData = decompressedData.toString('utf8');
|
|
428
|
+
|
|
429
|
+
// Validate the data
|
|
430
|
+
try {
|
|
431
|
+
GlobalOverviewReg.parse(JSON.parse(overviewData));
|
|
432
|
+
} catch (error) {
|
|
433
|
+
throw new Error(`Invalid snapshot data in ${backupId}: ${error}`);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Write both regular and gzipped versions
|
|
437
|
+
const overviewBuffer = Buffer.from(overviewData);
|
|
438
|
+
await this.storage.putFile(GlobalOverviewPath, overviewBuffer);
|
|
439
|
+
|
|
440
|
+
const gzippedBuffer = await this.gzipAsync(overviewData);
|
|
441
|
+
await this.storage.putFile(GlobalOverviewGzPath, Buffer.from(gzippedBuffer));
|
|
442
|
+
|
|
443
|
+
this.logger.info(`Global overview restored from snapshot ${backupId}`);
|
|
444
|
+
}
|
|
445
|
+
|
|
313
446
|
public async publishPackage(
|
|
314
447
|
manifest: BlockPackManifest,
|
|
315
448
|
fileReader: RelativeContentReader
|
|
@@ -8,7 +8,8 @@ import {
|
|
|
8
8
|
BlockPackOverview,
|
|
9
9
|
UpdateSuggestions,
|
|
10
10
|
SingleBlockPackOverview,
|
|
11
|
-
AnyChannel
|
|
11
|
+
AnyChannel,
|
|
12
|
+
BlockPackOverviewNoRegistryId
|
|
12
13
|
} from '@milaboratories/pl-model-middle-layer';
|
|
13
14
|
import { FolderReader } from '../../io';
|
|
14
15
|
import canonicalize from 'canonicalize';
|
|
@@ -26,8 +27,6 @@ import semver from 'semver';
|
|
|
26
27
|
import { calculateSha256 } from '../../util';
|
|
27
28
|
import { retry, Retry2TimesWithDelay } from '@milaboratories/ts-helpers';
|
|
28
29
|
|
|
29
|
-
export type BlockPackOverviewNoRegLabel = Omit<BlockPackOverview, 'registryId'>;
|
|
30
|
-
|
|
31
30
|
export type RegistryV2ReaderOps = {
|
|
32
31
|
/** Number of milliseconds to cache retrieved block list for */
|
|
33
32
|
cacheBlockListFor: number;
|
|
@@ -110,9 +109,9 @@ export class RegistryV2Reader {
|
|
|
110
109
|
}
|
|
111
110
|
|
|
112
111
|
private listCacheTimestamp: number = 0;
|
|
113
|
-
private listCache:
|
|
112
|
+
private listCache: BlockPackOverviewNoRegistryId[] | undefined = undefined;
|
|
114
113
|
|
|
115
|
-
public async listBlockPacks(): Promise<
|
|
114
|
+
public async listBlockPacks(): Promise<BlockPackOverviewNoRegistryId[]> {
|
|
116
115
|
if (
|
|
117
116
|
this.listCache !== undefined &&
|
|
118
117
|
Date.now() - this.listCacheTimestamp <= this.ops.cacheBlockListFor
|
|
@@ -154,7 +153,7 @@ export class RegistryV2Reader {
|
|
|
154
153
|
id: p.id,
|
|
155
154
|
latestByChannel: Object.fromEntries(byChannelEntries),
|
|
156
155
|
allVersions: p.allVersionsWithChannels
|
|
157
|
-
} satisfies
|
|
156
|
+
} satisfies BlockPackOverviewNoRegistryId;
|
|
158
157
|
})
|
|
159
158
|
);
|
|
160
159
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BlockPackId } from '@milaboratories/pl-model-middle-layer';
|
|
1
|
+
import { BlockPackId, BlockPackIdNoVersion } from '@milaboratories/pl-model-middle-layer';
|
|
2
2
|
|
|
3
3
|
export const VersionUpdatesPrefix = '_updates_v2/per_package_version/';
|
|
4
4
|
|
|
@@ -11,3 +11,18 @@ export const PackageUpdatePattern =
|
|
|
11
11
|
|
|
12
12
|
export const GlobalUpdateSeedInFile = '_updates_v2/_global_update_in';
|
|
13
13
|
export const GlobalUpdateSeedOutFile = '_updates_v2/_global_update_out';
|
|
14
|
+
|
|
15
|
+
// Snapshot storage structure
|
|
16
|
+
export const OverviewSnapshotsPrefix = '_overview_snapshots_v2/';
|
|
17
|
+
export const GlobalSnapshotsPrefix = '_overview_snapshots_v2/global/';
|
|
18
|
+
export const PackageSnapshotsPrefix = '_overview_snapshots_v2/per_package/';
|
|
19
|
+
|
|
20
|
+
export const GlobalOverviewSnapshotPattern = /^(?<timestamp>\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}\.\d{3}Z-[a-z0-9]+)\.json\.gz$/;
|
|
21
|
+
|
|
22
|
+
export function globalOverviewSnapshotPath(timestamp: string): string {
|
|
23
|
+
return `${GlobalSnapshotsPrefix}${timestamp}.json.gz`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function packageOverviewSnapshotPath(bp: BlockPackIdNoVersion, timestamp: string): string {
|
|
27
|
+
return `${PackageSnapshotsPrefix}${bp.organization}/${bp.name}/${timestamp}.json.gz`;
|
|
28
|
+
}
|
|
@@ -44,13 +44,13 @@ export const PackageOverviewVersionEntry = z.object({
|
|
|
44
44
|
description: BlockPackDescriptionManifest,
|
|
45
45
|
channels: z.array(z.string()).default(() => []),
|
|
46
46
|
manifestSha256: Sha256Schema
|
|
47
|
-
});
|
|
47
|
+
}).passthrough();
|
|
48
48
|
export type PackageOverviewVersionEntry = z.infer<typeof PackageOverviewVersionEntry>;
|
|
49
49
|
|
|
50
50
|
export const PackageOverview = z.object({
|
|
51
51
|
schema: z.literal('v2'),
|
|
52
52
|
versions: z.array(PackageOverviewVersionEntry)
|
|
53
|
-
});
|
|
53
|
+
}).passthrough();
|
|
54
54
|
export type PackageOverview = z.infer<typeof PackageOverview>;
|
|
55
55
|
|
|
56
56
|
export function packageOverviewPathInsideV2(bp: BlockPackIdNoVersion): string {
|
|
@@ -93,10 +93,10 @@ export function GlobalOverviewEntry<const Description extends z.ZodTypeAny>(
|
|
|
93
93
|
z.object({
|
|
94
94
|
description: descriptionType,
|
|
95
95
|
manifestSha256: Sha256Schema
|
|
96
|
-
})
|
|
96
|
+
}).passthrough()
|
|
97
97
|
)
|
|
98
98
|
.default({})
|
|
99
|
-
});
|
|
99
|
+
}).passthrough();
|
|
100
100
|
return (
|
|
101
101
|
universalSchema
|
|
102
102
|
.transform((o) => {
|
|
@@ -131,7 +131,7 @@ export function GlobalOverview<const Description extends z.ZodTypeAny>(
|
|
|
131
131
|
return z.object({
|
|
132
132
|
schema: z.literal('v2'),
|
|
133
133
|
packages: z.array(GlobalOverviewEntry(descriptionType))
|
|
134
|
-
});
|
|
134
|
+
}).passthrough();
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
export const GlobalOverviewReg = GlobalOverview(BlockPackDescriptionManifest);
|