@k8ts/metadata
Version:
K8ts tools for working with k8ts metadata.
295 lines • 9.9 kB
JavaScript
"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