@platforma-sdk/block-tools 2.5.92 → 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 +7 -7
- 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
|
@@ -3,11 +3,24 @@ import { RegistryStorage } from '../../io/storage';
|
|
|
3
3
|
import { BlockPackId, BlockPackIdNoVersion, BlockPackManifest } from '@milaboratories/pl-model-middle-layer';
|
|
4
4
|
import { GlobalOverviewReg, PackageOverview } from './schema_public';
|
|
5
5
|
import { RelativeContentReader } from '../model';
|
|
6
|
+
export interface BlockRegistrySettings {
|
|
7
|
+
skipSnapshotCreation?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface GlobalOverviewBackupDescription {
|
|
10
|
+
timestamp: string;
|
|
11
|
+
path: string;
|
|
12
|
+
}
|
|
6
13
|
export declare class BlockRegistryV2 {
|
|
7
14
|
private readonly storage;
|
|
8
15
|
private readonly logger;
|
|
16
|
+
private readonly settings;
|
|
9
17
|
private readonly gzipAsync;
|
|
10
|
-
|
|
18
|
+
private readonly gunzipAsync;
|
|
19
|
+
constructor(storage: RegistryStorage, logger?: MiLogger, settings?: BlockRegistrySettings);
|
|
20
|
+
private generateTimestamp;
|
|
21
|
+
private generatePreWriteTimestamp;
|
|
22
|
+
private createGlobalOverviewSnapshot;
|
|
23
|
+
private createPackageOverviewSnapshot;
|
|
11
24
|
private updateRegistry;
|
|
12
25
|
updateIfNeeded(mode?: 'force' | 'normal' | 'dry-run'): Promise<void>;
|
|
13
26
|
getPackageOverview(name: BlockPackIdNoVersion): Promise<undefined | PackageOverview>;
|
|
@@ -15,5 +28,7 @@ export declare class BlockRegistryV2 {
|
|
|
15
28
|
private marchChanged;
|
|
16
29
|
addPackageToChannel(id: BlockPackId, channel: string): Promise<void>;
|
|
17
30
|
removePackageFromChannel(id: BlockPackId, channel: string): Promise<void>;
|
|
31
|
+
listGlobalOverviewSnapshots(): Promise<GlobalOverviewBackupDescription[]>;
|
|
32
|
+
restoreGlobalOverviewFromSnapshot(backupId: string): Promise<void>;
|
|
18
33
|
publishPackage(manifest: BlockPackManifest, fileReader: RelativeContentReader): Promise<void>;
|
|
19
34
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { BlockPackId, BlockPackIdNoVersion,
|
|
1
|
+
import { BlockPackId, BlockPackIdNoVersion, UpdateSuggestions, SingleBlockPackOverview, BlockPackOverviewNoRegistryId } from '@milaboratories/pl-model-middle-layer';
|
|
2
2
|
import { FolderReader } from '../../io';
|
|
3
3
|
import { BlockComponentsAbsoluteUrl } from '../model';
|
|
4
|
-
export type BlockPackOverviewNoRegLabel = Omit<BlockPackOverview, 'registryId'>;
|
|
5
4
|
export type RegistryV2ReaderOps = {
|
|
6
5
|
/** Number of milliseconds to cache retrieved block list for */
|
|
7
6
|
cacheBlockListFor: number;
|
|
@@ -24,7 +23,7 @@ export declare class RegistryV2Reader {
|
|
|
24
23
|
private embedMetaContent;
|
|
25
24
|
private listCacheTimestamp;
|
|
26
25
|
private listCache;
|
|
27
|
-
listBlockPacks(): Promise<
|
|
26
|
+
listBlockPacks(): Promise<BlockPackOverviewNoRegistryId[]>;
|
|
28
27
|
getLatestOverview(id: BlockPackIdNoVersion, channel: string): Promise<SingleBlockPackOverview | undefined>;
|
|
29
28
|
getUpdateSuggestions(id: BlockPackId, channel: string): Promise<UpdateSuggestions<string> | undefined>;
|
|
30
29
|
getSpecificOverview(id: BlockPackId, channel: string): Promise<SingleBlockPackOverview>;
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import { BlockPackId } from '@milaboratories/pl-model-middle-layer';
|
|
1
|
+
import { BlockPackId, BlockPackIdNoVersion } from '@milaboratories/pl-model-middle-layer';
|
|
2
2
|
export declare const VersionUpdatesPrefix = "_updates_v2/per_package_version/";
|
|
3
3
|
export declare function packageUpdateSeedPath(bp: BlockPackId, seed: string): string;
|
|
4
4
|
export declare const PackageUpdatePattern: RegExp;
|
|
5
5
|
export declare const GlobalUpdateSeedInFile = "_updates_v2/_global_update_in";
|
|
6
6
|
export declare const GlobalUpdateSeedOutFile = "_updates_v2/_global_update_out";
|
|
7
|
+
export declare const OverviewSnapshotsPrefix = "_overview_snapshots_v2/";
|
|
8
|
+
export declare const GlobalSnapshotsPrefix = "_overview_snapshots_v2/global/";
|
|
9
|
+
export declare const PackageSnapshotsPrefix = "_overview_snapshots_v2/per_package/";
|
|
10
|
+
export declare const GlobalOverviewSnapshotPattern: RegExp;
|
|
11
|
+
export declare function globalOverviewSnapshotPath(timestamp: string): string;
|
|
12
|
+
export declare function packageOverviewSnapshotPath(bp: BlockPackIdNoVersion, timestamp: string): string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platforma-sdk/block-tools",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "Utility to manipulate Platforma Blocks and Block Registry",
|
|
5
5
|
"types": "./dist/lib.d.ts",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -33,12 +33,12 @@
|
|
|
33
33
|
"tar": "^7.4.3",
|
|
34
34
|
"yaml": "^2.8.0",
|
|
35
35
|
"zod": "~3.23.8",
|
|
36
|
-
"@milaboratories/pl-model-common": "1.19.14",
|
|
37
|
-
"@milaboratories/pl-model-middle-layer": "1.8.21",
|
|
38
|
-
"@milaboratories/resolve-helper": "1.1.1",
|
|
39
36
|
"@milaboratories/pl-http": "1.1.7",
|
|
37
|
+
"@milaboratories/pl-model-common": "1.19.15",
|
|
38
|
+
"@milaboratories/resolve-helper": "1.1.1",
|
|
40
39
|
"@milaboratories/ts-helpers-oclif": "1.1.27",
|
|
41
|
-
"@milaboratories/ts-helpers": "1.4.6"
|
|
40
|
+
"@milaboratories/ts-helpers": "1.4.6",
|
|
41
|
+
"@milaboratories/pl-model-middle-layer": "1.8.22"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"typescript": "~5.6.3",
|
|
@@ -55,9 +55,9 @@
|
|
|
55
55
|
"@jest/globals": "^29.7.0",
|
|
56
56
|
"oclif": "^4.16.2",
|
|
57
57
|
"ts-jest": "^29.2.6",
|
|
58
|
-
"@milaboratories/
|
|
58
|
+
"@milaboratories/ts-configs": "1.0.6",
|
|
59
59
|
"@milaboratories/oclif-index": "1.1.1",
|
|
60
|
-
"@milaboratories/
|
|
60
|
+
"@milaboratories/build-configs": "1.0.8"
|
|
61
61
|
},
|
|
62
62
|
"oclif": {
|
|
63
63
|
"bin": "block-tools",
|
package/src/cmd/index.ts
CHANGED
|
@@ -2,19 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
import Cmd0 from './build-meta';
|
|
4
4
|
import Cmd1 from './build-model';
|
|
5
|
-
import Cmd3 from './
|
|
6
|
-
import Cmd4 from './
|
|
7
|
-
import Cmd5 from './
|
|
8
|
-
import Cmd6 from './
|
|
9
|
-
import Cmd7 from './
|
|
5
|
+
import Cmd3 from './list-overview-snapshots';
|
|
6
|
+
import Cmd4 from './mark-stable';
|
|
7
|
+
import Cmd5 from './pack';
|
|
8
|
+
import Cmd6 from './publish';
|
|
9
|
+
import Cmd7 from './refresh-registry';
|
|
10
|
+
import Cmd8 from './restore-overview-from-snapshot';
|
|
11
|
+
import Cmd9 from './upload-package-v1';
|
|
10
12
|
|
|
11
13
|
// prettier-ignore
|
|
12
14
|
export const COMMANDS = {
|
|
13
15
|
'build-meta': Cmd0,
|
|
14
16
|
'build-model': Cmd1,
|
|
15
|
-
'
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'
|
|
19
|
-
'
|
|
17
|
+
'list-overview-snapshots': Cmd3,
|
|
18
|
+
'mark-stable': Cmd4,
|
|
19
|
+
'pack': Cmd5,
|
|
20
|
+
'publish': Cmd6,
|
|
21
|
+
'refresh-registry': Cmd7,
|
|
22
|
+
'restore-overview-from-snapshot': Cmd8,
|
|
23
|
+
'upload-package-v1': Cmd9,
|
|
20
24
|
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
|
2
|
+
import { BlockRegistryV2 } from '../v2/registry/registry';
|
|
3
|
+
import { storageByUrl } from '../io/storage';
|
|
4
|
+
import { OclifLoggerAdapter } from '@milaboratories/ts-helpers-oclif';
|
|
5
|
+
|
|
6
|
+
export default class ListOverviewSnapshots extends Command {
|
|
7
|
+
static description = 'List all available global overview snapshots in the registry';
|
|
8
|
+
|
|
9
|
+
static flags = {
|
|
10
|
+
registry: Flags.string({
|
|
11
|
+
char: 'r',
|
|
12
|
+
summary: 'full address of the registry',
|
|
13
|
+
helpValue: '<address>',
|
|
14
|
+
env: 'PL_REGISTRY',
|
|
15
|
+
required: true
|
|
16
|
+
}),
|
|
17
|
+
|
|
18
|
+
json: Flags.boolean({
|
|
19
|
+
summary: 'output in JSON format',
|
|
20
|
+
default: false
|
|
21
|
+
})
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
public async run(): Promise<void> {
|
|
25
|
+
const { flags } = await this.parse(ListOverviewSnapshots);
|
|
26
|
+
const storage = storageByUrl(flags.registry);
|
|
27
|
+
const registry = new BlockRegistryV2(storage, new OclifLoggerAdapter(this));
|
|
28
|
+
|
|
29
|
+
const snapshots = await registry.listGlobalOverviewSnapshots();
|
|
30
|
+
|
|
31
|
+
if (flags.json) {
|
|
32
|
+
this.log(JSON.stringify(snapshots, null, 2));
|
|
33
|
+
} else {
|
|
34
|
+
if (snapshots.length === 0) {
|
|
35
|
+
this.log('No snapshots found.');
|
|
36
|
+
} else {
|
|
37
|
+
this.log(`Found ${snapshots.length} snapshot(s):\n`);
|
|
38
|
+
for (const snapshot of snapshots) {
|
|
39
|
+
this.log(` ${snapshot.timestamp}`);
|
|
40
|
+
this.log(` Path: ${snapshot.path}`);
|
|
41
|
+
this.log('');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
|
2
|
+
import { BlockRegistryV2 } from '../v2/registry/registry';
|
|
3
|
+
import { storageByUrl } from '../io/storage';
|
|
4
|
+
import { OclifLoggerAdapter } from '@milaboratories/ts-helpers-oclif';
|
|
5
|
+
|
|
6
|
+
export default class RestoreOverviewFromSnapshot extends Command {
|
|
7
|
+
static description = 'Restore global overview from a snapshot';
|
|
8
|
+
|
|
9
|
+
static flags = {
|
|
10
|
+
registry: Flags.string({
|
|
11
|
+
char: 'r',
|
|
12
|
+
summary: 'full address of the registry',
|
|
13
|
+
helpValue: '<address>',
|
|
14
|
+
env: 'PL_REGISTRY',
|
|
15
|
+
required: true
|
|
16
|
+
}),
|
|
17
|
+
|
|
18
|
+
snapshot: Flags.string({
|
|
19
|
+
char: 's',
|
|
20
|
+
summary: 'snapshot timestamp ID to restore from',
|
|
21
|
+
helpValue: '<timestamp>',
|
|
22
|
+
required: true
|
|
23
|
+
}),
|
|
24
|
+
|
|
25
|
+
'skip-confirmation': Flags.boolean({
|
|
26
|
+
summary: 'skip confirmation prompt (use with caution)',
|
|
27
|
+
default: false
|
|
28
|
+
})
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
public async run(): Promise<void> {
|
|
32
|
+
const { flags } = await this.parse(RestoreOverviewFromSnapshot);
|
|
33
|
+
const storage = storageByUrl(flags.registry);
|
|
34
|
+
const registry = new BlockRegistryV2(storage, new OclifLoggerAdapter(this));
|
|
35
|
+
|
|
36
|
+
// Check if snapshot exists
|
|
37
|
+
const snapshots = await registry.listGlobalOverviewSnapshots();
|
|
38
|
+
const targetSnapshot = snapshots.find(s => s.timestamp === flags.snapshot);
|
|
39
|
+
|
|
40
|
+
if (!targetSnapshot) {
|
|
41
|
+
this.error(`Snapshot '${flags.snapshot}' not found. Available snapshots:\n${
|
|
42
|
+
snapshots.map(s => ` - ${s.timestamp}`).join('\n') || ' (none)'
|
|
43
|
+
}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Confirmation prompt (unless skipped)
|
|
47
|
+
if (!flags['skip-confirmation']) {
|
|
48
|
+
const readline = await import('node:readline');
|
|
49
|
+
const rl = readline.createInterface({
|
|
50
|
+
input: process.stdin,
|
|
51
|
+
output: process.stdout
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const answer = await new Promise<string>((resolve) => {
|
|
55
|
+
rl.question(
|
|
56
|
+
`⚠️ This will overwrite the current global overview with snapshot '${flags.snapshot}'.\n` +
|
|
57
|
+
`Are you sure you want to continue? (y/N): `,
|
|
58
|
+
resolve
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
rl.close();
|
|
63
|
+
|
|
64
|
+
if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
|
|
65
|
+
this.log('Restore cancelled.');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Perform restore
|
|
71
|
+
try {
|
|
72
|
+
await registry.restoreGlobalOverviewFromSnapshot(flags.snapshot);
|
|
73
|
+
this.log(`✅ Successfully restored global overview from snapshot '${flags.snapshot}'`);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
this.error(`Failed to restore from snapshot: ${error}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -5,8 +5,9 @@ import path from 'path';
|
|
|
5
5
|
import fsp from 'fs/promises';
|
|
6
6
|
import { BlockRegistryV2 } from './registry';
|
|
7
7
|
import semver from 'semver';
|
|
8
|
-
import { UpdateSuggestions } from '@milaboratories/pl-model-middle-layer';
|
|
8
|
+
import { UpdateSuggestions, BlockPackManifest } from '@milaboratories/pl-model-middle-layer';
|
|
9
9
|
import { inferUpdateSuggestions } from './registry_reader';
|
|
10
|
+
import { OverviewSnapshotsPrefix } from './schema_internal';
|
|
10
11
|
|
|
11
12
|
type TestStorageInstance = {
|
|
12
13
|
storage: RegistryStorage;
|
|
@@ -55,11 +56,182 @@ if (testS3Address !== undefined) {
|
|
|
55
56
|
});
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
test.each(testStorages)('
|
|
59
|
+
test.each(testStorages)('registry snapshots test with $name', async ({ storageProvider }) => {
|
|
59
60
|
const { storage, teardown } = storageProvider();
|
|
60
61
|
const registry = new BlockRegistryV2(storage);
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
// Force an update to trigger snapshot creation (even with empty registry)
|
|
65
|
+
await registry.updateIfNeeded('force');
|
|
66
|
+
|
|
67
|
+
// Check that snapshot files actually exist in storage
|
|
68
|
+
const snapshotFiles = await storage.listFiles(OverviewSnapshotsPrefix);
|
|
69
|
+
expect(snapshotFiles.length).toBeGreaterThan(0);
|
|
70
|
+
|
|
71
|
+
// Check that snapshots were created
|
|
72
|
+
const snapshots = await registry.listGlobalOverviewSnapshots();
|
|
73
|
+
expect(snapshots.length).toBeGreaterThan(0);
|
|
74
|
+
expect(snapshots[0]).toHaveProperty('timestamp');
|
|
75
|
+
expect(snapshots[0]).toHaveProperty('path');
|
|
76
|
+
expect(snapshots[0].path).toMatch(/^_overview_snapshots_v2\/global\/.*\.json\.gz$/);
|
|
77
|
+
|
|
78
|
+
// Test that global overview files exist (should be empty initially)
|
|
79
|
+
const globalOverview = await registry.getGlobalOverview();
|
|
80
|
+
expect(globalOverview).toBeDefined();
|
|
81
|
+
expect(globalOverview?.packages).toHaveLength(0);
|
|
82
|
+
|
|
83
|
+
// Test restore functionality
|
|
84
|
+
const snapshotId = snapshots[0].timestamp;
|
|
85
|
+
await registry.restoreGlobalOverviewFromSnapshot(snapshotId);
|
|
86
|
+
|
|
87
|
+
// Verify restored overview is still valid
|
|
88
|
+
const restoredOverview = await registry.getGlobalOverview();
|
|
89
|
+
expect(restoredOverview).toBeDefined();
|
|
90
|
+
expect(restoredOverview?.packages).toHaveLength(0);
|
|
91
|
+
|
|
92
|
+
} finally {
|
|
93
|
+
await teardown();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test.each(testStorages)('registry snapshots disabled test with $name', async ({ storageProvider }) => {
|
|
98
|
+
const { storage, teardown } = storageProvider();
|
|
99
|
+
const registry = new BlockRegistryV2(storage, undefined, { skipSnapshotCreation: true });
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
// Force an update which would normally create snapshots
|
|
103
|
+
await registry.updateIfNeeded('force');
|
|
104
|
+
|
|
105
|
+
// Check that no snapshots were created
|
|
106
|
+
const snapshots = await registry.listGlobalOverviewSnapshots();
|
|
107
|
+
expect(snapshots).toHaveLength(0);
|
|
108
|
+
|
|
109
|
+
const snapshotFiles = await storage.listFiles(OverviewSnapshotsPrefix);
|
|
110
|
+
expect(snapshotFiles).toHaveLength(0);
|
|
111
|
+
|
|
112
|
+
} finally {
|
|
113
|
+
await teardown();
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test.each(testStorages)('force mode removes deleted packages and versions with $name', async ({ storageProvider }) => {
|
|
118
|
+
const { storage, teardown } = storageProvider();
|
|
119
|
+
const registry = new BlockRegistryV2(storage);
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
// Create mock manifests for testing
|
|
123
|
+
const createMockManifest = (org: string, name: string, version: string): BlockPackManifest => ({
|
|
124
|
+
schema: 'v2',
|
|
125
|
+
description: {
|
|
126
|
+
id: { organization: org, name: name, version: version },
|
|
127
|
+
title: `Test ${name}`,
|
|
128
|
+
summary: 'Test package',
|
|
129
|
+
components: {
|
|
130
|
+
workflow: { type: 'workflow-v1', main: { type: 'relative', path: 'workflow.json' } },
|
|
131
|
+
model: { type: 'relative', path: 'model.json' },
|
|
132
|
+
ui: { type: 'relative', path: 'ui.json' }
|
|
133
|
+
},
|
|
134
|
+
meta: {
|
|
135
|
+
title: `Test ${name}`,
|
|
136
|
+
description: 'Test package description',
|
|
137
|
+
organization: {
|
|
138
|
+
name: 'Test Organization',
|
|
139
|
+
url: 'https://test.com'
|
|
140
|
+
},
|
|
141
|
+
tags: []
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
files: [
|
|
145
|
+
{
|
|
146
|
+
name: 'model.json',
|
|
147
|
+
size: 13,
|
|
148
|
+
sha256: '6FD977DB9B2AFE87A9CEEE48432881299A6AAF83D935FBBE83007660287F9C2E'
|
|
149
|
+
}
|
|
150
|
+
]
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const mockFileReader = async (fileName: string) => {
|
|
154
|
+
if (fileName === 'model.json') {
|
|
155
|
+
return Buffer.from('{"test":true}');
|
|
156
|
+
}
|
|
157
|
+
throw new Error(`Unknown file: ${fileName}`);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// 1. Publish multiple packages and versions
|
|
161
|
+
const pkg1v1 = createMockManifest('testorg', 'pkg1', '1.0.0');
|
|
162
|
+
const pkg1v2 = createMockManifest('testorg', 'pkg1', '2.0.0');
|
|
163
|
+
const pkg2v1 = createMockManifest('testorg', 'pkg2', '1.0.0');
|
|
164
|
+
const pkg3v1 = createMockManifest('anotherorg', 'pkg3', '1.0.0');
|
|
165
|
+
|
|
166
|
+
await registry.publishPackage(pkg1v1, mockFileReader);
|
|
167
|
+
await registry.publishPackage(pkg1v2, mockFileReader);
|
|
168
|
+
await registry.publishPackage(pkg2v1, mockFileReader);
|
|
169
|
+
await registry.publishPackage(pkg3v1, mockFileReader);
|
|
170
|
+
|
|
171
|
+
// Update registry to create overviews
|
|
172
|
+
await registry.updateIfNeeded('normal');
|
|
173
|
+
|
|
174
|
+
// Verify initial state
|
|
175
|
+
let globalOverview = await registry.getGlobalOverview();
|
|
176
|
+
expect(globalOverview?.packages).toHaveLength(3); // testorg:pkg1, testorg:pkg2, anotherorg:pkg3
|
|
177
|
+
|
|
178
|
+
let pkg1Overview = await registry.getPackageOverview({ organization: 'testorg', name: 'pkg1' });
|
|
179
|
+
expect(pkg1Overview?.versions).toHaveLength(2); // v1.0.0 and v2.0.0
|
|
180
|
+
|
|
181
|
+
let pkg2Overview = await registry.getPackageOverview({ organization: 'testorg', name: 'pkg2' });
|
|
182
|
+
expect(pkg2Overview?.versions).toHaveLength(1); // v1.0.0
|
|
183
|
+
|
|
184
|
+
let pkg3Overview = await registry.getPackageOverview({ organization: 'anotherorg', name: 'pkg3' });
|
|
185
|
+
expect(pkg3Overview?.versions).toHaveLength(1); // v1.0.0
|
|
186
|
+
|
|
187
|
+
// 2. Manually delete some packages/versions from storage (simulating external deletion)
|
|
188
|
+
// Delete pkg1 v1.0.0
|
|
189
|
+
await storage.deleteFiles('v2/testorg/pkg1/1.0.0/manifest.json', 'v2/testorg/pkg1/1.0.0/model.json');
|
|
190
|
+
|
|
191
|
+
// Delete entire pkg2
|
|
192
|
+
await storage.deleteFiles('v2/testorg/pkg2/1.0.0/manifest.json', 'v2/testorg/pkg2/1.0.0/model.json');
|
|
193
|
+
|
|
194
|
+
// Leave pkg1 v2.0.0 and pkg3 v1.0.0 intact
|
|
195
|
+
|
|
196
|
+
// 3. Count snapshots before force mode
|
|
197
|
+
const initialSnapshots = await storage.listFiles('_overview_snapshots_v2/');
|
|
198
|
+
|
|
199
|
+
// 4. Run force mode - should create pre-write snapshots and rebuild from scratch
|
|
200
|
+
await registry.updateIfNeeded('force');
|
|
201
|
+
|
|
202
|
+
// 5. Verify pre-write snapshots were created
|
|
203
|
+
const finalSnapshots = await storage.listFiles('_overview_snapshots_v2/');
|
|
204
|
+
expect(finalSnapshots.length).toBeGreaterThan(initialSnapshots.length);
|
|
205
|
+
|
|
206
|
+
// Check for pre-write snapshots (should contain "-prewrite-" in filename)
|
|
207
|
+
const preWriteSnapshots = finalSnapshots.filter(s => s.includes('-prewrite-'));
|
|
208
|
+
expect(preWriteSnapshots.length).toBeGreaterThan(0);
|
|
209
|
+
|
|
210
|
+
// 6. Verify overviews now only reflect what exists in storage
|
|
211
|
+
globalOverview = await registry.getGlobalOverview();
|
|
212
|
+
expect(globalOverview?.packages).toHaveLength(2); // Only testorg:pkg1 and anotherorg:pkg3 should remain
|
|
213
|
+
|
|
214
|
+
const remainingPackageNames = globalOverview?.packages.map(p => `${p.id.organization}:${p.id.name}`).sort();
|
|
215
|
+
expect(remainingPackageNames).toEqual(['anotherorg:pkg3', 'testorg:pkg1']);
|
|
216
|
+
|
|
217
|
+
// 7. Verify pkg1 now only has v2.0.0
|
|
218
|
+
pkg1Overview = await registry.getPackageOverview({ organization: 'testorg', name: 'pkg1' });
|
|
219
|
+
expect(pkg1Overview?.versions).toHaveLength(1);
|
|
220
|
+
expect(pkg1Overview?.versions[0].description.id.version).toBe('2.0.0');
|
|
221
|
+
|
|
222
|
+
// 8. Verify pkg2 overview is unchanged (since pkg2 was completely deleted,
|
|
223
|
+
// force mode doesn't process it, so the old overview file remains)
|
|
224
|
+
pkg2Overview = await registry.getPackageOverview({ organization: 'testorg', name: 'pkg2' });
|
|
225
|
+
expect(pkg2Overview?.versions).toHaveLength(1); // Old overview remains
|
|
226
|
+
|
|
227
|
+
// 9. Verify pkg3 is unchanged
|
|
228
|
+
pkg3Overview = await registry.getPackageOverview({ organization: 'anotherorg', name: 'pkg3' });
|
|
229
|
+
expect(pkg3Overview?.versions).toHaveLength(1);
|
|
230
|
+
expect(pkg3Overview?.versions[0].description.id.version).toBe('1.0.0');
|
|
231
|
+
|
|
232
|
+
} finally {
|
|
233
|
+
await teardown();
|
|
234
|
+
}
|
|
63
235
|
});
|
|
64
236
|
|
|
65
237
|
test.each([
|