@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.
Files changed (100) hide show
  1. package/dist/cli.js +1 -756
  2. package/dist/cli.js.map +1 -1
  3. package/dist/cli.mjs +187 -0
  4. package/dist/cli.mjs.map +1 -0
  5. package/dist/cmd/build-meta.d.ts +10 -0
  6. package/dist/cmd/build-meta.d.ts.map +1 -0
  7. package/dist/cmd/build-model.d.ts +11 -0
  8. package/dist/cmd/build-model.d.ts.map +1 -0
  9. package/dist/cmd/index.d.ts +11 -0
  10. package/dist/cmd/index.d.ts.map +1 -0
  11. package/dist/cmd/pack-block.d.ts +10 -0
  12. package/dist/cmd/pack-block.d.ts.map +1 -0
  13. package/dist/cmd/upload-package-v1.d.ts +15 -0
  14. package/dist/cmd/upload-package-v1.d.ts.map +1 -0
  15. package/dist/common_types.d.ts +3 -0
  16. package/dist/common_types.d.ts.map +1 -0
  17. package/dist/config-BJognM_j.mjs +536 -0
  18. package/dist/config-BJognM_j.mjs.map +1 -0
  19. package/dist/config-CfA0Dj6h.js +3 -0
  20. package/dist/config-CfA0Dj6h.js.map +1 -0
  21. package/dist/index.js +2 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/index.mjs +43 -0
  24. package/dist/index.mjs.map +1 -0
  25. package/dist/lib/storage.d.ts +29 -0
  26. package/dist/lib/storage.d.ts.map +1 -0
  27. package/dist/lib.d.ts +3 -2637
  28. package/dist/lib.d.ts.map +1 -0
  29. package/dist/registry_v1/config.d.ts +12 -0
  30. package/dist/registry_v1/config.d.ts.map +1 -0
  31. package/dist/registry_v1/config_schema.d.ts +94 -0
  32. package/dist/registry_v1/config_schema.d.ts.map +1 -0
  33. package/dist/registry_v1/flags.d.ts +9 -0
  34. package/dist/registry_v1/flags.d.ts.map +1 -0
  35. package/dist/registry_v1/index.d.ts +4 -0
  36. package/dist/registry_v1/index.d.ts.map +1 -0
  37. package/dist/registry_v1/registry.d.ts +46 -0
  38. package/dist/registry_v1/registry.d.ts.map +1 -0
  39. package/dist/registry_v1/v1_repo_schema.d.ts +25 -0
  40. package/dist/registry_v1/v1_repo_schema.d.ts.map +1 -0
  41. package/dist/util.d.ts +4 -0
  42. package/dist/util.d.ts.map +1 -0
  43. package/dist/v2/build_dist.d.ts +3 -0
  44. package/dist/v2/build_dist.d.ts.map +1 -0
  45. package/dist/v2/index.d.ts +4 -0
  46. package/dist/v2/index.d.ts.map +1 -0
  47. package/dist/v2/model/block_components.d.ts +384 -0
  48. package/dist/v2/model/block_components.d.ts.map +1 -0
  49. package/dist/v2/model/common.d.ts +3 -0
  50. package/dist/v2/model/common.d.ts.map +1 -0
  51. package/dist/v2/model/content_conversion.d.ts +35 -0
  52. package/dist/v2/model/content_conversion.d.ts.map +1 -0
  53. package/dist/v2/model/content_types.d.ts +478 -0
  54. package/dist/v2/model/content_types.d.ts.map +1 -0
  55. package/dist/{lib.d.cts → v2/model/index.d.ts} +449 -1005
  56. package/dist/v2/model/index.d.ts.map +1 -0
  57. package/dist/v2/model/meta.d.ts +805 -0
  58. package/dist/v2/model/meta.d.ts.map +1 -0
  59. package/dist/v2/registry/schema.d.ts +15 -0
  60. package/dist/v2/registry/schema.d.ts.map +1 -0
  61. package/dist/v2/source_package.d.ts +8 -0
  62. package/dist/v2/source_package.d.ts.map +1 -0
  63. package/package.json +24 -17
  64. package/src/cmd/build-meta.ts +38 -0
  65. package/src/cmd/build-model.ts +76 -0
  66. package/src/cmd/index.ts +12 -0
  67. package/src/cmd/pack-block.ts +32 -0
  68. package/src/cmd/upload-package-v1.ts +105 -0
  69. package/src/common_types.ts +3 -0
  70. package/src/lib/storage.test.ts +91 -0
  71. package/src/lib/storage.ts +140 -0
  72. package/src/lib.ts +2 -0
  73. package/src/registry_v1/config.ts +90 -0
  74. package/src/registry_v1/config_schema.ts +30 -0
  75. package/src/registry_v1/flags.ts +23 -0
  76. package/src/registry_v1/index.ts +3 -0
  77. package/src/registry_v1/registry.test.ts +122 -0
  78. package/src/registry_v1/registry.ts +253 -0
  79. package/src/registry_v1/v1_repo_schema.ts +42 -0
  80. package/src/util.ts +25 -0
  81. package/src/v2/build_dist.test.ts +16 -0
  82. package/src/v2/build_dist.ts +29 -0
  83. package/src/v2/index.ts +3 -0
  84. package/src/v2/model/block_components.ts +32 -0
  85. package/src/v2/model/common.ts +2 -0
  86. package/src/v2/model/content_conversion.ts +178 -0
  87. package/src/v2/model/content_types.ts +233 -0
  88. package/src/v2/model/index.ts +46 -0
  89. package/src/v2/model/meta.ts +36 -0
  90. package/src/v2/registry/schema.ts +29 -0
  91. package/src/v2/source_package.test.ts +27 -0
  92. package/src/v2/source_package.ts +82 -0
  93. package/dist/cli.cjs +0 -786
  94. package/dist/cli.cjs.map +0 -1
  95. package/dist/cli.d.cts +0 -58
  96. package/dist/cli.d.ts +0 -58
  97. package/dist/lib.cjs +0 -629
  98. package/dist/lib.cjs.map +0 -1
  99. package/dist/lib.js +0 -577
  100. 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,3 @@
1
+ export * from './v1_repo_schema';
2
+ export * from './config_schema';
3
+ export { PlRegPackageConfig } from './config';
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ export * from './model';
2
+ export * from './build_dist';
3
+ export * from './source_package';
@@ -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>>;
@@ -0,0 +1,2 @@
1
+ export type ContextType = 'local' | 'remote';
2
+ export type ContentType = 'text' | 'binary';