UNPKG

@k8ts/metadata

Version:

K8ts tools for working with k8ts metadata.

295 lines 9.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Metadata = void 0; const doddle_1 = require("doddle"); const error_1 = require("./error"); const input_1 = require("./input"); const metadata_key_1 = require("./input/key/metadata-key"); const map_1 = require("./utils/map"); const order_meta_keyed_object_1 = require("./utils/order-meta-keyed-object"); const validate_1 = require("./utils/validate"); /** * Mutable storage for k8s metadata. K8s metadata includes labels, annotations, and core fields. * These are addressed using `CharPrefix`. * * - **Labels**: `%key` → `metadata.labels.key` * - **Annotations**: `^key` → `metadata.annotations.key` * - **Comments**: `#key` → Build-time metadata, not manifested in k8s objects. * - **Core metadata**: `key` → `metadata.key` (e.g., `name`, `namespace`, etc.) * * In addition, you can address different metadata keys under the same domain using a `DomainKey`, * of the form `example.com/`. When using a `DomainKey`, you use a `CharPrefix` on the inner keys. * * This lets you add different kinds of metadata under the same domain with ease. * * @example * meta.add("%app", "my-app") // adds label 'app' with value 'my-app' * meta.add("example.section/", { * "%label1": "value1", // adds `%example.section/label1` with value 'value1' * "^annotation1": "value2" // adds `^example.section/annotation1` with value 'value2' * }) */ class Metadata { _dict; /** * Constructs a new Metadata instance from a map of key-value pairs. Validates all keys and * values during construction. * * @param _dict Internal map storing metadata key-value pairs * @throws {K8tsMetadataError} If any key or value is invalid */ constructor(a, b) { this._dict = _pairToMap([a, b]); for (const [key, value] of this._dict.entries()) { (0, validate_1.checkMetadataValue)(key, value); } } /** * Makes Metadata instances iterable, yielding [ValueKey, string] pairs. * * @example * for (const [key, value] of meta) { * console.log(key.str, value) * } */ *[Symbol.iterator]() { for (const entry of this._dict.entries()) { yield entry; } } /** * Creates a deep clone of this object. * * @returns */ clone() { return new Metadata(this); } delete(a, ...rest) { if (a.endsWith("/")) { const sectionKey = (0, input_1.parseSectionKey)(a); const onlyKeys = rest.map(input_1.parseInnerKey); if (onlyKeys.length > 0) { var deleteOnly = (key) => onlyKeys.some(ok => ok.equals(key)); } else { var deleteOnly = (_key) => _key.domain().equals(sectionKey); } for (const k of this._keys) { if (deleteOnly(k)) { this._dict.delete(k.str); } } } else { this._dict.delete(a); } return this; } add(a, b) { const parsed = _pairToMap([a, b]); for (const [k, v] of parsed) { if (this._dict.has(k)) { const prev = this._dict.get(k); throw new error_1.K8tsMetadataError(`Duplicate entry for ${k}, was ${prev} now ${v}`, { key: k.str }); } this._dict.set(k, v); } return this; } /** * Compares this Metadata instance to another for equality. Two instances are equal if they * contain the same key-value pairs. * * @param other The other Metadata instance or input to compare against * @returns Whether the two Metadata instances are equal */ equals(other) { return (0, map_1.equalsMap)(this._dict, new Metadata(other)._dict); } overwrite(a, b) { if (a === undefined) { return this; } const fromPair = _pairToMap([a, b]); for (const [k, v] of fromPair.entries()) { this._dict.set(k, v); } return this; } has(key) { const parsed = (0, input_1.parseKey)(key); if (parsed instanceof metadata_key_1.Metadata_Key_Value) { return this._dict.has(key); } else { return this._matchDomainPrefixes(parsed).size > 0; } } /** * Retrieves the value for the specified key. Throws if the key doesn't exist. * * @example * const appName = meta.get("%app") * * @param key The value key to retrieve * @returns The value associated with the key * @throws {K8tsMetadataError} If the key is not found */ get(key) { const parsed = (0, input_1.parseKey)(key); const v = this._dict.get(key); if (v === undefined) { throw new error_1.K8tsMetadataError(`Key ${key} not found!`, { key }); } return v; } /** * Attempts to retrieve the value for the specified key, returning a fallback if not found. * * @example * const appName = meta.tryGet("%app", "default-app") * * @param key The value key to retrieve * @param fallback Optional fallback value if key doesn't exist * @returns The value associated with the key, or the fallback value * @throws {K8tsMetadataError} If a domain key is provided instead of a value key */ tryGet(key, fallback) { const parsed = (0, input_1.parseKey)(key); if (!(parsed instanceof metadata_key_1.Metadata_Key_Value)) { throw new error_1.K8tsMetadataError("Unexpected domain key!", { key }); } return this._dict.get(key) ?? fallback; } get _parsedPairs() { return (0, doddle_1.seq)(this._dict) .map(([k, v]) => [(0, input_1.parseKey)(k), v]) .toArray() .pull(); } _matchDomainPrefixes(key) { return (0, doddle_1.seq)(this._parsedPairs) .filter(([k, v]) => k.parent?.equals(key) ?? false) .toMap(x => [x[0].str, x[1]]) .pull(); } /** * Creates a new Metadata instance containing only some of the keys. You can pass both entire * keys and domain prefixes to include all keys under that domain. * * @example * const subset = meta.pick("%app", "name", "example.com/") * * @param keySpecs Keys or domain prefixes to include in the result * @returns A new Metadata instance with only the picked keys */ pick(...keySpecs) { const parsed = keySpecs.map(input_1.parseKey); const keyStrSet = new Set(); for (const key of parsed) { if (key instanceof metadata_key_1.Metadata_Key_Value) { keyStrSet.add(key.str); } else { const sectionKeys = this._matchDomainPrefixes(key); for (const k of sectionKeys.keys()) { keyStrSet.add(k); } } } const out = new Map(); for (const [k, v] of this._dict.entries()) { if (keyStrSet.has(k)) out.set(k, v); } return new Metadata(out); } _prefixed(prefix) { const out = {}; for (const [k, v] of this._parsedPairs) { if (k.prefix() === prefix) { out[k.suffix] = v; } } return (0, order_meta_keyed_object_1.orderMetaKeyedObject)(out); } /** * Returns all labels as a plain object that can be embedded into a k8s manifest, with keys in * canonical order. * * @example * const labels = Metadata.make({ * "%app": "my-app", * "%tier": "backend" * }).labels * // { app: "my-app", tier: "backend" } */ get labels() { return this._prefixed("%"); } /** * Returns all annotations as a plain object that can be embedded into a k8s manifest, with keys * in canonical order. * * @example * const annotations = Metadata.make({ * "^note": "This is important", * "^description": "Detailed info" * }).annotations * // { note: "This is important", description: "Detailed info" } */ get annotations() { return this._prefixed("^"); } /** * Returns all comments (build-time metadata) as a plain object, with keys in canonical order. * * @example * const comments = Metadata.make({ * "#note": "Internal use only" * }).comments * // { note: "Internal use only" } */ get comments() { return this._prefixed("#"); } /** * Returns all metadata key-value pairs as a flat JavaScript object, with each key prefixed * appropriately. * * @example * const all = Metadata.make({ * "%app": "my-app", * "^note": "This is important", * name: "my-resource" * }).values * // { "%app": "my-app", "^note": "This is important", "name": "my-resource" } */ get record() { return (0, map_1.toJS)(this._dict); } get _keys() { return (0, doddle_1.seq)(this._parsedPairs) .map(([k, v]) => k) .toArray() .pull(); } } exports.Metadata = Metadata; function _pairToObject(pair) { let [key, value] = pair; key = key instanceof metadata_key_1.Metadata_Key_Value ? key.str : key; if (typeof key === "string") { return { [key]: value }; } return key; } function _pairToMap(pair) { return (0, input_1.parseMetaInput)(_pairToObject(pair)); } //# sourceMappingURL=meta.js.map