@platforma-sdk/block-tools 2.5.92 → 2.6.1

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.
Files changed (36) hide show
  1. package/README.md +38 -2
  2. package/dist/cli.js +5 -1
  3. package/dist/cli.js.map +1 -1
  4. package/dist/cli.mjs +208 -129
  5. package/dist/cli.mjs.map +1 -1
  6. package/dist/cmd/index.d.ts +14 -10
  7. package/dist/cmd/list-overview-snapshots.d.ts +9 -0
  8. package/dist/cmd/restore-overview-from-snapshot.d.ts +10 -0
  9. package/dist/config-DjpRXRy9.js +3 -0
  10. package/dist/config-DjpRXRy9.js.map +1 -0
  11. package/dist/config-XBQ2O39y.mjs +2020 -0
  12. package/dist/config-XBQ2O39y.mjs.map +1 -0
  13. package/dist/index.js +1 -1
  14. package/dist/index.js.map +1 -1
  15. package/dist/index.mjs +2 -2
  16. package/dist/index.mjs.map +1 -1
  17. package/dist/registry_v1/config_schema.d.ts +5 -5
  18. package/dist/v2/model/block_description.d.ts +7329 -1422
  19. package/dist/v2/model/block_meta.d.ts +481 -88
  20. package/dist/v2/registry/registry.d.ts +16 -1
  21. package/dist/v2/registry/registry_reader.d.ts +2 -3
  22. package/dist/v2/registry/schema_internal.d.ts +7 -1
  23. package/package.json +7 -7
  24. package/src/cmd/index.ts +14 -10
  25. package/src/cmd/list-overview-snapshots.ts +46 -0
  26. package/src/cmd/restore-overview-from-snapshot.ts +78 -0
  27. package/src/v2/registry/registry.test.ts +176 -4
  28. package/src/v2/registry/registry.ts +146 -13
  29. package/src/v2/registry/registry_reader.ts +5 -6
  30. package/src/v2/registry/schema_internal.ts +16 -1
  31. package/src/v2/registry/schema_public.ts +5 -5
  32. package/dist/config-VnABe7ki.mjs +0 -1951
  33. package/dist/config-VnABe7ki.mjs.map +0 -1
  34. package/dist/config-t7F2nAAr.js +0 -3
  35. package/dist/config-t7F2nAAr.js.map +0 -1
  36. 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
- const overview: GlobalOverviewReg =
101
- overviewContent === undefined
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
- const packageOverview: PackageOverview =
113
- pOverviewContent === undefined
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
- if (mode !== 'dry-run')
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: BlockPackOverviewNoRegLabel[] | undefined = undefined;
112
+ private listCache: BlockPackOverviewNoRegistryId[] | undefined = undefined;
114
113
 
115
- public async listBlockPacks(): Promise<BlockPackOverviewNoRegLabel[]> {
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 BlockPackOverviewNoRegLabel;
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);