@ztimson/utils 0.27.10 → 0.27.11

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/index.cjs CHANGED
@@ -1919,10 +1919,12 @@ ${opts.message || this.desc}`;
1919
1919
  __publicField(this, "fullPath");
1920
1920
  /** Path including the name, excluding the module */
1921
1921
  __publicField(this, "path");
1922
- /** Last sagment of path */
1922
+ /** Last segment of path */
1923
1923
  __publicField(this, "name");
1924
1924
  /** List of methods */
1925
1925
  __publicField(this, "methods");
1926
+ /** Whether this path contains glob patterns */
1927
+ __publicField(this, "hasGlob");
1926
1928
  if (typeof e == "object") {
1927
1929
  Object.assign(this, e);
1928
1930
  return;
@@ -1931,17 +1933,34 @@ ${opts.message || this.desc}`;
1931
1933
  Object.assign(this, _PathEvent.pathEventCache.get(e));
1932
1934
  return;
1933
1935
  }
1934
- let [p, scope, method] = e.replaceAll(/\/{2,}/g, "/").split(":");
1935
- if (!method) method = scope || "*";
1936
- if (p == "*" || !p && method == "*") {
1937
- p = "";
1938
- method = "*";
1936
+ let [p, method] = e.replaceAll(/\/{2,}/g, "/").split(":");
1937
+ if (!method) method = "*";
1938
+ if (p === "" || p === void 0) {
1939
+ this.module = "";
1940
+ this.path = "";
1941
+ this.fullPath = "";
1942
+ this.name = "";
1943
+ this.methods = new ASet(["n"]);
1944
+ this.hasGlob = false;
1945
+ _PathEvent.pathEventCache.set(e, this);
1946
+ return;
1947
+ }
1948
+ if (p === "*") {
1949
+ this.module = "";
1950
+ this.path = "";
1951
+ this.fullPath = "**";
1952
+ this.name = "";
1953
+ this.methods = new ASet(["*"]);
1954
+ this.hasGlob = true;
1955
+ _PathEvent.pathEventCache.set(e, this);
1956
+ return;
1939
1957
  }
1940
1958
  let temp = p.split("/").filter((p2) => !!p2);
1941
1959
  this.module = temp.splice(0, 1)[0] || "";
1942
1960
  this.path = temp.join("/");
1943
1961
  this.fullPath = `${this.module}${this.module && this.path ? "/" : ""}${this.path}`;
1944
1962
  this.name = temp.pop() || "";
1963
+ this.hasGlob = this.fullPath.includes("*");
1945
1964
  this.methods = new ASet(method.split(""));
1946
1965
  _PathEvent.pathEventCache.set(e, this);
1947
1966
  }
@@ -1966,6 +1985,13 @@ ${opts.message || this.desc}`;
1966
1985
  set create(v) {
1967
1986
  v ? this.methods.delete("n").delete("*").add("c") : this.methods.delete("c");
1968
1987
  }
1988
+ /** Execute method specified */
1989
+ get execute() {
1990
+ return !this.methods.has("n") && (this.methods.has("*") || this.methods.has("x"));
1991
+ }
1992
+ set execute(v) {
1993
+ v ? this.methods.delete("n").delete("*").add("x") : this.methods.delete("x");
1994
+ }
1969
1995
  /** Read method specified */
1970
1996
  get read() {
1971
1997
  return !this.methods.has("n") && (this.methods.has("*") || this.methods.has("r"));
@@ -1991,6 +2017,64 @@ ${opts.message || this.desc}`;
1991
2017
  static clearCache() {
1992
2018
  _PathEvent.pathEventCache.clear();
1993
2019
  }
2020
+ /** Clear the permission cache */
2021
+ static clearPermissionCache() {
2022
+ _PathEvent.permissionCache.clear();
2023
+ }
2024
+ /**
2025
+ * Score a path for specificity ranking (lower = more specific = higher priority)
2026
+ * @private
2027
+ */
2028
+ static scoreSpecificity(path) {
2029
+ if (path === "**" || path === "") return Number.MAX_SAFE_INTEGER;
2030
+ const segments = path.split("/").filter((p) => !!p);
2031
+ let score = -segments.length;
2032
+ segments.forEach((seg) => {
2033
+ if (seg === "**") score += 0.5;
2034
+ else if (seg === "*") score += 0.25;
2035
+ });
2036
+ return score;
2037
+ }
2038
+ /**
2039
+ * Check if a path matches a glob pattern
2040
+ * @private
2041
+ */
2042
+ static pathMatchesGlob(path, pattern) {
2043
+ if (pattern === path) return true;
2044
+ const pathParts = path.split("/").filter((p) => !!p);
2045
+ const patternParts = pattern.split("/").filter((p) => !!p);
2046
+ let pathIdx = 0;
2047
+ let patternIdx = 0;
2048
+ while (patternIdx < patternParts.length && pathIdx < pathParts.length) {
2049
+ const patternPart = patternParts[patternIdx];
2050
+ if (patternPart === "**") {
2051
+ if (patternIdx === patternParts.length - 1) {
2052
+ return true;
2053
+ }
2054
+ patternParts[patternIdx + 1];
2055
+ while (pathIdx < pathParts.length) {
2056
+ if (_PathEvent.pathMatchesGlob(pathParts.slice(pathIdx).join("/"), patternParts.slice(patternIdx + 1).join("/"))) {
2057
+ return true;
2058
+ }
2059
+ pathIdx++;
2060
+ }
2061
+ return false;
2062
+ } else if (patternPart === "*") {
2063
+ pathIdx++;
2064
+ patternIdx++;
2065
+ } else {
2066
+ if (patternPart !== pathParts[pathIdx]) {
2067
+ return false;
2068
+ }
2069
+ pathIdx++;
2070
+ patternIdx++;
2071
+ }
2072
+ }
2073
+ if (patternIdx < patternParts.length) {
2074
+ return patternParts.slice(patternIdx).every((p) => p === "**");
2075
+ }
2076
+ return pathIdx === pathParts.length;
2077
+ }
1994
2078
  /**
1995
2079
  * Combine multiple events into one parsed object. Longest path takes precedent, but all subsequent methods are
1996
2080
  * combined until a "none" is reached
@@ -1999,38 +2083,58 @@ ${opts.message || this.desc}`;
1999
2083
  * @return {PathEvent} Final combined permission
2000
2084
  */
2001
2085
  static combine(...paths) {
2002
- let hitNone = false;
2003
- const combined = paths.map((p) => p instanceof _PathEvent ? p : new _PathEvent(p)).toSorted((p1, p2) => {
2004
- const l1 = p1.fullPath.length, l2 = p2.fullPath.length;
2005
- return l1 < l2 ? 1 : l1 > l2 ? -1 : 0;
2006
- }).reduce((acc, p) => {
2007
- if (acc && !acc.fullPath.startsWith(p.fullPath)) return acc;
2008
- if (p.none) hitNone = true;
2009
- if (!acc) return p;
2010
- if (hitNone) return acc;
2011
- acc.methods = new ASet([...acc.methods, ...p.methods]);
2012
- return acc;
2013
- }, null);
2014
- return combined;
2086
+ const parsed = paths.map((p) => p instanceof _PathEvent ? p : new _PathEvent(p));
2087
+ const sorted = parsed.toSorted((p1, p2) => {
2088
+ const score1 = _PathEvent.scoreSpecificity(p1.fullPath);
2089
+ const score2 = _PathEvent.scoreSpecificity(p2.fullPath);
2090
+ return score1 - score2;
2091
+ });
2092
+ let result = null;
2093
+ for (const p of sorted) {
2094
+ if (!result) {
2095
+ result = p;
2096
+ } else {
2097
+ if (result.fullPath.startsWith(p.fullPath)) {
2098
+ if (p.none) {
2099
+ break;
2100
+ }
2101
+ result.methods = new ASet([...result.methods, ...p.methods]);
2102
+ }
2103
+ }
2104
+ }
2105
+ return result || new _PathEvent("");
2015
2106
  }
2016
2107
  /**
2017
2108
  * Filter a set of paths based on the target
2018
2109
  *
2019
2110
  * @param {string | PathEvent | (string | PathEvent)[]} target Array of events that will filtered
2020
- * @param filter {...PathEvent} Must container one of
2021
- * @return {boolean} Whether there is any overlap
2111
+ * @param filter {...PathEvent} Must contain one of
2112
+ * @return {PathEvent[]} Filtered results
2022
2113
  */
2023
2114
  static filter(target, ...filter) {
2024
2115
  const parsedTarget = makeArray(target).map((pe) => pe instanceof _PathEvent ? pe : new _PathEvent(pe));
2025
2116
  const parsedFilter = makeArray(filter).map((pe) => pe instanceof _PathEvent ? pe : new _PathEvent(pe));
2026
- return parsedTarget.filter((t) => !!parsedFilter.find((r) => {
2027
- const wildcard = r.fullPath == "*" || t.fullPath == "*";
2028
- const p1 = r.fullPath.includes("*") ? r.fullPath.slice(0, r.fullPath.indexOf("*")) : r.fullPath;
2029
- const p2 = t.fullPath.includes("*") ? t.fullPath.slice(0, t.fullPath.indexOf("*")) : t.fullPath;
2030
- const scope = p1.startsWith(p2) || p2.startsWith(p1);
2031
- const methods = r.all || t.all || r.methods.intersection(t.methods).length;
2032
- return (wildcard || scope) && methods;
2033
- }));
2117
+ return parsedTarget.filter((t) => {
2118
+ const combined = _PathEvent.combine(t);
2119
+ return !!parsedFilter.find((r) => _PathEvent.matches(r, combined));
2120
+ });
2121
+ }
2122
+ /**
2123
+ * Check if a filter pattern matches a target path
2124
+ * @private
2125
+ */
2126
+ static matches(pattern, target) {
2127
+ if (pattern.fullPath === "" || target.fullPath === "") return false;
2128
+ if (pattern.fullPath === "*" || target.fullPath === "*") return pattern.methods.has("*") || target.methods.has("*") || pattern.methods.intersection(target.methods).length > 0;
2129
+ const methodsMatch = pattern.all || target.all || pattern.methods.intersection(target.methods).length > 0;
2130
+ if (!methodsMatch) return false;
2131
+ if (!pattern.hasGlob && !target.hasGlob) {
2132
+ return pattern.fullPath === target.fullPath;
2133
+ }
2134
+ if (pattern.hasGlob) {
2135
+ return this.pathMatchesGlob(target.fullPath, pattern.fullPath);
2136
+ }
2137
+ return this.pathMatchesGlob(pattern.fullPath, target.fullPath);
2034
2138
  }
2035
2139
  /**
2036
2140
  * Squash 2 sets of paths & return true if any overlap is found
@@ -2042,21 +2146,15 @@ ${opts.message || this.desc}`;
2042
2146
  static has(target, ...has) {
2043
2147
  const parsedTarget = makeArray(target).map((pe) => pe instanceof _PathEvent ? pe : new _PathEvent(pe));
2044
2148
  const parsedRequired = makeArray(has).map((pe) => pe instanceof _PathEvent ? pe : new _PathEvent(pe));
2045
- return !!parsedRequired.find((r) => !!parsedTarget.find((t) => {
2046
- const wildcard = r.fullPath == "*" || t.fullPath == "*";
2047
- const p1 = r.fullPath.includes("*") ? r.fullPath.slice(0, r.fullPath.indexOf("*")) : r.fullPath;
2048
- const p2 = t.fullPath.includes("*") ? t.fullPath.slice(0, t.fullPath.indexOf("*")) : t.fullPath;
2049
- const scope = p1.startsWith(p2);
2050
- const methods = r.all || t.all || r.methods.intersection(t.methods).length;
2051
- return (wildcard || scope) && methods;
2052
- }));
2149
+ const effectiveTarget = parsedTarget.length === 1 ? parsedTarget[0] : _PathEvent.combine(...parsedTarget);
2150
+ return !!parsedRequired.find((r) => _PathEvent.matches(r, effectiveTarget));
2053
2151
  }
2054
2152
  /**
2055
2153
  * Squash 2 sets of paths & return true if the target has all paths
2056
2154
  *
2057
2155
  * @param {string | PathEvent | (string | PathEvent)[]} target Array of Events as strings or pre-parsed
2058
2156
  * @param has Target must have all these paths
2059
- * @return {boolean} Whether there is any overlap
2157
+ * @return {boolean} Whether all are present
2060
2158
  */
2061
2159
  static hasAll(target, ...has) {
2062
2160
  return has.filter((h) => _PathEvent.has(target, h)).length == has.length;
@@ -2064,7 +2162,7 @@ ${opts.message || this.desc}`;
2064
2162
  /**
2065
2163
  * Same as `has` but raises an error if there is no overlap
2066
2164
  *
2067
- * @param {string | string[]} target Array of Events as strings or pre-parsed
2165
+ * @param {string | PathEvent | (string | PathEvent)[]} target Array of Events as strings or pre-parsed
2068
2166
  * @param has Target must have at least one of these path
2069
2167
  */
2070
2168
  static hasFatal(target, ...has) {
@@ -2073,7 +2171,7 @@ ${opts.message || this.desc}`;
2073
2171
  /**
2074
2172
  * Same as `hasAll` but raises an error if the target is missing any paths
2075
2173
  *
2076
- * @param {string | string[]} target Array of Events as strings or pre-parsed
2174
+ * @param {string | PathEvent | (string | PathEvent)[]} target Array of Events as strings or pre-parsed
2077
2175
  * @param has Target must have all these paths
2078
2176
  */
2079
2177
  static hasAllFatal(target, ...has) {
@@ -2105,7 +2203,7 @@ ${opts.message || this.desc}`;
2105
2203
  * Squash 2 sets of paths & return true if the target has all paths
2106
2204
  *
2107
2205
  * @param has Target must have all these paths
2108
- * @return {boolean} Whether there is any overlap
2206
+ * @return {boolean} Whether all are present
2109
2207
  */
2110
2208
  hasAll(...has) {
2111
2209
  return _PathEvent.hasAll(this, ...has);
@@ -2130,7 +2228,7 @@ ${opts.message || this.desc}`;
2130
2228
  * Filter a set of paths based on this event
2131
2229
  *
2132
2230
  * @param {string | PathEvent | (string | PathEvent)[]} target Array of events that will filtered
2133
- * @return {boolean} Whether there is any overlap
2231
+ * @return {PathEvent[]} Filtered results
2134
2232
  */
2135
2233
  filter(target) {
2136
2234
  return _PathEvent.filter(target, this);
@@ -2146,6 +2244,10 @@ ${opts.message || this.desc}`;
2146
2244
  };
2147
2245
  /** Internal cache for PathEvent instances to avoid redundant parsing */
2148
2246
  __publicField(_PathEvent, "pathEventCache", /* @__PURE__ */ new Map());
2247
+ /** Cache for compiled permissions (path + required permissions → result) */
2248
+ __publicField(_PathEvent, "permissionCache", /* @__PURE__ */ new Map());
2249
+ /** Max size for permission cache before LRU eviction */
2250
+ __publicField(_PathEvent, "MAX_PERMISSION_CACHE_SIZE", 1e3);
2149
2251
  let PathEvent = _PathEvent;
2150
2252
  class PathEventEmitter {
2151
2253
  constructor(prefix = "") {
@@ -2154,16 +2256,27 @@ ${opts.message || this.desc}`;
2154
2256
  }
2155
2257
  emit(event, ...args) {
2156
2258
  const parsed = event instanceof PathEvent ? event : new PathEvent(`${this.prefix}/${event}`);
2157
- this.listeners.filter((l) => PathEvent.has(l[0], parsed)).forEach(async (l) => l[1](parsed, ...args));
2259
+ this.listeners.filter((l) => PathEvent.has(l[0], parsed)).forEach((l) => l[1](parsed, ...args));
2158
2260
  }
2159
2261
  off(listener) {
2160
2262
  this.listeners = this.listeners.filter((l) => l[1] != listener);
2161
2263
  }
2162
2264
  on(event, listener) {
2163
2265
  makeArray(event).forEach((e) => {
2164
- if (typeof e == "string" && e[0] == "*" && this.prefix) e = e.slice(1);
2266
+ let fullEvent;
2267
+ if (typeof e === "string") {
2268
+ if (e[0] === ":" && this.prefix) {
2269
+ fullEvent = `${this.prefix}${e}`;
2270
+ } else if (this.prefix) {
2271
+ fullEvent = `${this.prefix}/${e}`;
2272
+ } else {
2273
+ fullEvent = e;
2274
+ }
2275
+ } else {
2276
+ fullEvent = e instanceof PathEvent ? PathEvent.toString(e.fullPath, e.methods) : e;
2277
+ }
2165
2278
  this.listeners.push([
2166
- e instanceof PathEvent ? e : new PathEvent(`${this.prefix}/${e}`),
2279
+ new PathEvent(fullEvent),
2167
2280
  listener
2168
2281
  ]);
2169
2282
  });
@@ -2179,7 +2292,7 @@ ${opts.message || this.desc}`;
2179
2292
  });
2180
2293
  }
2181
2294
  relayEvents(emitter) {
2182
- emitter.on("*", (event, ...args) => this.emit(event, ...args));
2295
+ emitter.on("**", (event, ...args) => this.emit(event, ...args));
2183
2296
  }
2184
2297
  }
2185
2298
  function search(rows, search2, regex, transform = (r) => r) {