@platforma-sdk/block-tools 2.1.5 → 2.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/cli.js +1 -756
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +187 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/cmd/build-meta.d.ts +10 -0
- package/dist/cmd/build-meta.d.ts.map +1 -0
- package/dist/cmd/build-model.d.ts +11 -0
- package/dist/cmd/build-model.d.ts.map +1 -0
- package/dist/cmd/index.d.ts +11 -0
- package/dist/cmd/index.d.ts.map +1 -0
- package/dist/cmd/pack-block.d.ts +10 -0
- package/dist/cmd/pack-block.d.ts.map +1 -0
- package/dist/cmd/upload-package-v1.d.ts +15 -0
- package/dist/cmd/upload-package-v1.d.ts.map +1 -0
- package/dist/common_types.d.ts +3 -0
- package/dist/common_types.d.ts.map +1 -0
- package/dist/config-BJognM_j.mjs +536 -0
- package/dist/config-BJognM_j.mjs.map +1 -0
- package/dist/config-CfA0Dj6h.js +3 -0
- package/dist/config-CfA0Dj6h.js.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +43 -0
- package/dist/index.mjs.map +1 -0
- package/dist/lib/storage.d.ts +29 -0
- package/dist/lib/storage.d.ts.map +1 -0
- package/dist/lib.d.ts +3 -2637
- package/dist/lib.d.ts.map +1 -0
- package/dist/registry_v1/config.d.ts +12 -0
- package/dist/registry_v1/config.d.ts.map +1 -0
- package/dist/registry_v1/config_schema.d.ts +94 -0
- package/dist/registry_v1/config_schema.d.ts.map +1 -0
- package/dist/registry_v1/flags.d.ts +9 -0
- package/dist/registry_v1/flags.d.ts.map +1 -0
- package/dist/registry_v1/index.d.ts +4 -0
- package/dist/registry_v1/index.d.ts.map +1 -0
- package/dist/registry_v1/registry.d.ts +46 -0
- package/dist/registry_v1/registry.d.ts.map +1 -0
- package/dist/registry_v1/v1_repo_schema.d.ts +25 -0
- package/dist/registry_v1/v1_repo_schema.d.ts.map +1 -0
- package/dist/util.d.ts +4 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/v2/build_dist.d.ts +3 -0
- package/dist/v2/build_dist.d.ts.map +1 -0
- package/dist/v2/index.d.ts +4 -0
- package/dist/v2/index.d.ts.map +1 -0
- package/dist/v2/model/block_components.d.ts +384 -0
- package/dist/v2/model/block_components.d.ts.map +1 -0
- package/dist/v2/model/common.d.ts +3 -0
- package/dist/v2/model/common.d.ts.map +1 -0
- package/dist/v2/model/content_conversion.d.ts +35 -0
- package/dist/v2/model/content_conversion.d.ts.map +1 -0
- package/dist/v2/model/content_types.d.ts +478 -0
- package/dist/v2/model/content_types.d.ts.map +1 -0
- package/dist/{lib.d.cts → v2/model/index.d.ts} +449 -1005
- package/dist/v2/model/index.d.ts.map +1 -0
- package/dist/v2/model/meta.d.ts +805 -0
- package/dist/v2/model/meta.d.ts.map +1 -0
- package/dist/v2/registry/schema.d.ts +15 -0
- package/dist/v2/registry/schema.d.ts.map +1 -0
- package/dist/v2/source_package.d.ts +8 -0
- package/dist/v2/source_package.d.ts.map +1 -0
- package/package.json +24 -17
- package/src/cmd/build-meta.ts +38 -0
- package/src/cmd/build-model.ts +76 -0
- package/src/cmd/index.ts +12 -0
- package/src/cmd/pack-block.ts +32 -0
- package/src/cmd/upload-package-v1.ts +105 -0
- package/src/common_types.ts +3 -0
- package/src/lib/storage.test.ts +91 -0
- package/src/lib/storage.ts +140 -0
- package/src/lib.ts +2 -0
- package/src/registry_v1/config.ts +90 -0
- package/src/registry_v1/config_schema.ts +30 -0
- package/src/registry_v1/flags.ts +23 -0
- package/src/registry_v1/index.ts +3 -0
- package/src/registry_v1/registry.test.ts +122 -0
- package/src/registry_v1/registry.ts +253 -0
- package/src/registry_v1/v1_repo_schema.ts +42 -0
- package/src/util.ts +25 -0
- package/src/v2/build_dist.test.ts +16 -0
- package/src/v2/build_dist.ts +29 -0
- package/src/v2/index.ts +3 -0
- package/src/v2/model/block_components.ts +32 -0
- package/src/v2/model/common.ts +2 -0
- package/src/v2/model/content_conversion.ts +178 -0
- package/src/v2/model/content_types.ts +233 -0
- package/src/v2/model/index.ts +46 -0
- package/src/v2/model/meta.ts +36 -0
- package/src/v2/registry/schema.ts +29 -0
- package/src/v2/source_package.test.ts +27 -0
- package/src/v2/source_package.ts +82 -0
- package/dist/cli.cjs +0 -786
- package/dist/cli.cjs.map +0 -1
- package/dist/cli.d.cts +0 -58
- package/dist/cli.d.ts +0 -58
- package/dist/lib.cjs +0 -629
- package/dist/lib.cjs.map +0 -1
- package/dist/lib.js +0 -577
- package/dist/lib.js.map +0 -1
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import YAML from 'yaml';
|
|
2
|
+
import { tryLoadFile } from '../util';
|
|
3
|
+
import {
|
|
4
|
+
PlPackageJsonConfigFile,
|
|
5
|
+
PlPackageYamlConfigFile,
|
|
6
|
+
PlRegFullPackageConfigData,
|
|
7
|
+
PlRegPackageConfigDataShard
|
|
8
|
+
} from './config_schema';
|
|
9
|
+
import * as os from 'node:os';
|
|
10
|
+
import { BlockRegistry } from './registry';
|
|
11
|
+
import { storageByUrl } from '../lib/storage';
|
|
12
|
+
import { FullBlockPackageName } from './v1_repo_schema';
|
|
13
|
+
import { MiLogger } from '@milaboratories/ts-helpers';
|
|
14
|
+
|
|
15
|
+
function mergeConfigs(
|
|
16
|
+
c1: PlRegPackageConfigDataShard,
|
|
17
|
+
c2: PlRegPackageConfigDataShard | undefined
|
|
18
|
+
): PlRegPackageConfigDataShard {
|
|
19
|
+
if (c2 === undefined) return c1;
|
|
20
|
+
return {
|
|
21
|
+
...c1,
|
|
22
|
+
...c2,
|
|
23
|
+
registries: { ...c1.registries, ...c2.registries },
|
|
24
|
+
files: { ...c1.files, ...c2.files }
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function tryLoadJsonConfigFromFile(
|
|
29
|
+
file: string
|
|
30
|
+
): Promise<PlRegPackageConfigDataShard | undefined> {
|
|
31
|
+
return tryLoadFile(file, (buf) => PlRegPackageConfigDataShard.parse(JSON.parse(buf.toString())));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function tryLoadYamlConfigFromFile(
|
|
35
|
+
file: string
|
|
36
|
+
): Promise<PlRegPackageConfigDataShard | undefined> {
|
|
37
|
+
return tryLoadFile(file, (buf) => PlRegPackageConfigDataShard.parse(YAML.parse(buf.toString())));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function loadConfigShard(): Promise<PlRegPackageConfigDataShard> {
|
|
41
|
+
let conf = PlRegPackageConfigDataShard.parse({});
|
|
42
|
+
|
|
43
|
+
conf = mergeConfigs(conf, await tryLoadJsonConfigFromFile('./.pl.reg.json'));
|
|
44
|
+
conf = mergeConfigs(conf, await tryLoadYamlConfigFromFile('./.pl.reg.yaml'));
|
|
45
|
+
conf = mergeConfigs(conf, await tryLoadJsonConfigFromFile(`${os.homedir()}/.pl.reg.json`));
|
|
46
|
+
conf = mergeConfigs(conf, await tryLoadYamlConfigFromFile(`${os.homedir()}/.pl.reg.yaml`));
|
|
47
|
+
conf = mergeConfigs(conf, await tryLoadJsonConfigFromFile(PlPackageJsonConfigFile));
|
|
48
|
+
conf = mergeConfigs(conf, await tryLoadYamlConfigFromFile(PlPackageYamlConfigFile));
|
|
49
|
+
|
|
50
|
+
return conf;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let conf: PlRegPackageConfigDataShard | undefined = undefined;
|
|
54
|
+
let confPromise: Promise<PlRegPackageConfigDataShard> | undefined = undefined;
|
|
55
|
+
|
|
56
|
+
async function getConfigShard() {
|
|
57
|
+
if (conf !== undefined) return conf;
|
|
58
|
+
if (confPromise !== undefined) return await confPromise;
|
|
59
|
+
confPromise = loadConfigShard();
|
|
60
|
+
return await confPromise;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class PlRegPackageConfig {
|
|
64
|
+
constructor(public readonly conf: PlRegFullPackageConfigData) {}
|
|
65
|
+
|
|
66
|
+
createRegistry(logger?: MiLogger): BlockRegistry {
|
|
67
|
+
let address = this.conf.registry;
|
|
68
|
+
if (!address.startsWith('file:') && !address.startsWith('s3:')) {
|
|
69
|
+
const regByAlias = this.conf.registries[address];
|
|
70
|
+
if (!regByAlias) throw new Error(`Registry with alias "${address}" not found`);
|
|
71
|
+
address = regByAlias;
|
|
72
|
+
}
|
|
73
|
+
return new BlockRegistry(storageByUrl(address), logger);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
get fullPackageName(): FullBlockPackageName {
|
|
77
|
+
return {
|
|
78
|
+
organization: this.conf.organization,
|
|
79
|
+
package: this.conf.package,
|
|
80
|
+
version: this.conf.version
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function getConfig(finalShard: PlRegPackageConfigDataShard) {
|
|
86
|
+
const confShard = await loadConfigShard();
|
|
87
|
+
return new PlRegPackageConfig(
|
|
88
|
+
PlRegFullPackageConfigData.parse(mergeConfigs(confShard, finalShard))
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { PlRegAddress } from '../common_types';
|
|
3
|
+
import { SemVer } from '@milaboratories/pl-model-middle-layer';
|
|
4
|
+
|
|
5
|
+
export const PlPackageConfigData = z.object({
|
|
6
|
+
organization: z.string(),
|
|
7
|
+
package: z.string(),
|
|
8
|
+
version: SemVer.optional(),
|
|
9
|
+
files: z.record(z.string().regex(/^[^\/]+$/), z.string()).default({}),
|
|
10
|
+
meta: z.object({}).passthrough()
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export const PlRegCommonConfigData = z.object({
|
|
14
|
+
registries: z.record(z.string(), PlRegAddress).default({}),
|
|
15
|
+
registry: z.string().optional()
|
|
16
|
+
});
|
|
17
|
+
export type PlRegCommonConfigData = z.infer<typeof PlRegCommonConfigData>;
|
|
18
|
+
|
|
19
|
+
export const PlRegFullPackageConfigData = PlRegCommonConfigData.merge(PlPackageConfigData).required(
|
|
20
|
+
{ registry: true, version: true }
|
|
21
|
+
);
|
|
22
|
+
export type PlRegFullPackageConfigData = z.infer<typeof PlRegFullPackageConfigData>;
|
|
23
|
+
export const PlRegPackageConfigDataShard = PlRegFullPackageConfigData.partial().required({
|
|
24
|
+
registries: true,
|
|
25
|
+
files: true
|
|
26
|
+
});
|
|
27
|
+
export type PlRegPackageConfigDataShard = z.infer<typeof PlRegPackageConfigDataShard>;
|
|
28
|
+
|
|
29
|
+
export const PlPackageJsonConfigFile = 'pl.package.json';
|
|
30
|
+
export const PlPackageYamlConfigFile = 'pl.package.yaml';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export interface TargetFile {
|
|
5
|
+
src: string;
|
|
6
|
+
destName: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function parseTargetFile(arg: string): TargetFile {
|
|
10
|
+
const match = arg.match(/(?<destName>[^\/\\]+)=(?<src>.*)/);
|
|
11
|
+
if (match) {
|
|
12
|
+
const { src, destName } = match.groups!;
|
|
13
|
+
return { src, destName };
|
|
14
|
+
} else {
|
|
15
|
+
return { src: arg, destName: path.basename(arg) };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const targetFile = Flags.custom<TargetFile>({
|
|
20
|
+
summary: 'target files to upload',
|
|
21
|
+
helpValue: 'file_path | package_name=file_path',
|
|
22
|
+
parse: async (arg) => parseTargetFile(arg)
|
|
23
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { RegistryStorage, S3Storage, storageByUrl } from '../lib/storage';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import { BlockRegistry } from './registry';
|
|
6
|
+
import { ConsoleLoggerAdapter } from '@milaboratories/ts-helpers';
|
|
7
|
+
|
|
8
|
+
type TestStorageInstance = {
|
|
9
|
+
storage: RegistryStorage;
|
|
10
|
+
teardown: () => Promise<void>;
|
|
11
|
+
};
|
|
12
|
+
type TestStorageTarget = {
|
|
13
|
+
name: string;
|
|
14
|
+
storageProvider: () => TestStorageInstance;
|
|
15
|
+
};
|
|
16
|
+
const testStorages: TestStorageTarget[] = [
|
|
17
|
+
{
|
|
18
|
+
name: 'local',
|
|
19
|
+
storageProvider: () => {
|
|
20
|
+
const uuid = randomUUID().toString();
|
|
21
|
+
const tmp = path.resolve('tmp');
|
|
22
|
+
const storagePath = path.resolve(tmp, uuid);
|
|
23
|
+
const storage = storageByUrl('file://' + storagePath);
|
|
24
|
+
return {
|
|
25
|
+
storage,
|
|
26
|
+
teardown: async () => {
|
|
27
|
+
await fs.promises.rm(storagePath, { recursive: true, force: true });
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const testS3Address = process.env.TEST_S3_ADDRESS;
|
|
35
|
+
if (testS3Address !== undefined) {
|
|
36
|
+
testStorages.push({
|
|
37
|
+
name: 's3',
|
|
38
|
+
storageProvider: () => {
|
|
39
|
+
const uuid = randomUUID().toString();
|
|
40
|
+
const testS3AddressURL = new URL(testS3Address!);
|
|
41
|
+
testS3AddressURL.pathname = `${testS3AddressURL.pathname.replace(/\/$/, '')}/${uuid}`;
|
|
42
|
+
const storage = storageByUrl(testS3AddressURL.toString());
|
|
43
|
+
return {
|
|
44
|
+
storage,
|
|
45
|
+
teardown: async () => {
|
|
46
|
+
const allFiles = await storage.listFiles('');
|
|
47
|
+
console.log('Deleting: ', allFiles);
|
|
48
|
+
await storage.deleteFiles(...allFiles);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
test.each(testStorages)('basic registry test with $name', async ({ storageProvider }) => {
|
|
56
|
+
const { storage, teardown } = storageProvider();
|
|
57
|
+
const registry = new BlockRegistry(storage, new ConsoleLoggerAdapter());
|
|
58
|
+
await registry.updateIfNeeded();
|
|
59
|
+
const constructor1 = registry.constructNewPackage({
|
|
60
|
+
organization: 'org1',
|
|
61
|
+
package: 'pkg1',
|
|
62
|
+
version: '1.1.0'
|
|
63
|
+
});
|
|
64
|
+
await constructor1.writeMeta({ some: 'value1' });
|
|
65
|
+
await constructor1.finish();
|
|
66
|
+
await registry.updateIfNeeded();
|
|
67
|
+
const constructor2 = registry.constructNewPackage({
|
|
68
|
+
organization: 'org1',
|
|
69
|
+
package: 'pkg1',
|
|
70
|
+
version: '1.2.0'
|
|
71
|
+
});
|
|
72
|
+
await constructor2.writeMeta({ some: 'value2' });
|
|
73
|
+
await constructor2.finish();
|
|
74
|
+
await registry.updateIfNeeded();
|
|
75
|
+
expect(await registry.getPackageOverview({ organization: 'org1', package: 'pkg1' })).toEqual([
|
|
76
|
+
{ version: '1.2.0', meta: { some: 'value2' } },
|
|
77
|
+
{ version: '1.1.0', meta: { some: 'value1' } }
|
|
78
|
+
]);
|
|
79
|
+
expect(await registry.getGlobalOverview()).toEqual([
|
|
80
|
+
{
|
|
81
|
+
organization: 'org1',
|
|
82
|
+
package: 'pkg1',
|
|
83
|
+
allVersions: ['1.1.0', '1.2.0'],
|
|
84
|
+
latestVersion: '1.2.0',
|
|
85
|
+
latestMeta: { some: 'value2' }
|
|
86
|
+
}
|
|
87
|
+
]);
|
|
88
|
+
await teardown();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test.each(testStorages)('package modification test with $name', async ({ storageProvider }) => {
|
|
92
|
+
const { storage, teardown } = storageProvider();
|
|
93
|
+
const registry = new BlockRegistry(storage, new ConsoleLoggerAdapter());
|
|
94
|
+
const constructor1 = registry.constructNewPackage({
|
|
95
|
+
organization: 'org1',
|
|
96
|
+
package: 'pkg1',
|
|
97
|
+
version: '1.1.0'
|
|
98
|
+
});
|
|
99
|
+
await constructor1.writeMeta({ some: 'value1' });
|
|
100
|
+
await constructor1.finish();
|
|
101
|
+
const constructor2 = registry.constructNewPackage({
|
|
102
|
+
organization: 'org1',
|
|
103
|
+
package: 'pkg1',
|
|
104
|
+
version: '1.1.0'
|
|
105
|
+
});
|
|
106
|
+
await constructor2.writeMeta({ some: 'value2' });
|
|
107
|
+
await constructor2.finish();
|
|
108
|
+
await registry.updateIfNeeded();
|
|
109
|
+
expect(await registry.getPackageOverview({ organization: 'org1', package: 'pkg1' })).toEqual([
|
|
110
|
+
{ version: '1.1.0', meta: { some: 'value2' } }
|
|
111
|
+
]);
|
|
112
|
+
expect(await registry.getGlobalOverview()).toEqual([
|
|
113
|
+
{
|
|
114
|
+
organization: 'org1',
|
|
115
|
+
package: 'pkg1',
|
|
116
|
+
allVersions: ['1.1.0'],
|
|
117
|
+
latestVersion: '1.1.0',
|
|
118
|
+
latestMeta: { some: 'value2' }
|
|
119
|
+
}
|
|
120
|
+
]);
|
|
121
|
+
await teardown();
|
|
122
|
+
});
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { RegistryStorage } from '../lib/storage';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import semver from 'semver/preload';
|
|
4
|
+
import {
|
|
5
|
+
BlockPackageNameWithoutVersion,
|
|
6
|
+
FullBlockPackageName,
|
|
7
|
+
GlobalOverview,
|
|
8
|
+
GlobalOverviewPath,
|
|
9
|
+
MetaFile,
|
|
10
|
+
PackageOverview,
|
|
11
|
+
packageOverviewPath,
|
|
12
|
+
payloadFilePath
|
|
13
|
+
} from './v1_repo_schema';
|
|
14
|
+
import { MiLogger } from '@milaboratories/ts-helpers';
|
|
15
|
+
|
|
16
|
+
function fullNameToPath(name: FullBlockPackageName): string {
|
|
17
|
+
return `${name.organization}/${name.package}/${name.version}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const VersionUpdatesPrefix = '_updates_v1/per_package_version/';
|
|
21
|
+
|
|
22
|
+
function packageUpdatePath(bp: FullBlockPackageName, seed: string): string {
|
|
23
|
+
return `${VersionUpdatesPrefix}${bp.organization}/${bp.package}/${bp.version}/${seed}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const PackageUpdatePattern =
|
|
27
|
+
/(?<packageKeyWithoutVersion>(?<organization>[^\/]+)\/(?<pkg>[^\/]+))\/(?<version>[^\/]+)\/(?<seed>[^\/]+)$/;
|
|
28
|
+
|
|
29
|
+
const GlobalUpdateSeedInFile = '_updates_v1/_global_update_in';
|
|
30
|
+
const GlobalUpdateSeedOutFile = '_updates_v1/_global_update_out';
|
|
31
|
+
|
|
32
|
+
/*
|
|
33
|
+
Note on convergence guarantee.
|
|
34
|
+
|
|
35
|
+
Here is what S3 guarantees:
|
|
36
|
+
|
|
37
|
+
Amazon S3 delivers strong read-after-write consistency automatically,
|
|
38
|
+
without changes to performance or availability, without sacrificing
|
|
39
|
+
regional isolation for applications, and at no additional cost.
|
|
40
|
+
|
|
41
|
+
After a successful write of a new object or an overwrite of an existing
|
|
42
|
+
object, any subsequent read request immediately receives the latest
|
|
43
|
+
version of the object. S3 also provides strong consistency for list
|
|
44
|
+
operations, so after a write, you can immediately perform a listing
|
|
45
|
+
of the objects in a bucket with any changes reflected.
|
|
46
|
+
|
|
47
|
+
https://aws.amazon.com/s3/faqs/#What_data_consistency_model_does_Amazon_S3_employ
|
|
48
|
+
|
|
49
|
+
The following registry update schema with _update_seed / _updated_seed
|
|
50
|
+
seems to guarantee eventual convergence of registry state, though I don't
|
|
51
|
+
have enough time to really think it through, beware.
|
|
52
|
+
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Layout:
|
|
57
|
+
*
|
|
58
|
+
* _updates_v1/per_package/
|
|
59
|
+
* organisationA/package1/1.2.3/seedABC <-- Tells that change happened for organisationA/package1 version 1.2.3, and reassembly of package1 overview is required
|
|
60
|
+
* organisationA/package1/1.2.3/seedCDE
|
|
61
|
+
* organisationB/package2/1.4.3/seedFGH
|
|
62
|
+
*
|
|
63
|
+
* _updates_v1/_global_update_in <-- Anybody who changes contents writes a random seed in this file
|
|
64
|
+
* _updates_v1/_global_update_out <-- Update process writes update seed from the _global_update_in here after successful update.
|
|
65
|
+
* Mismatch between contents of those files is a sign that another update should be performed.
|
|
66
|
+
*
|
|
67
|
+
* v1/ <-- Actual block packages content
|
|
68
|
+
* organisationA/package2/1.2.3/meta.json
|
|
69
|
+
* organisationA/package2/1.2.3/main.template.plj.gz
|
|
70
|
+
* organisationA/package2/1.2.3/...
|
|
71
|
+
* organisationA/package2/overview.json <-- Per-package aggregated meta-data over all available versions
|
|
72
|
+
* ...
|
|
73
|
+
*
|
|
74
|
+
* v1/overview.json <-- aggregated information about all packages
|
|
75
|
+
*
|
|
76
|
+
*/
|
|
77
|
+
export class BlockRegistry {
|
|
78
|
+
constructor(
|
|
79
|
+
private readonly storage: RegistryStorage,
|
|
80
|
+
private readonly logger?: MiLogger
|
|
81
|
+
) {}
|
|
82
|
+
|
|
83
|
+
constructNewPackage(pack: FullBlockPackageName): BlockRegistryPackConstructor {
|
|
84
|
+
return new BlockRegistryPackConstructor(this.storage, pack);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private async updateRegistry() {
|
|
88
|
+
this.logger?.info('Initiating registry refresh...');
|
|
89
|
+
|
|
90
|
+
// reading update requests
|
|
91
|
+
const packagesToUpdate = new Map<string, PackageUpdateInfo>();
|
|
92
|
+
const seedPaths: string[] = [];
|
|
93
|
+
const rawSeedPaths = await this.storage.listFiles(VersionUpdatesPrefix);
|
|
94
|
+
this.logger?.info('Packages to be updated:');
|
|
95
|
+
for (const seedPath of rawSeedPaths) {
|
|
96
|
+
const match = seedPath.match(PackageUpdatePattern);
|
|
97
|
+
if (!match) continue;
|
|
98
|
+
seedPaths.push(seedPath);
|
|
99
|
+
const { packageKeyWithoutVersion, organization, pkg, version, seed } = match.groups!;
|
|
100
|
+
|
|
101
|
+
let update = packagesToUpdate.get(packageKeyWithoutVersion);
|
|
102
|
+
let added = false;
|
|
103
|
+
if (!update) {
|
|
104
|
+
packagesToUpdate.set(packageKeyWithoutVersion, {
|
|
105
|
+
package: { organization, package: pkg },
|
|
106
|
+
versions: new Set([version])
|
|
107
|
+
});
|
|
108
|
+
added = true;
|
|
109
|
+
} else if (!update.versions.has(version)) {
|
|
110
|
+
update.versions.add(version);
|
|
111
|
+
added = true;
|
|
112
|
+
}
|
|
113
|
+
this.logger?.info(` - ${organization}:${pkg}:${version}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// loading global overview
|
|
117
|
+
const overviewContent = await this.storage.getFile(GlobalOverviewPath);
|
|
118
|
+
let overview =
|
|
119
|
+
overviewContent === undefined
|
|
120
|
+
? []
|
|
121
|
+
: (JSON.parse(overviewContent.toString()) as GlobalOverview);
|
|
122
|
+
this.logger?.info(`Global overview loaded, ${overview.length} records`);
|
|
123
|
+
|
|
124
|
+
// updating packages
|
|
125
|
+
for (const [, packageInfo] of packagesToUpdate.entries()) {
|
|
126
|
+
// reading existing overview
|
|
127
|
+
const overviewFile = packageOverviewPath(packageInfo.package);
|
|
128
|
+
const pOverviewContent = await this.storage.getFile(overviewFile);
|
|
129
|
+
let packageOverview =
|
|
130
|
+
pOverviewContent === undefined
|
|
131
|
+
? []
|
|
132
|
+
: (JSON.parse(pOverviewContent.toString()) as PackageOverview);
|
|
133
|
+
this.logger?.info(
|
|
134
|
+
`Updating ${packageInfo.package.organization}:${packageInfo.package.package} overview, ${packageOverview.length} records`
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// removing versions that we will update
|
|
138
|
+
packageOverview = packageOverview.filter((e) => !packageInfo.versions.has(e.version));
|
|
139
|
+
|
|
140
|
+
// reading new entries
|
|
141
|
+
for (const [v] of packageInfo.versions.entries()) {
|
|
142
|
+
const version = v.toString();
|
|
143
|
+
const metaContent = await this.storage.getFile(
|
|
144
|
+
payloadFilePath(
|
|
145
|
+
{
|
|
146
|
+
...packageInfo.package,
|
|
147
|
+
version
|
|
148
|
+
},
|
|
149
|
+
MetaFile
|
|
150
|
+
)
|
|
151
|
+
);
|
|
152
|
+
if (!metaContent) continue;
|
|
153
|
+
packageOverview.push({ version, meta: JSON.parse(metaContent.toString()) });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// sorting entries according to version
|
|
157
|
+
packageOverview.sort((e1, e2) => semver.compare(e2.version, e1.version));
|
|
158
|
+
|
|
159
|
+
// write package overview back
|
|
160
|
+
await this.storage.putFile(overviewFile, Buffer.from(JSON.stringify(packageOverview)));
|
|
161
|
+
this.logger?.info(`Done (${packageOverview.length} records)`);
|
|
162
|
+
|
|
163
|
+
// patching corresponding entry in overview
|
|
164
|
+
overview = overview.filter(
|
|
165
|
+
(e) =>
|
|
166
|
+
e.organization !== packageInfo.package.organization ||
|
|
167
|
+
e.package !== packageInfo.package.package
|
|
168
|
+
);
|
|
169
|
+
overview.push({
|
|
170
|
+
organization: packageInfo.package.organization,
|
|
171
|
+
package: packageInfo.package.package,
|
|
172
|
+
allVersions: packageOverview.map((e) => e.version).reverse(),
|
|
173
|
+
latestVersion: packageOverview[0].version,
|
|
174
|
+
latestMeta: packageOverview[0].meta
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// writing global overview
|
|
179
|
+
await this.storage.putFile(GlobalOverviewPath, Buffer.from(JSON.stringify(overview)));
|
|
180
|
+
this.logger?.info(`Global overview updated (${overview.length} records)`);
|
|
181
|
+
|
|
182
|
+
// deleting seeds
|
|
183
|
+
await this.storage.deleteFiles(...seedPaths.map((sp) => `${VersionUpdatesPrefix}${sp}`));
|
|
184
|
+
this.logger?.info(`Version update requests cleared`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async updateIfNeeded(force: boolean = false): Promise<void> {
|
|
188
|
+
// implementation of main convergence algorithm
|
|
189
|
+
|
|
190
|
+
this.logger?.info(`Checking if registry requires refresh...`);
|
|
191
|
+
const updateRequestSeed = await this.storage.getFile(GlobalUpdateSeedInFile);
|
|
192
|
+
const currentUpdatedSeed = await this.storage.getFile(GlobalUpdateSeedOutFile);
|
|
193
|
+
if (!force && updateRequestSeed === undefined && currentUpdatedSeed === undefined) return;
|
|
194
|
+
if (
|
|
195
|
+
!force &&
|
|
196
|
+
updateRequestSeed !== undefined &&
|
|
197
|
+
currentUpdatedSeed !== undefined &&
|
|
198
|
+
updateRequestSeed.equals(currentUpdatedSeed)
|
|
199
|
+
)
|
|
200
|
+
return;
|
|
201
|
+
|
|
202
|
+
await this.updateRegistry();
|
|
203
|
+
|
|
204
|
+
if (updateRequestSeed) {
|
|
205
|
+
await this.storage.putFile(GlobalUpdateSeedOutFile, updateRequestSeed);
|
|
206
|
+
this.logger?.info(`Refresh finished`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async getPackageOverview(
|
|
211
|
+
name: BlockPackageNameWithoutVersion
|
|
212
|
+
): Promise<undefined | PackageOverview> {
|
|
213
|
+
const content = await this.storage.getFile(packageOverviewPath(name));
|
|
214
|
+
if (content === undefined) return undefined;
|
|
215
|
+
return JSON.parse(content.toString()) as PackageOverview;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async getGlobalOverview(): Promise<undefined | GlobalOverview> {
|
|
219
|
+
const content = await this.storage.getFile(GlobalOverviewPath);
|
|
220
|
+
if (content === undefined) return undefined;
|
|
221
|
+
return JSON.parse(content.toString()) as GlobalOverview;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export class BlockRegistryPackConstructor {
|
|
226
|
+
private metaAdded: boolean = false;
|
|
227
|
+
public readonly seed = randomUUID();
|
|
228
|
+
|
|
229
|
+
constructor(
|
|
230
|
+
private readonly storage: RegistryStorage,
|
|
231
|
+
public readonly name: FullBlockPackageName
|
|
232
|
+
) {}
|
|
233
|
+
|
|
234
|
+
async addFile(file: string, content: Buffer): Promise<void> {
|
|
235
|
+
await this.storage.putFile(payloadFilePath(this.name, file), content);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async writeMeta(meta: object) {
|
|
239
|
+
await this.addFile(MetaFile, Buffer.from(JSON.stringify(meta)));
|
|
240
|
+
this.metaAdded = true;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async finish() {
|
|
244
|
+
if (!this.metaAdded) throw new Error('meta not added');
|
|
245
|
+
await this.storage.putFile(packageUpdatePath(this.name, this.seed), Buffer.of(0));
|
|
246
|
+
await this.storage.putFile(GlobalUpdateSeedInFile, Buffer.from(this.seed));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
interface PackageUpdateInfo {
|
|
251
|
+
package: BlockPackageNameWithoutVersion;
|
|
252
|
+
versions: Set<String>;
|
|
253
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface FullBlockPackageName {
|
|
2
|
+
organization: string;
|
|
3
|
+
package: string;
|
|
4
|
+
version: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const MainPrefix = 'v1/';
|
|
8
|
+
|
|
9
|
+
export function packageContentPrefix(bp: FullBlockPackageName): string {
|
|
10
|
+
return `${MainPrefix}${bp.organization}/${bp.package}/${bp.version}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function payloadFilePath(bp: FullBlockPackageName, file: string): string {
|
|
14
|
+
return `${MainPrefix}${bp.organization}/${bp.package}/${bp.version}/${file}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type BlockPackageNameWithoutVersion = Pick<FullBlockPackageName, 'organization' | 'package'>;
|
|
18
|
+
|
|
19
|
+
export function packageOverviewPath(bp: BlockPackageNameWithoutVersion): string {
|
|
20
|
+
return `${MainPrefix}${bp.organization}/${bp.package}/overview.json`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const GlobalOverviewPath = `${MainPrefix}overview.json`;
|
|
24
|
+
|
|
25
|
+
export const MetaFile = 'meta.json';
|
|
26
|
+
|
|
27
|
+
export interface PackageOverviewEntry {
|
|
28
|
+
version: string;
|
|
29
|
+
meta: object;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type PackageOverview = PackageOverviewEntry[];
|
|
33
|
+
|
|
34
|
+
export interface GlobalOverviewEntry {
|
|
35
|
+
organization: string;
|
|
36
|
+
package: string;
|
|
37
|
+
allVersions: string[];
|
|
38
|
+
latestVersion: string;
|
|
39
|
+
latestMeta: object;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type GlobalOverview = GlobalOverviewEntry[];
|
package/src/util.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { BigIntStats } from 'node:fs';
|
|
2
|
+
import fsp from 'node:fs/promises';
|
|
3
|
+
|
|
4
|
+
export async function tryLoadFile<T>(
|
|
5
|
+
file: string,
|
|
6
|
+
map: (buf: Buffer) => T
|
|
7
|
+
): Promise<T | undefined> {
|
|
8
|
+
try {
|
|
9
|
+
return map(await fsp.readFile(file));
|
|
10
|
+
} catch (err: any) {
|
|
11
|
+
if (err.code == 'ENOENT') return undefined;
|
|
12
|
+
else throw new Error('', { cause: err });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function tryStat(path: string): Promise<BigIntStats | undefined> {
|
|
17
|
+
try {
|
|
18
|
+
return await fsp.stat(path, { bigint: true });
|
|
19
|
+
} catch (error: any) {
|
|
20
|
+
if (error.code === 'ENOENT') {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { loadPackDescription } from './source_package';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fsp from 'node:fs/promises';
|
|
5
|
+
import { buildBlockPackDist } from './build_dist';
|
|
6
|
+
|
|
7
|
+
test.skip('create dist test', async () => {
|
|
8
|
+
const description = await loadPackDescription(
|
|
9
|
+
'/Volumes/Data/Projects/MiLaboratory/blocks-beta/block-template'
|
|
10
|
+
);
|
|
11
|
+
console.dir(description, { depth: 5 });
|
|
12
|
+
const uuid = randomUUID();
|
|
13
|
+
const distPath = path.resolve('tmp', uuid);
|
|
14
|
+
const manifest = await buildBlockPackDist(description, distPath);
|
|
15
|
+
console.dir(manifest, { depth: 5 });
|
|
16
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BlockPackDescriptionAbsolute,
|
|
3
|
+
BlockPackDescriptionConsolidateToFolder,
|
|
4
|
+
BlockPackDescriptionManifest,
|
|
5
|
+
BlockPackManifest,
|
|
6
|
+
BlockPackManifestFile
|
|
7
|
+
} from './model';
|
|
8
|
+
import fsp from 'node:fs/promises';
|
|
9
|
+
import { BlockPackMetaConsolidate, BlockPackMetaDescription } from './model/meta';
|
|
10
|
+
import { patch } from 'semver';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
|
|
13
|
+
export async function buildBlockPackDist(
|
|
14
|
+
description: BlockPackDescriptionAbsolute,
|
|
15
|
+
dst: string
|
|
16
|
+
): Promise<BlockPackManifest> {
|
|
17
|
+
await fsp.mkdir(dst, { recursive: true });
|
|
18
|
+
const files: string[] = [];
|
|
19
|
+
const descriptionRelative = await BlockPackDescriptionConsolidateToFolder(dst, files).parseAsync(
|
|
20
|
+
description
|
|
21
|
+
);
|
|
22
|
+
const manifest: BlockPackManifest = BlockPackManifest.parse({
|
|
23
|
+
schema: 'v1',
|
|
24
|
+
...descriptionRelative,
|
|
25
|
+
files
|
|
26
|
+
});
|
|
27
|
+
await fsp.writeFile(path.resolve(dst, BlockPackManifestFile), JSON.stringify(manifest));
|
|
28
|
+
return manifest;
|
|
29
|
+
}
|
package/src/v2/index.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import {
|
|
3
|
+
ResolvedModuleFile,
|
|
4
|
+
ResolvedModuleFolder,
|
|
5
|
+
packFolderToRelativeTgz,
|
|
6
|
+
cpAbsoluteToRelative,
|
|
7
|
+
mapRemoteToAbsolute,
|
|
8
|
+
} from './content_conversion';
|
|
9
|
+
import { BlockComponents, BlockComponentsManifest, ContentAbsoluteBinaryLocal, ContentAbsoluteFolder, ContentRelativeBinary } from '@milaboratories/pl-model-middle-layer';
|
|
10
|
+
|
|
11
|
+
export function BlockComponentsDescription(moduleRoot: string) {
|
|
12
|
+
return BlockComponents(
|
|
13
|
+
ResolvedModuleFile(moduleRoot),
|
|
14
|
+
ResolvedModuleFolder(moduleRoot, 'index.html')
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
export type BlockComponentsDescription = z.infer<ReturnType<typeof BlockComponentsDescription>>;
|
|
18
|
+
|
|
19
|
+
export function BlockComponentsConsolidate(dstFolder: string, fileAccumulator?: string[]) {
|
|
20
|
+
return BlockComponents(
|
|
21
|
+
ContentAbsoluteBinaryLocal.transform(cpAbsoluteToRelative(dstFolder, fileAccumulator)),
|
|
22
|
+
ContentAbsoluteFolder.transform(packFolderToRelativeTgz(dstFolder, 'ui.tgz', fileAccumulator))
|
|
23
|
+
).pipe(BlockComponentsManifest);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function BlockComponentsAbsoluteUrl(prefix: string) {
|
|
27
|
+
return BlockComponents(
|
|
28
|
+
ContentRelativeBinary.transform(mapRemoteToAbsolute(prefix)),
|
|
29
|
+
ContentRelativeBinary.transform(mapRemoteToAbsolute(prefix))
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
export type BlockComponentsAbsolute = z.infer<ReturnType<typeof BlockComponentsAbsoluteUrl>>;
|