@fjall/util 2.5.0 → 2.7.0

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/.minified CHANGED
@@ -1 +1 @@
1
- 57 files minified at 2026-05-29T04:05:00.235Z
1
+ 59 files minified at 2026-06-01T11:34:54.161Z
package/dist/Config.d.ts CHANGED
@@ -5,6 +5,12 @@ export type ProviderAccount = {
5
5
  environment: string;
6
6
  managed?: boolean;
7
7
  oidcRoleArn?: string;
8
+ /**
9
+ * Home region for this account's infrastructure. Defaults to the org's
10
+ * primaryRegion when absent. Keys the cascade's per-account IPAM pool
11
+ * lookup (accountId+region), so it must be stamped before cascade deploy.
12
+ */
13
+ region?: string;
8
14
  };
9
15
  export type Profile = {
10
16
  type: "sso" | "oidc";
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Result of scanning a list of account names for construct-key collisions.
3
+ * `empty-construct-key` — a name normalises to "" (no alphanumeric content).
4
+ * `duplicate-construct-key` — two names share a construct key (same account
5
+ * case- and separator-insensitively); `existingName` is the first-seen name.
6
+ */
7
+ export type AccountNameCollision = {
8
+ kind: "empty-construct-key";
9
+ name: string;
10
+ } | {
11
+ kind: "duplicate-construct-key";
12
+ name: string;
13
+ existingName: string;
14
+ constructKey: string;
15
+ };
16
+ /**
17
+ * Scan account names for the first construct-key collision in iteration order.
18
+ * Empty-key is checked before duplicate per element. Pure — never throws; the
19
+ * caller decides how to surface a non-null result. Single source for the
20
+ * collision invariant shared by getConfig (synth-time) and buildOrgConfigPayload
21
+ * (runtime); see `accountConstructKey` for the normalisation it builds on.
22
+ */
23
+ export declare function findAccountNameCollision(names: readonly string[]): AccountNameCollision | null;
@@ -0,0 +1 @@
1
+ import{accountConstructKey as r}from"./caseConversion.js";function u(c){const e=new Map;for(const n of c){const t=r(n);if(t==="")return{kind:"empty-construct-key",name:n};const o=e.get(t);if(o!==void 0)return{kind:"duplicate-construct-key",name:n,existingName:o,constructKey:t};e.set(t,n)}return null}export{u as findAccountNameCollision};
@@ -37,3 +37,37 @@ export declare function capitalise(str: string): string;
37
37
  * e.g., "example.com" -> "examplecom"
38
38
  */
39
39
  export declare function getSafeZoneName(zoneName: string): string;
40
+ /**
41
+ * Normalise an account name to its canonical construct key: lowercase with every
42
+ * non-alphanumeric character stripped. Single source of truth for case- and
43
+ * separator-insensitive account identity; matched to the SQL expression in the
44
+ * `ConnectedAwsAccount_org_normalised_name_unique` functional unique index — the two
45
+ * MUST move together.
46
+ *
47
+ * The two normalisers are identical **for ASCII input**, where JS `toLowerCase()`
48
+ * and PostgreSQL `lower()` agree regardless of collation. They are NOT guaranteed
49
+ * identical for non-ASCII input: `lower()` depends on the database `lc_ctype` /
50
+ * collation provider. Empirically (PG 17, 2026-05-30) the deployed RDS default
51
+ * (`en_US.UTF-8`, libc provider) agrees with JS on every tested candidate, but the
52
+ * `C`/`POSIX` locale diverges — e.g. Turkish dotted `İ` (U+0130) folds to `i` in JS
53
+ * but is left intact (then stripped) by `lower()` under `C`. Use
54
+ * `hasAsciiStableConstructKey` at write boundaries to keep the two engines in
55
+ * lock-step regardless of the deployed collation.
56
+ *
57
+ * Pure — never throws; empty-key rejection lives in the consumers.
58
+ * e.g., "My-Prod" -> "myprod", "My Prod" -> "myprod", "Production" -> "production"
59
+ */
60
+ export declare function accountConstructKey(name: string): string;
61
+ /**
62
+ * True when an account name's construct key is drawn only from ASCII alphanumerics —
63
+ * i.e. JS `toLowerCase()` and SQL `lower()` are guaranteed to produce the same key
64
+ * regardless of the database collation, so the `@fjall/util` normaliser and the
65
+ * `ConnectedAwsAccount_org_normalised_name_unique` SQL index cannot diverge on this
66
+ * name. Also rejects names whose construct key is empty.
67
+ *
68
+ * Rejects only genuinely divergent names (e.g. Kelvin sign U+212A or Turkish `İ` as
69
+ * the sole alnum source) — names like "Café-Prod" still pass, because the `é` is
70
+ * stripped identically by `[^a-z0-9]` on both sides
71
+ * (`accountConstructKey("Café-Prod") === "cafprod"`).
72
+ */
73
+ export declare function hasAsciiStableConstructKey(name: string): boolean;
@@ -1 +1 @@
1
- function n(e){return e.replace(/[-_](.)/g,(r,a)=>a.toUpperCase()).replace(/^./,r=>r.toUpperCase())}function t(e){return e.replace(/([A-Z]+)([A-Z][a-z])/g,"$1-$2").replace(/([a-z\d])([A-Z])/g,"$1-$2").replace(/[\s_]+/g,"-").toLowerCase()}function o(e){return t(e).replace(/-/g,"_")}function p(e){return t(e).replace(/-/g,"_").toUpperCase()}function c(e){return e.length===0?e:e.charAt(0).toUpperCase()+e.slice(1)}function i(e){return e.split(".").join("")}export{c as capitalise,i as getSafeZoneName,t as toKebab,n as toPascalCase,p as toScreamingSnake,o as toValidDatabaseName};
1
+ function n(e){return e.replace(/[-_](.)/g,(t,o)=>o.toUpperCase()).replace(/^./,t=>t.toUpperCase())}function a(e){return e.replace(/([A-Z]+)([A-Z][a-z])/g,"$1-$2").replace(/([a-z\d])([A-Z])/g,"$1-$2").replace(/[\s_]+/g,"-").toLowerCase()}function c(e){return a(e).replace(/-/g,"_")}function p(e){return a(e).replace(/-/g,"_").toUpperCase()}function u(e){return e.length===0?e:e.charAt(0).toUpperCase()+e.slice(1)}function i(e){return e.split(".").join("")}function r(e){return e.toLowerCase().replace(/[^a-z0-9]/g,"")}function l(e){const t=e.replace(/\P{ASCII}/gu,"");return r(t)===r(e)&&r(e)!==""}export{r as accountConstructKey,u as capitalise,i as getSafeZoneName,l as hasAsciiStableConstructKey,a as toKebab,n as toPascalCase,p as toScreamingSnake,c as toValidDatabaseName};
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Map over `items` running at most `limit` invocations of `mapper` at once,
3
+ * resolving to results in input order. Mirrors `Promise.allSettled`: the
4
+ * returned promise never rejects — a `mapper` that throws produces a
5
+ * `{ status: "rejected", reason }` slot and the remaining work continues.
6
+ *
7
+ * Use instead of `Promise.allSettled(items.map(mapper))` whenever each task is
8
+ * heavyweight (spawns a process, holds memory, hits a rate-limited API): an
9
+ * unbounded fan-out over N items spawns N tasks at once and falls over at
10
+ * scale, whereas this keeps a fixed working set regardless of N.
11
+ *
12
+ * `limit` is clamped to `[1, items.length]`.
13
+ */
14
+ export declare function mapSettledWithConcurrency<T, R>(items: readonly T[], limit: number, mapper: (item: T, index: number) => Promise<R>): Promise<Array<PromiseSettledResult<R>>>;
@@ -0,0 +1 @@
1
+ async function h(t,c,l){const e=new Array(t.length);if(t.length===0)return e;const o=Math.max(1,Math.min(c,t.length));let a=0;const s=async()=>{for(;a<t.length;){const n=a++;try{const r=await l(t[n],n);e[n]={status:"fulfilled",value:r}}catch(r){e[n]={status:"rejected",reason:r}}}};return await Promise.all(Array.from({length:o},()=>s())),e}export{h as mapSettledWithConcurrency};
package/dist/index.d.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  export { DNS_APEX, getDomainExportNames, type ManagedDomainExports } from "./domainExports.js";
2
- export { toPascalCase, toKebab, toValidDatabaseName, toScreamingSnake, capitalise, getSafeZoneName } from "./caseConversion.js";
2
+ export { toPascalCase, toKebab, toValidDatabaseName, toScreamingSnake, capitalise, getSafeZoneName, accountConstructKey, hasAsciiStableConstructKey } from "./caseConversion.js";
3
+ export { findAccountNameCollision, type AccountNameCollision } from "./accountNameCollision.js";
3
4
  export { normaliseError, getErrorMessage, hasErrorCode, getErrorCode, getErrorStack, formatErrorString } from "./errorUtils.js";
4
5
  export { singleton } from "./singleton.js";
5
6
  export { DANGEROUS_ENV_VARS, filterDangerousEnvVars, maskSensitiveOutput, parseShellArgs } from "./securityHelpers.js";
6
7
  export { sleep } from "./sleep.js";
8
+ export { mapSettledWithConcurrency } from "./concurrency.js";
7
9
  export { STANDARD_ENVIRONMENTS, STANDARD_ENVIRONMENTS_WITH_ROOT, STRUCTURAL_ENVIRONMENTS, type StandardEnvironment, type StandardEnvironmentWithRoot, isValidEnvironment, ENVIRONMENT_LABELS, getEnvironmentLabel, ACCOUNT_ROLES, type AccountRole } from "./environments.js";
8
10
  export { RESOURCE_CATEGORIES, type ResourceCategory, categoriseResource, getExpectedDuration, getFriendlyResourceType } from "./resourceCategorisation.js";
9
11
  export { parseGitRemoteUrl, type GitProvider, type ParsedGitRemote } from "./gitRemoteParser.js";
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{DNS_APEX as o,getDomainExportNames as t}from"./domainExports.js";import{toPascalCase as a,toKebab as S,toValidDatabaseName as _,toScreamingSnake as N,capitalise as i,getSafeZoneName as n}from"./caseConversion.js";import{normaliseError as m,getErrorMessage as s,hasErrorCode as A,getErrorCode as p,getErrorStack as f,formatErrorString as g}from"./errorUtils.js";import{singleton as C}from"./singleton.js";import{DANGEROUS_ENV_VARS as I,filterDangerousEnvVars as x,maskSensitiveOutput as V,parseShellArgs as d}from"./securityHelpers.js";import{sleep as M}from"./sleep.js";import{STANDARD_ENVIRONMENTS as P,STANDARD_ENVIRONMENTS_WITH_ROOT as u,STRUCTURAL_ENVIRONMENTS as c,isValidEnvironment as v,ENVIRONMENT_LABELS as H,getEnvironmentLabel as U,ACCOUNT_ROLES as L}from"./environments.js";import{RESOURCE_CATEGORIES as h,categoriseResource as G,getExpectedDuration as F,getFriendlyResourceType as X}from"./resourceCategorisation.js";import{parseGitRemoteUrl as y}from"./gitRemoteParser.js";import{abbreviateRegion as B}from"./regions.js";import{SCOPE_VALUES as Z}from"./tokenScopes.js";import{deriveRegionsFromOrgConfig as q,deriveTargets as w,deriveAllTargets as z,findTarget as J,generateTargetName as Q}from"./targets.js";import{buildAppConfigPath as $}from"./appPath.js";import{findInfrastructurePaths as re,findBoundaryPath as oe,isInfrastructureFile as te}from"./findInfrastructurePaths.js";import{inferContainerFromCandidates as ae}from"./inferContainerFromCandidates.js";import{RESERVED_APP_NAMES as _e,isReservedAppName as Ne}from"./reservedAppNames.js";import{deriveContentHashTag as ne}from"./deriveContentHashTag.js";import{MIGRATION_SNAPSHOT_NAME_PREFIX as me,EXPECTED_SCHEMA_VERSION_ENV as se,EXPECTED_SCHEMA_VERSION_TOOL_ENV as Ae,EXPECTED_CH_SCHEMA_VERSION_ENV as pe,SCHEMA_ADMIN_USER_ENV as fe,SCHEMA_ADMIN_PASSWORD_ENV as ge,PRISMA_MIGRATION_DIR_RE as Te,CLICKHOUSE_MIGRATION_SKIP_RE as Ce}from"./migration/constants.js";export{L as ACCOUNT_ROLES,Ce as CLICKHOUSE_MIGRATION_SKIP_RE,I as DANGEROUS_ENV_VARS,o as DNS_APEX,H as ENVIRONMENT_LABELS,pe as EXPECTED_CH_SCHEMA_VERSION_ENV,se as EXPECTED_SCHEMA_VERSION_ENV,Ae as EXPECTED_SCHEMA_VERSION_TOOL_ENV,me as MIGRATION_SNAPSHOT_NAME_PREFIX,Te as PRISMA_MIGRATION_DIR_RE,_e as RESERVED_APP_NAMES,h as RESOURCE_CATEGORIES,ge as SCHEMA_ADMIN_PASSWORD_ENV,fe as SCHEMA_ADMIN_USER_ENV,Z as SCOPE_VALUES,P as STANDARD_ENVIRONMENTS,u as STANDARD_ENVIRONMENTS_WITH_ROOT,c as STRUCTURAL_ENVIRONMENTS,B as abbreviateRegion,$ as buildAppConfigPath,i as capitalise,G as categoriseResource,z as deriveAllTargets,ne as deriveContentHashTag,q as deriveRegionsFromOrgConfig,w as deriveTargets,x as filterDangerousEnvVars,oe as findBoundaryPath,re as findInfrastructurePaths,J as findTarget,g as formatErrorString,Q as generateTargetName,t as getDomainExportNames,U as getEnvironmentLabel,p as getErrorCode,s as getErrorMessage,f as getErrorStack,F as getExpectedDuration,X as getFriendlyResourceType,n as getSafeZoneName,A as hasErrorCode,ae as inferContainerFromCandidates,te as isInfrastructureFile,Ne as isReservedAppName,v as isValidEnvironment,V as maskSensitiveOutput,m as normaliseError,y as parseGitRemoteUrl,d as parseShellArgs,C as singleton,M as sleep,S as toKebab,a as toPascalCase,N as toScreamingSnake,_ as toValidDatabaseName};
1
+ import{DNS_APEX as o,getDomainExportNames as t}from"./domainExports.js";import{toPascalCase as a,toKebab as i,toValidDatabaseName as n,toScreamingSnake as S,capitalise as N,getSafeZoneName as _,accountConstructKey as s,hasAsciiStableConstructKey as m}from"./caseConversion.js";import{findAccountNameCollision as A}from"./accountNameCollision.js";import{normaliseError as f,getErrorMessage as C,hasErrorCode as g,getErrorCode as T,getErrorStack as O,formatErrorString as I}from"./errorUtils.js";import{singleton as l}from"./singleton.js";import{DANGEROUS_ENV_VARS as c,filterDangerousEnvVars as V,maskSensitiveOutput as D,parseShellArgs as M}from"./securityHelpers.js";import{sleep as P}from"./sleep.js";import{mapSettledWithConcurrency as H}from"./concurrency.js";import{STANDARD_ENVIRONMENTS as b,STANDARD_ENVIRONMENTS_WITH_ROOT as h,STRUCTURAL_ENVIRONMENTS as L,isValidEnvironment as y,ENVIRONMENT_LABELS as G,getEnvironmentLabel as F,ACCOUNT_ROLES as K}from"./environments.js";import{RESOURCE_CATEGORIES as k,categoriseResource as W,getExpectedDuration as B,getFriendlyResourceType as Z}from"./resourceCategorisation.js";import{parseGitRemoteUrl as q}from"./gitRemoteParser.js";import{abbreviateRegion as z}from"./regions.js";import{SCOPE_VALUES as Q}from"./tokenScopes.js";import{deriveRegionsFromOrgConfig as $,deriveTargets as ee,deriveAllTargets as re,findTarget as oe,generateTargetName as te}from"./targets.js";import{buildAppConfigPath as ae}from"./appPath.js";import{findInfrastructurePaths as ne,findBoundaryPath as Se,isInfrastructureFile as Ne}from"./findInfrastructurePaths.js";import{inferContainerFromCandidates as se}from"./inferContainerFromCandidates.js";import{RESERVED_APP_NAMES as Re,isReservedAppName as Ae}from"./reservedAppNames.js";import{deriveContentHashTag as fe}from"./deriveContentHashTag.js";import{MIGRATION_SNAPSHOT_NAME_PREFIX as ge,EXPECTED_SCHEMA_VERSION_ENV as Te,EXPECTED_SCHEMA_VERSION_TOOL_ENV as Oe,EXPECTED_CH_SCHEMA_VERSION_ENV as Ie,SCHEMA_ADMIN_USER_ENV as xe,SCHEMA_ADMIN_PASSWORD_ENV as le,PRISMA_MIGRATION_DIR_RE as de,CLICKHOUSE_MIGRATION_SKIP_RE as ce}from"./migration/constants.js";export{K as ACCOUNT_ROLES,ce as CLICKHOUSE_MIGRATION_SKIP_RE,c as DANGEROUS_ENV_VARS,o as DNS_APEX,G as ENVIRONMENT_LABELS,Ie as EXPECTED_CH_SCHEMA_VERSION_ENV,Te as EXPECTED_SCHEMA_VERSION_ENV,Oe as EXPECTED_SCHEMA_VERSION_TOOL_ENV,ge as MIGRATION_SNAPSHOT_NAME_PREFIX,de as PRISMA_MIGRATION_DIR_RE,Re as RESERVED_APP_NAMES,k as RESOURCE_CATEGORIES,le as SCHEMA_ADMIN_PASSWORD_ENV,xe as SCHEMA_ADMIN_USER_ENV,Q as SCOPE_VALUES,b as STANDARD_ENVIRONMENTS,h as STANDARD_ENVIRONMENTS_WITH_ROOT,L as STRUCTURAL_ENVIRONMENTS,z as abbreviateRegion,s as accountConstructKey,ae as buildAppConfigPath,N as capitalise,W as categoriseResource,re as deriveAllTargets,fe as deriveContentHashTag,$ as deriveRegionsFromOrgConfig,ee as deriveTargets,V as filterDangerousEnvVars,A as findAccountNameCollision,Se as findBoundaryPath,ne as findInfrastructurePaths,oe as findTarget,I as formatErrorString,te as generateTargetName,t as getDomainExportNames,F as getEnvironmentLabel,T as getErrorCode,C as getErrorMessage,O as getErrorStack,B as getExpectedDuration,Z as getFriendlyResourceType,_ as getSafeZoneName,m as hasAsciiStableConstructKey,g as hasErrorCode,se as inferContainerFromCandidates,Ne as isInfrastructureFile,Ae as isReservedAppName,y as isValidEnvironment,H as mapSettledWithConcurrency,D as maskSensitiveOutput,f as normaliseError,q as parseGitRemoteUrl,M as parseShellArgs,l as singleton,P as sleep,i as toKebab,a as toPascalCase,S as toScreamingSnake,n as toValidDatabaseName};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjall/util",
3
- "version": "2.5.0",
3
+ "version": "2.7.0",
4
4
  "description": "Common utility methods",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -117,5 +117,5 @@
117
117
  "engines": {
118
118
  "node": ">=22.0.0"
119
119
  },
120
- "gitHead": "5c8f0e004f5520c692f2ee2063c3558c2451f2cf"
120
+ "gitHead": "cfcfbb9f546974d62756e257fce012f629db79ce"
121
121
  }