oc 0.50.55 → 0.50.56

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 (33) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/.turbo/turbo-test-silent.log +11 -11
  3. package/.turbo/turbo-test.log +6 -1614
  4. package/CHANGELOG.md +8 -0
  5. package/README.md +180 -0
  6. package/dist/cli/commands.d.ts +8 -0
  7. package/dist/cli/commands.js +8 -0
  8. package/dist/cli/facade/dev.d.ts +2 -2
  9. package/dist/cli/facade/registry-migrate-metadata.d.ts +20 -0
  10. package/dist/cli/facade/registry-migrate-metadata.js +105 -0
  11. package/dist/cli/index.js +2 -0
  12. package/dist/components/oc-client/_package/package.json +4 -4
  13. package/dist/components/oc-client/_package/server.js +1 -1
  14. package/dist/components/oc-client/package.json +1 -1
  15. package/dist/registry/domain/components-cache/index.d.ts +3 -1
  16. package/dist/registry/domain/components-cache/index.js +28 -5
  17. package/dist/registry/domain/components-details.d.ts +3 -1
  18. package/dist/registry/domain/components-details.js +26 -5
  19. package/dist/registry/domain/metadata-adapter-options.d.ts +6 -0
  20. package/dist/registry/domain/metadata-adapter-options.js +12 -0
  21. package/dist/registry/domain/metadata-index.d.ts +15 -0
  22. package/dist/registry/domain/metadata-index.js +132 -0
  23. package/dist/registry/domain/metadata-migration.d.ts +38 -0
  24. package/dist/registry/domain/metadata-migration.js +128 -0
  25. package/dist/registry/domain/repository.d.ts +1 -0
  26. package/dist/registry/domain/repository.js +106 -7
  27. package/dist/registry/domain/validators/registry-configuration.js +21 -0
  28. package/dist/registry/index.d.ts +1 -1
  29. package/dist/registry/index.js +6 -2
  30. package/dist/resources/index.d.ts +3 -0
  31. package/dist/resources/index.js +3 -0
  32. package/dist/types.d.ts +41 -1
  33. package/package.json +3 -2
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createMetadataIndex = exports.getComponentRow = exports.getComponentsDetailsFromRows = exports.getComponentsListFromRows = void 0;
7
+ const semver_1 = __importDefault(require("semver"));
8
+ // Derive `lastEdit` from the data itself (the most recent publish) rather than
9
+ // the wall-clock time the snapshot happened to be built. This keeps the value
10
+ // semantically meaningful (it only advances when something is actually
11
+ // published) so the poll's `data.lastEdit > cached.lastEdit` guard works and
12
+ // the exported legacy `components.json` reflects the real last edit.
13
+ const getLastEditFromRows = (rows) => rows.reduce((max, row) => (row.publishDate > max ? row.publishDate : max), 0);
14
+ const getComponentsListFromRows = (rows) => {
15
+ const components = {};
16
+ for (const row of rows) {
17
+ const versions = components[row.name] || [];
18
+ versions.push(row.version);
19
+ components[row.name] = versions;
20
+ }
21
+ for (const versions of Object.values(components)) {
22
+ versions.sort(semver_1.default.compare);
23
+ }
24
+ return {
25
+ lastEdit: getLastEditFromRows(rows),
26
+ components
27
+ };
28
+ };
29
+ exports.getComponentsListFromRows = getComponentsListFromRows;
30
+ const getComponentsDetailsFromRows = (rows) => {
31
+ const components = {};
32
+ for (const row of rows) {
33
+ const componentDetails = components[row.name] || {};
34
+ componentDetails[row.version] = { publishDate: row.publishDate };
35
+ if (row.templateSize !== undefined) {
36
+ componentDetails[row.version].templateSize = row.templateSize;
37
+ }
38
+ components[row.name] = componentDetails;
39
+ }
40
+ return {
41
+ lastEdit: getLastEditFromRows(rows),
42
+ components
43
+ };
44
+ };
45
+ exports.getComponentsDetailsFromRows = getComponentsDetailsFromRows;
46
+ const getComponentRow = (name, version, component) => ({
47
+ name,
48
+ version,
49
+ publishDate: component.oc.date || 0,
50
+ templateSize: component.oc.files.template.size
51
+ });
52
+ exports.getComponentRow = getComponentRow;
53
+ const FORCED_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
54
+ const buildSnapshot = (rows) => ({
55
+ componentsList: (0, exports.getComponentsListFromRows)(rows),
56
+ componentsDetails: (0, exports.getComponentsDetailsFromRows)(rows)
57
+ });
58
+ const createMetadataIndex = (metadataStore) => {
59
+ let snapshot;
60
+ let lastChangeToken;
61
+ let lastForcedRefreshAt = 0;
62
+ let localAddGeneration = 0;
63
+ const refresh = async () => {
64
+ const refreshGeneration = localAddGeneration;
65
+ const changeToken = metadataStore.getChangeToken
66
+ ? await metadataStore.getChangeToken()
67
+ : undefined;
68
+ const now = Date.now();
69
+ if (snapshot &&
70
+ changeToken !== undefined &&
71
+ changeToken === lastChangeToken &&
72
+ now - lastForcedRefreshAt < FORCED_REFRESH_INTERVAL_MS) {
73
+ return snapshot;
74
+ }
75
+ const rows = await metadataStore.getAllComponents();
76
+ if (snapshot && refreshGeneration !== localAddGeneration) {
77
+ return snapshot;
78
+ }
79
+ snapshot = buildSnapshot(rows);
80
+ lastChangeToken = changeToken;
81
+ lastForcedRefreshAt = now;
82
+ return snapshot;
83
+ };
84
+ const add = (row) => {
85
+ // No snapshot yet: build one from the single row.
86
+ if (!snapshot) {
87
+ snapshot = buildSnapshot([row]);
88
+ localAddGeneration += 1;
89
+ return snapshot;
90
+ }
91
+ const { name, version } = row;
92
+ const prevList = snapshot.componentsList.components;
93
+ const prevDetails = snapshot.componentsDetails.components;
94
+ // Already present: keep the snapshot unchanged (mirrors the unique-constraint
95
+ // guarantee, so a duplicate publish doesn't disturb the cache).
96
+ if (prevDetails[name]?.[version]) {
97
+ return snapshot;
98
+ }
99
+ // Only the published component is rebuilt; every other component entry is
100
+ // shared by reference, so the cost is O(versions-of-this-component) rather
101
+ // than O(registry). New container objects are produced (rather than mutating
102
+ // in place) so in-flight readers keep a consistent view of the old snapshot.
103
+ const versions = [...(prevList[name] ?? []), version].sort(semver_1.default.compare);
104
+ const detail = {
105
+ ...(prevDetails[name] ?? {}),
106
+ [version]: { publishDate: row.publishDate }
107
+ };
108
+ if (row.templateSize !== undefined) {
109
+ detail[version].templateSize = row.templateSize;
110
+ }
111
+ const lastEdit = Math.max(snapshot.componentsList.lastEdit, row.publishDate);
112
+ snapshot = {
113
+ componentsList: {
114
+ lastEdit,
115
+ components: { ...prevList, [name]: versions }
116
+ },
117
+ componentsDetails: {
118
+ lastEdit,
119
+ components: { ...prevDetails, [name]: detail }
120
+ }
121
+ };
122
+ localAddGeneration += 1;
123
+ return snapshot;
124
+ };
125
+ return {
126
+ get: () => snapshot,
127
+ add,
128
+ refresh,
129
+ getOrRefresh: () => (snapshot ? Promise.resolve(snapshot) : refresh())
130
+ };
131
+ };
132
+ exports.createMetadataIndex = createMetadataIndex;
@@ -0,0 +1,38 @@
1
+ import type { StorageAdapter } from 'oc-storage-adapters-utils';
2
+ import type { ComponentRow, ComponentsDetails, MetadataStore } from '../../types';
3
+ export interface MetadataMigrationResult {
4
+ scanned: number;
5
+ inserted: number;
6
+ skipped: number;
7
+ }
8
+ export interface MetadataExportResult {
9
+ exported: number;
10
+ files: string[];
11
+ }
12
+ export declare const getComponentRowsFromComponentsDetails: (componentsDetails: ComponentsDetails) => ComponentRow[];
13
+ export declare const backfillMetadataRows: (metadataStore: MetadataStore, rows: ComponentRow[]) => Promise<MetadataMigrationResult>;
14
+ export declare const backfillMetadataFromComponentsDetails: (metadataStore: MetadataStore, componentsDetails: ComponentsDetails) => Promise<MetadataMigrationResult>;
15
+ export declare const getComponentRowsFromStorageDirectories: (options: {
16
+ cdn: StorageAdapter;
17
+ componentsDir: string;
18
+ }) => Promise<ComponentRow[]>;
19
+ export declare const backfillMetadataFromStorageDirectories: (options: {
20
+ metadataStore: MetadataStore;
21
+ cdn: StorageAdapter;
22
+ componentsDir: string;
23
+ }) => Promise<MetadataMigrationResult>;
24
+ export declare const reconcileMetadataFromStorage: (options: {
25
+ metadataStore: MetadataStore;
26
+ cdn: StorageAdapter;
27
+ componentsDir: string;
28
+ }) => Promise<MetadataMigrationResult>;
29
+ export declare const exportLegacyMetadataFiles: (options: {
30
+ metadataStore: MetadataStore;
31
+ cdn: StorageAdapter;
32
+ componentsDir: string;
33
+ }) => Promise<MetadataExportResult>;
34
+ export declare const backfillMetadataFromStorageDetails: (options: {
35
+ metadataStore: MetadataStore;
36
+ cdn: StorageAdapter;
37
+ componentsDir: string;
38
+ }) => Promise<MetadataMigrationResult>;
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.backfillMetadataFromStorageDetails = exports.exportLegacyMetadataFiles = exports.reconcileMetadataFromStorage = exports.backfillMetadataFromStorageDirectories = exports.getComponentRowsFromStorageDirectories = exports.backfillMetadataFromComponentsDetails = exports.backfillMetadataRows = exports.getComponentRowsFromComponentsDetails = void 0;
7
+ const oc_metadata_adapters_utils_1 = require("oc-metadata-adapters-utils");
8
+ const pLimit_1 = __importDefault(require("../../utils/pLimit"));
9
+ const metadata_index_1 = require("./metadata-index");
10
+ const isNotFoundError = (err, code) => err === code || err?.code === code;
11
+ const getTemplateSize = (component) => component.oc.files.template.size;
12
+ const getComponentRowsFromComponentsDetails = (componentsDetails) => {
13
+ const rows = [];
14
+ for (const [name, versions] of Object.entries(componentsDetails.components || {})) {
15
+ for (const [version, details] of Object.entries(versions)) {
16
+ const row = {
17
+ name,
18
+ version,
19
+ publishDate: details.publishDate
20
+ };
21
+ if (details.templateSize !== undefined) {
22
+ row.templateSize = details.templateSize;
23
+ }
24
+ rows.push(row);
25
+ }
26
+ }
27
+ return rows;
28
+ };
29
+ exports.getComponentRowsFromComponentsDetails = getComponentRowsFromComponentsDetails;
30
+ const backfillMetadataRows = async (metadataStore, rows) => {
31
+ const result = {
32
+ scanned: rows.length,
33
+ inserted: 0,
34
+ skipped: 0
35
+ };
36
+ const limit = (0, pLimit_1.default)(10);
37
+ await Promise.all(rows.map((row) => limit(async () => {
38
+ try {
39
+ await metadataStore.addVersion(row);
40
+ result.inserted += 1;
41
+ }
42
+ catch (err) {
43
+ if (err?.code === oc_metadata_adapters_utils_1.VERSION_ALREADY_EXISTS ||
44
+ err?.code === oc_metadata_adapters_utils_1.VERSION_PUBLISH_IN_PROGRESS) {
45
+ result.skipped += 1;
46
+ }
47
+ else {
48
+ throw err;
49
+ }
50
+ }
51
+ })));
52
+ return result;
53
+ };
54
+ exports.backfillMetadataRows = backfillMetadataRows;
55
+ const backfillMetadataFromComponentsDetails = async (metadataStore, componentsDetails) => (0, exports.backfillMetadataRows)(metadataStore, (0, exports.getComponentRowsFromComponentsDetails)(componentsDetails));
56
+ exports.backfillMetadataFromComponentsDetails = backfillMetadataFromComponentsDetails;
57
+ const getComponentRowsFromStorageDirectories = async (options) => {
58
+ let components;
59
+ try {
60
+ components = await options.cdn.listSubDirectories(options.componentsDir);
61
+ }
62
+ catch (err) {
63
+ if (isNotFoundError(err, 'dir_not_found')) {
64
+ return [];
65
+ }
66
+ throw err;
67
+ }
68
+ const maxConcurrentRequests = options.cdn.maxConcurrentRequests || 10;
69
+ const componentLimit = (0, pLimit_1.default)(maxConcurrentRequests);
70
+ const packageLimit = (0, pLimit_1.default)(maxConcurrentRequests);
71
+ const rowsByComponent = await Promise.all(components.map((name) => componentLimit(async () => {
72
+ const versions = await options.cdn.listSubDirectories(`${options.componentsDir}/${name}`);
73
+ const rows = await Promise.all(versions.map((version) => packageLimit(async () => {
74
+ try {
75
+ const component = await options.cdn.getJson(`${options.componentsDir}/${name}/${version}/package.json`, true);
76
+ const row = {
77
+ name,
78
+ version,
79
+ publishDate: component.oc.date || 0
80
+ };
81
+ const templateSize = getTemplateSize(component);
82
+ if (templateSize !== undefined) {
83
+ row.templateSize = templateSize;
84
+ }
85
+ return row;
86
+ }
87
+ catch (err) {
88
+ if (isNotFoundError(err, 'file_not_found')) {
89
+ return null;
90
+ }
91
+ throw err;
92
+ }
93
+ })));
94
+ return rows.filter((row) => row !== null);
95
+ })));
96
+ return rowsByComponent.flat();
97
+ };
98
+ exports.getComponentRowsFromStorageDirectories = getComponentRowsFromStorageDirectories;
99
+ const backfillMetadataFromStorageDirectories = async (options) => (0, exports.backfillMetadataRows)(options.metadataStore, await (0, exports.getComponentRowsFromStorageDirectories)(options));
100
+ exports.backfillMetadataFromStorageDirectories = backfillMetadataFromStorageDirectories;
101
+ exports.reconcileMetadataFromStorage = exports.backfillMetadataFromStorageDirectories;
102
+ const exportLegacyMetadataFiles = async (options) => {
103
+ const rows = await options.metadataStore.getAllComponents();
104
+ const componentsListPath = `${options.componentsDir}/components.json`;
105
+ const componentsDetailsPath = `${options.componentsDir}/components-details.json`;
106
+ await Promise.all([
107
+ options.cdn.putFileContent(JSON.stringify((0, metadata_index_1.getComponentsListFromRows)(rows)), componentsListPath, true),
108
+ options.cdn.putFileContent(JSON.stringify((0, metadata_index_1.getComponentsDetailsFromRows)(rows)), componentsDetailsPath, true)
109
+ ]);
110
+ return {
111
+ exported: rows.length,
112
+ files: [componentsListPath, componentsDetailsPath]
113
+ };
114
+ };
115
+ exports.exportLegacyMetadataFiles = exportLegacyMetadataFiles;
116
+ const backfillMetadataFromStorageDetails = async (options) => {
117
+ try {
118
+ const componentsDetails = await options.cdn.getJson(`${options.componentsDir}/components-details.json`, true);
119
+ return (0, exports.backfillMetadataFromComponentsDetails)(options.metadataStore, componentsDetails);
120
+ }
121
+ catch (err) {
122
+ if (isNotFoundError(err, 'file_not_found')) {
123
+ return (0, exports.backfillMetadataFromStorageDirectories)(options);
124
+ }
125
+ throw err;
126
+ }
127
+ };
128
+ exports.backfillMetadataFromStorageDetails = backfillMetadataFromStorageDetails;
@@ -28,5 +28,6 @@ export default function repository(conf: Config): {
28
28
  dryRun?: boolean;
29
29
  user?: string;
30
30
  }): Promise<void>;
31
+ close(): Promise<void>;
31
32
  };
32
33
  export type Repository = ReturnType<typeof repository>;
@@ -41,11 +41,16 @@ const node_path_1 = __importDefault(require("node:path"));
41
41
  const dotenv_1 = __importDefault(require("dotenv"));
42
42
  const fs_extra_1 = __importDefault(require("fs-extra"));
43
43
  const oc_get_unix_utc_timestamp_1 = __importDefault(require("oc-get-unix-utc-timestamp"));
44
+ const oc_metadata_adapters_utils_1 = require("oc-metadata-adapters-utils");
44
45
  const resources_1 = __importDefault(require("../../resources"));
45
46
  const settings_1 = __importDefault(require("../../resources/settings"));
46
47
  const error_to_string_1 = __importDefault(require("../../utils/error-to-string"));
47
48
  const components_cache_1 = __importDefault(require("./components-cache"));
48
49
  const components_details_1 = __importDefault(require("./components-details"));
50
+ const events_handler_1 = __importDefault(require("./events-handler"));
51
+ const metadata_adapter_options_1 = __importDefault(require("./metadata-adapter-options"));
52
+ const metadata_index_1 = require("./metadata-index");
53
+ const metadata_migration_1 = require("./metadata-migration");
49
54
  const register_templates_1 = __importDefault(require("./register-templates"));
50
55
  const storage_adapter_1 = __importDefault(require("./storage-adapter"));
51
56
  const validator = __importStar(require("./validators"));
@@ -58,10 +63,56 @@ function repository(conf) {
58
63
  const repositorySource = conf.local
59
64
  ? 'local repository'
60
65
  : cdn.adapterType + ' cdn';
61
- const componentsCache = (0, components_cache_1.default)(conf, cdn);
62
- const componentsDetails = (0, components_details_1.default)(conf, cdn);
66
+ const metadataStore = !conf.local && conf.metadata
67
+ ? conf.metadata.adapter((0, metadata_adapter_options_1.default)(conf))
68
+ : undefined;
69
+ const metadataIndex = metadataStore
70
+ ? (0, metadata_index_1.createMetadataIndex)(metadataStore)
71
+ : undefined;
72
+ const componentsCache = (0, components_cache_1.default)(conf, cdn, metadataIndex);
73
+ const componentsDetails = (0, components_details_1.default)(conf, cdn, metadataIndex);
74
+ let exportLegacyFilesLoop;
75
+ let closed = false;
63
76
  const getFilePath = (component, version, filePath) => `${options.componentsDir}/${component}/${version}/${filePath}`;
77
+ const exportLegacyFiles = () => {
78
+ if (!metadataStore || !conf.metadata?.exportLegacyFiles) {
79
+ return;
80
+ }
81
+ return (0, metadata_migration_1.exportLegacyMetadataFiles)({
82
+ metadataStore,
83
+ cdn,
84
+ componentsDir: options.componentsDir
85
+ }).catch((err) => events_handler_1.default.fire('error', {
86
+ code: 'metadata_legacy_files_export',
87
+ message: err?.message || String(err)
88
+ }));
89
+ };
90
+ // Run the DB→components.json export on a non-overlapping background timer
91
+ // instead of on the publish path, so a publish stays an O(1) append rather
92
+ // than triggering a full-registry scan + blob rewrite. The timer only runs
93
+ // when an interval is explicitly configured.
94
+ const scheduleLegacyFilesExport = () => {
95
+ const intervalSeconds = conf.metadata?.exportLegacyFilesInterval;
96
+ if (closed ||
97
+ !metadataStore ||
98
+ !conf.metadata?.exportLegacyFiles ||
99
+ !intervalSeconds) {
100
+ return;
101
+ }
102
+ exportLegacyFilesLoop = setTimeout(async () => {
103
+ await exportLegacyFiles();
104
+ if (!closed) {
105
+ scheduleLegacyFilesExport();
106
+ }
107
+ }, intervalSeconds * 1000);
108
+ };
64
109
  const { templatesHash, templatesInfo } = (0, register_templates_1.default)(conf.templates, conf.local);
110
+ const throwVersionAlreadyFound = (componentName, componentVersion) => {
111
+ throw {
112
+ code: resources_1.default.errors.registry.COMPONENT_VERSION_ALREADY_FOUND_CODE,
113
+ msg: resources_1.default.errors.registry.COMPONENT_VERSION_ALREADY_FOUND(componentName, componentVersion, repositorySource)
114
+ };
115
+ };
65
116
  const local = {
66
117
  components: undefined,
67
118
  getCompiledView(componentName) {
@@ -222,8 +273,21 @@ function repository(conf) {
222
273
  // when in local this won't get called
223
274
  return;
224
275
  }
276
+ if (metadataStore) {
277
+ await metadataStore.initialise();
278
+ if (conf.metadata?.reconcileFromStorage) {
279
+ await (0, metadata_migration_1.reconcileMetadataFromStorage)({
280
+ metadataStore,
281
+ cdn,
282
+ componentsDir: options.componentsDir
283
+ });
284
+ }
285
+ }
225
286
  const componentsList = await componentsCache.load();
226
- return componentsDetails.refresh(componentsList);
287
+ const details = await componentsDetails.refresh(componentsList);
288
+ void exportLegacyFiles();
289
+ scheduleLegacyFilesExport();
290
+ return details;
227
291
  },
228
292
  async publishComponent({ componentName, componentVersion, pkgDetails, user, dryRun = false }) {
229
293
  if (conf.local) {
@@ -257,21 +321,56 @@ function repository(conf) {
257
321
  }
258
322
  const componentVersions = await repository.getComponentVersions(componentName);
259
323
  if (!versionHandler.validateNewVersion(componentVersion, componentVersions)) {
260
- throw {
261
- code: resources_1.default.errors.registry.COMPONENT_VERSION_ALREADY_FOUND_CODE,
262
- msg: resources_1.default.errors.registry.COMPONENT_VERSION_ALREADY_FOUND(componentName, componentVersion, repositorySource)
263
- };
324
+ throwVersionAlreadyFound(componentName, componentVersion);
264
325
  }
265
326
  pkgDetails.packageJson.oc.date = (0, oc_get_unix_utc_timestamp_1.default)();
266
327
  pkgDetails.packageJson.oc.publisher = user;
267
328
  if (dryRun)
268
329
  return;
269
330
  await fs_extra_1.default.writeJson(node_path_1.default.join(pkgDetails.outputFolder, 'package.json'), pkgDetails.packageJson);
331
+ if (metadataStore) {
332
+ const componentRow = (0, metadata_index_1.getComponentRow)(componentName, componentVersion, pkgDetails.packageJson);
333
+ const { token } = await metadataStore
334
+ .reserveVersion(componentRow)
335
+ .catch((err) => {
336
+ if (err?.code === oc_metadata_adapters_utils_1.VERSION_ALREADY_EXISTS ||
337
+ err?.code === oc_metadata_adapters_utils_1.VERSION_PUBLISH_IN_PROGRESS ||
338
+ err?.code ===
339
+ resources_1.default.errors.registry.COMPONENT_VERSION_ALREADY_FOUND_CODE) {
340
+ throwVersionAlreadyFound(componentName, componentVersion);
341
+ }
342
+ throw err;
343
+ });
344
+ try {
345
+ await cdn.putDir(pkgDetails.outputFolder, `${options.componentsDir}/${componentName}/${componentVersion}`);
346
+ await metadataStore.commitVersion(componentName, componentVersion, token);
347
+ metadataIndex.add(componentRow);
348
+ }
349
+ catch (err) {
350
+ await metadataStore
351
+ .abortVersion(componentName, componentVersion, token)
352
+ .catch(() => undefined);
353
+ throw err;
354
+ }
355
+ return;
356
+ }
270
357
  await cdn.putDir(pkgDetails.outputFolder, `${options.componentsDir}/${componentName}/${componentVersion}`);
271
358
  void componentsCache
272
359
  .refresh()
273
360
  .then((componentsList) => componentsDetails.refresh(componentsList))
274
361
  .catch(() => undefined);
362
+ },
363
+ async close() {
364
+ closed = true;
365
+ componentsCache.close?.();
366
+ componentsDetails.close?.();
367
+ if (exportLegacyFilesLoop) {
368
+ clearTimeout(exportLegacyFilesLoop);
369
+ exportLegacyFilesLoop = undefined;
370
+ }
371
+ if (metadataStore?.close) {
372
+ await metadataStore.close();
373
+ }
275
374
  }
276
375
  };
277
376
  return repository;
@@ -39,6 +39,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.default = registryConfiguration;
40
40
  const resources_1 = __importDefault(require("../../../resources"));
41
41
  const auth = __importStar(require("../authentication"));
42
+ const metadata_adapter_options_1 = __importDefault(require("../metadata-adapter-options"));
42
43
  function registryConfiguration(conf) {
43
44
  const returnError = (message) => {
44
45
  return {
@@ -111,6 +112,26 @@ function registryConfiguration(conf) {
111
112
  }
112
113
  }
113
114
  }
115
+ if (!conf.local && conf.metadata) {
116
+ if (!conf.metadata.adapter) {
117
+ return returnError(resources_1.default.errors.registry.CONFIGURATION_METADATA_NOT_VALID('metadata'));
118
+ }
119
+ if (typeof conf.metadata.exportLegacyFilesInterval !== 'undefined') {
120
+ if (!Number.isFinite(conf.metadata.exportLegacyFilesInterval) ||
121
+ conf.metadata.exportLegacyFilesInterval <= 0) {
122
+ return returnError(resources_1.default.errors.registry
123
+ .CONFIGURATION_METADATA_EXPORT_INTERVAL_NOT_VALID);
124
+ }
125
+ if (conf.metadata.exportLegacyFiles !== true) {
126
+ return returnError(resources_1.default.errors.registry
127
+ .CONFIGURATION_METADATA_EXPORT_INTERVAL_WITHOUT_EXPORT_NOT_VALID);
128
+ }
129
+ }
130
+ const metadataStore = conf.metadata.adapter((0, metadata_adapter_options_1.default)(conf));
131
+ if (!metadataStore.isValid()) {
132
+ return returnError(resources_1.default.errors.registry.CONFIGURATION_METADATA_NOT_VALID(metadataStore.adapterType));
133
+ }
134
+ }
114
135
  if (conf.customHeadersToSkipOnWeakVersion) {
115
136
  if (!Array.isArray(conf.customHeadersToSkipOnWeakVersion)) {
116
137
  return returnError(resources_1.default.errors.registry
@@ -4,7 +4,7 @@ import type { Plugin } from '../types';
4
4
  import { RegistryOptions } from './domain/options-sanitiser';
5
5
  export { RegistryOptions };
6
6
  export default function registry<T = any>(inputOptions: RegistryOptions<T>): {
7
- close: (callback: (err?: Error | undefined | string) => void) => void | http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>;
7
+ close: (callback: (err?: Error | undefined | string) => void) => void;
8
8
  on: <T_1 extends keyof {
9
9
  error: {
10
10
  code: string;
@@ -59,10 +59,14 @@ function registry(inputOptions) {
59
59
  let server;
60
60
  const repository = (0, repository_1.default)(options);
61
61
  const close = (callback) => {
62
+ const closeMetadataStore = () => Promise.resolve(repository.close?.()).catch(() => undefined);
62
63
  if (server?.listening) {
63
- return server.close(callback);
64
+ server.close((err) => {
65
+ void closeMetadataStore().finally(() => callback(err));
66
+ });
67
+ return;
64
68
  }
65
- return callback('not opened');
69
+ void closeMetadataStore().finally(() => callback('not opened'));
66
70
  };
67
71
  const register = (plugin, callback) => {
68
72
  plugins.push(Object.assign(plugin, { callback }));
@@ -28,6 +28,9 @@ declare const _default: {
28
28
  COMPONENT_SET_COOKIE_PARAMETERS_NOT_VALID: string;
29
29
  CONFIGURATION_DEPENDENCIES_MUST_BE_ARRAY: string;
30
30
  CONFIGURATION_EMPTY: string;
31
+ CONFIGURATION_METADATA_NOT_VALID: (adapterType: string) => string;
32
+ CONFIGURATION_METADATA_EXPORT_INTERVAL_NOT_VALID: string;
33
+ CONFIGURATION_METADATA_EXPORT_INTERVAL_WITHOUT_EXPORT_NOT_VALID: string;
31
34
  CONFIGURATION_ONREQUEST_MUST_BE_FUNCTION: string;
32
35
  CONFIGURATION_OFFREQUEST_MUST_BE_FUNCTION: string;
33
36
  CONFIGURATION_PUBLISH_BASIC_AUTH_CREDENTIALS_MISSING: string;
@@ -77,6 +77,9 @@ exports.default = {
77
77
  COMPONENT_SET_COOKIE_PARAMETERS_NOT_VALID: 'context.setCookie parameters are not valid',
78
78
  CONFIGURATION_DEPENDENCIES_MUST_BE_ARRAY: 'Registry configuration is not valid: dependencies must be an array',
79
79
  CONFIGURATION_EMPTY: 'Registry configuration is empty',
80
+ CONFIGURATION_METADATA_NOT_VALID: (adapterType) => `Registry configuration is not valid: ${adapterType} is not a valid metadata adapter`,
81
+ CONFIGURATION_METADATA_EXPORT_INTERVAL_NOT_VALID: 'Registry configuration is not valid: metadata.exportLegacyFilesInterval must be a positive number',
82
+ CONFIGURATION_METADATA_EXPORT_INTERVAL_WITHOUT_EXPORT_NOT_VALID: 'Registry configuration is not valid: metadata.exportLegacyFilesInterval requires metadata.exportLegacyFiles to be true',
80
83
  CONFIGURATION_ONREQUEST_MUST_BE_FUNCTION: "Registry configuration is not valid: registry.on's callback must be a function",
81
84
  CONFIGURATION_OFFREQUEST_MUST_BE_FUNCTION: "Registry configuration is not valid: registry.off's callback must be a function",
82
85
  CONFIGURATION_PUBLISH_BASIC_AUTH_CREDENTIALS_MISSING: 'Registry configuration is not valid: basic auth requires username and password',
package/dist/types.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import type { NextFunction, Request, Response } from 'express';
2
+ import type { MetadataStore as MetadataStoreType } from 'oc-metadata-adapters-utils';
2
3
  import type { StorageAdapter } from 'oc-storage-adapters-utils';
3
4
  import type { PackageJson } from 'type-fest';
5
+ export type { ComponentRow, MetadataStore } from 'oc-metadata-adapters-utils';
4
6
  type Middleware = (req: Request, res: Response, next: NextFunction) => void;
5
7
  export interface Author {
6
8
  email?: string;
@@ -38,6 +40,44 @@ export interface ComponentsList {
38
40
  components: Record<string, string[]>;
39
41
  lastEdit: number;
40
42
  }
43
+ export interface MetadataConfig<T = any> {
44
+ /**
45
+ * Factory for the metadata store. It must be side-effect free because config
46
+ * validation may instantiate a throwaway store before registry startup.
47
+ */
48
+ adapter: (options: T) => MetadataStoreType;
49
+ options: T;
50
+ /**
51
+ * Controls whether OC asks the metadata adapter to manage its schema/table.
52
+ * When omitted, OC behaves as `true` and passes that default to adapter
53
+ * validation/startup/migration paths.
54
+ *
55
+ * @default true
56
+ */
57
+ manageSchema?: boolean;
58
+ /**
59
+ * When true, startup scans storage and inserts any missing metadata rows
60
+ * before cache hydration. Existing rows are skipped.
61
+ *
62
+ * @default false
63
+ */
64
+ reconcileFromStorage?: boolean;
65
+ /**
66
+ * When true, startup exports DB-derived `components.json` and
67
+ * `components-details.json` projections to storage. This is a one-way export
68
+ * from metadata to storage and is not triggered on each publish.
69
+ *
70
+ * @default false
71
+ */
72
+ exportLegacyFiles?: boolean;
73
+ /**
74
+ * Interval, in seconds, at which the DB→`components.json` legacy export runs
75
+ * in the background. Only used when `exportLegacyFiles` is true. When omitted,
76
+ * the export runs once at startup only (no background refresh). The export is
77
+ * never coupled to the publish path.
78
+ */
79
+ exportLegacyFilesInterval?: number;
80
+ }
41
81
  export interface OcParameter {
42
82
  default?: string | boolean | number;
43
83
  description?: string;
@@ -334,6 +374,7 @@ export interface Config<T = any> {
334
374
  * @default "/"
335
375
  */
336
376
  prefix: string;
377
+ metadata?: MetadataConfig;
337
378
  /**
338
379
  * Authentication strategy for component publishing.
339
380
  */
@@ -491,4 +532,3 @@ declare global {
491
532
  }
492
533
  }
493
534
  }
494
- export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oc",
3
- "version": "0.50.55",
3
+ "version": "0.50.56",
4
4
  "description": "A framework for developing and distributing html components",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -98,7 +98,8 @@
98
98
  "oc-client-browser": "*",
99
99
  "oc-empty-response-handler": "^1.0.2",
100
100
  "oc-get-unix-utc-timestamp": "^1.0.6",
101
- "oc-s3-storage-adapter": "^2.2.2",
101
+ "oc-metadata-adapters-utils": "^0.1.1",
102
+ "oc-s3-storage-adapter": "^2.2.3",
102
103
  "oc-storage-adapters-utils": "^2.1.2",
103
104
  "oc-template-es6": "^2.0.0",
104
105
  "oc-template-es6-compiler": "^7.1.4",