@fjall/components-infrastructure 35.0.0 → 36.0.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.
@@ -5,6 +5,7 @@ import { RdsAuroraGlobal } from "../../resources/aws/database/rdsAuroraGlobal.js
5
5
  import { DynamoDBTable } from "../../resources/aws/database/dynamodb.js";
6
6
  import { ClickHouseDatabase } from "./clickhouseDatabase.js";
7
7
  import { FjallLogger, warnIfPropertiesIgnored } from "../../utils/validationLogger.js";
8
+ import { DATABASE_WARN_GROUPS } from "./databasePropApplicability.js";
8
9
  import { toPascalCase } from "../../utils/capitaliseString.js";
9
10
  import { ResourceNaming } from "../../utils/resourceNaming.js";
10
11
  import { resolveAlertsTopic as resolveAlertsTopicShared } from "../../utils/resolveAlertsTopic.js";
@@ -127,12 +128,9 @@ function validateRelationalDatabaseProps(props) {
127
128
  throw new Error("GlobalAurora database requires 'primaryRegion'. " +
128
129
  "Specify the AWS region where the primary cluster will be created.");
129
130
  }
130
- // Instance-only options on Aurora/GlobalAurora
131
- warnIfPropertiesIgnored(props, ["readReplica", "multiAz", "allocatedStorage", "iamAuthentication"], "Instance", "Instance database");
132
- // Aurora-only options on Instance
133
- warnIfPropertiesIgnored(props, ["readers", "writer"], ["Aurora", "GlobalAurora"], "Aurora database");
134
- // GlobalAurora-only options on non-global databases
135
- warnIfPropertiesIgnored(props, ["primaryRegion", "secondaryRegions", "enableGlobalWriteForwarding"], "GlobalAurora", "GlobalAurora database");
131
+ for (const group of DATABASE_WARN_GROUPS) {
132
+ warnIfPropertiesIgnored(props, group.properties, group.applicableTypes, group.typeName);
133
+ }
136
134
  }
137
135
  /**
138
136
  * Validates database props and logs warnings for ignored or misconfigured options.
@@ -0,0 +1,15 @@
1
+ import type { AuroraDatabaseProps, GlobalAuroraDatabaseProps, InstanceDatabaseProps } from "./database.js";
2
+ import { type RelationalDatabaseType } from "./interfaces/database.js";
3
+ type AllDatabasePropKeys = keyof AuroraDatabaseProps | keyof InstanceDatabaseProps | keyof GlobalAuroraDatabaseProps;
4
+ export interface DatabaseWarnGroup {
5
+ properties: string[];
6
+ applicableTypes: RelationalDatabaseType[];
7
+ typeName: string;
8
+ }
9
+ export declare const DATABASE_WARN_GROUPS: readonly DatabaseWarnGroup[];
10
+ /**
11
+ * Runtime key set for tests — derived from the compiled guard, never
12
+ * re-listed (typescript-standards § compile-time guards).
13
+ */
14
+ export declare const ALL_DATABASE_PROP_KEYS: AllDatabasePropKeys[];
15
+ export {};
@@ -0,0 +1,98 @@
1
+ import { RELATIONAL_DATABASE_TYPES } from "./interfaces/database.js";
2
+ /**
3
+ * Compile-time lockstep guard for `validateRelationalDatabaseProps`'s
4
+ * ignored-prop warnings — the database twin of `COMPUTE_PROP_APPLICABILITY`.
5
+ * The Record over the full top-level key union rejects BOTH a missing key (a
6
+ * new prop added to any relational props interface fails TS2741 here until
7
+ * classified) and a stale extra key (a removed/renamed prop fails TS2353).
8
+ * `"all"` means the prop is declared on every relational variant and never
9
+ * warns; a list names the variants that declare it, and a prop declared on
10
+ * two of the three variants is first-class here — the shape no
11
+ * single-variant hand list (or Exclude-difference bucket) can express.
12
+ *
13
+ * Applicability is DECLARATION, not type identity: `engine` and `encryption`
14
+ * map `"all"` although their TYPES differ per variant (`IClusterEngine` vs
15
+ * `IInstanceEngine`, `AuroraEncryptionConfig` vs `EncryptionConfig`) — every
16
+ * variant reads them, so they are never ignored and must never warn.
17
+ */
18
+ const DATABASE_PROP_APPLICABILITY = {
19
+ type: "all",
20
+ vpc: "all",
21
+ databaseName: "all",
22
+ instanceType: "all",
23
+ deletionProtection: "all",
24
+ appBackupTier: "all",
25
+ alertsTopic: "all",
26
+ alarms: "all",
27
+ applicationId: "all",
28
+ migrations: "all",
29
+ databaseEngine: "all",
30
+ engine: "all",
31
+ port: "all",
32
+ proxy: "all",
33
+ credentials: "all",
34
+ encryption: "all",
35
+ backupRetention: "all",
36
+ databaseInsights: "all",
37
+ snapshotIdentifier: "all",
38
+ snapshotUsername: "all",
39
+ writer: ["Aurora", "GlobalAurora"],
40
+ readers: ["Aurora", "GlobalAurora"],
41
+ monitoringInterval: ["Aurora", "GlobalAurora"],
42
+ preferredMaintenanceWindow: ["Aurora", "GlobalAurora"],
43
+ removalPolicy: ["Aurora"],
44
+ engineVersion: ["Aurora"],
45
+ clusterIdentifier: ["Aurora"],
46
+ credentialsSecretName: ["Aurora"],
47
+ credentialsKmsAliasName: ["Aurora"],
48
+ serverlessV2MinCapacity: ["Aurora"],
49
+ serverlessV2MaxCapacity: ["Aurora"],
50
+ serverlessV2AutoPauseDuration: ["Aurora"],
51
+ enableDataApi: ["Aurora"],
52
+ allowVpcAccess: ["Aurora"],
53
+ allowedIpCidr: ["Aurora"],
54
+ publiclyAccessible: ["Aurora", "Instance"],
55
+ primaryRegion: ["GlobalAurora"],
56
+ secondaryRegions: ["GlobalAurora"],
57
+ globalClusterIdentifier: ["GlobalAurora"],
58
+ enableGlobalWriteForwarding: ["GlobalAurora"],
59
+ allocatedStorage: ["Instance"],
60
+ multiAz: ["Instance"],
61
+ readReplica: ["Instance"],
62
+ iamAuthentication: ["Instance"]
63
+ };
64
+ /**
65
+ * The variant literals are already the user-facing vocabulary (the
66
+ * `type: "Aurora"` a user writes), so the warning names exactly that —
67
+ * no label table to keep in step. Groups are keyed on the variants in
68
+ * `RELATIONAL_DATABASE_TYPES` order, so two entries declaring the same
69
+ * variants in a different order land in one group with one label.
70
+ */
71
+ const orderedVariants = (variants) => [...variants].sort((a, b) => RELATIONAL_DATABASE_TYPES.indexOf(a) -
72
+ RELATIONAL_DATABASE_TYPES.indexOf(b));
73
+ export const DATABASE_WARN_GROUPS = (() => {
74
+ const groups = new Map();
75
+ for (const [prop, applicability] of Object.entries(DATABASE_PROP_APPLICABILITY)) {
76
+ if (applicability === "all")
77
+ continue;
78
+ const applicableTypes = orderedVariants(applicability);
79
+ const key = applicableTypes.join("+");
80
+ const existing = groups.get(key);
81
+ if (existing !== undefined) {
82
+ existing.properties.push(prop);
83
+ }
84
+ else {
85
+ groups.set(key, {
86
+ properties: [prop],
87
+ applicableTypes,
88
+ typeName: applicableTypes.join(" or ") + " database"
89
+ });
90
+ }
91
+ }
92
+ return [...groups.values()];
93
+ })();
94
+ /**
95
+ * Runtime key set for tests — derived from the compiled guard, never
96
+ * re-listed (typescript-standards § compile-time guards).
97
+ */
98
+ export const ALL_DATABASE_PROP_KEYS = Object.keys(DATABASE_PROP_APPLICABILITY);
@@ -92,32 +92,60 @@ async function findExistingTaskArn(client, clusterArn, startedBy) {
92
92
  return result.taskArns[0];
93
93
  }
94
94
 
95
+ function describeFailureReason(desc) {
96
+ const failure = desc && desc.failures && desc.failures[0];
97
+ return failure && failure.reason ? failure.reason : "no failure reported";
98
+ }
99
+
100
+ // DescribeTasks is eventually consistent with RunTask: for a few seconds
101
+ // after a launch it answers `tasks: []` with a MISSING failure for the very
102
+ // ARN RunTask just returned. Until the task has been seen once, a miss is
103
+ // "not visible yet" and the poll continues; once it has been seen, a miss is
104
+ // the task vanishing, which is loud. A task never seen inside one
105
+ // invocation's poll budget is loud too — that is a hundred seconds, not
106
+ // consistency lag.
95
107
  async function pollTaskUntilStopped(client, clusterArn, taskArn, sleep) {
96
108
  const sleepFn =
97
109
  sleep || ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
110
+ let seen = false;
111
+ let lastMissReason = "no failure reported";
98
112
  for (let i = 0; i < MAX_POLLS_PER_INVOCATION; i++) {
99
113
  const desc = await client.send(
100
114
  new DescribeTasksCommand({ cluster: clusterArn, tasks: [taskArn] })
101
115
  );
102
116
  const task = desc.tasks && desc.tasks[0];
103
- if (!task) {
104
- return { hookStatus: "FAILED", reason: "DescribeTasks returned no task" };
105
- }
106
- if (task.lastStatus === "STOPPED") {
107
- const exitCode =
108
- task.containers && task.containers[0]
109
- ? task.containers[0].exitCode
110
- : undefined;
111
- if (exitCode === 0) return { hookStatus: "SUCCEEDED" };
112
- return {
113
- hookStatus: "FAILED",
114
- reason: `Task exited ${exitCode}: ${task.stoppedReason || "no reason"}`
115
- };
117
+ if (task) {
118
+ seen = true;
119
+ if (task.lastStatus === "STOPPED") {
120
+ const exitCode =
121
+ task.containers && task.containers[0]
122
+ ? task.containers[0].exitCode
123
+ : undefined;
124
+ if (exitCode === 0) return { hookStatus: "SUCCEEDED" };
125
+ return {
126
+ hookStatus: "FAILED",
127
+ reason: `Task exited ${exitCode}: ${task.stoppedReason || "no reason"}`
128
+ };
129
+ }
130
+ } else {
131
+ lastMissReason = describeFailureReason(desc);
132
+ if (seen) {
133
+ return {
134
+ hookStatus: "FAILED",
135
+ reason: `DescribeTasks returned no task after it had been seen (${lastMissReason})`
136
+ };
137
+ }
116
138
  }
117
139
  if (i < MAX_POLLS_PER_INVOCATION - 1) {
118
140
  await sleepFn(POLL_INTERVAL_MS);
119
141
  }
120
142
  }
143
+ if (!seen) {
144
+ return {
145
+ hookStatus: "FAILED",
146
+ reason: `DescribeTasks returned no task in ${MAX_POLLS_PER_INVOCATION} polls (${lastMissReason})`
147
+ };
148
+ }
121
149
  return {
122
150
  hookStatus: "IN_PROGRESS",
123
151
  callBackDelay: HOOK_CALLBACK_DELAY_SECONDS
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjall/components-infrastructure",
3
- "version": "35.0.0",
3
+ "version": "36.0.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/fjall-tech/fjall.git",
@@ -80,8 +80,8 @@
80
80
  },
81
81
  "dependencies": {
82
82
  "@aws-sdk/client-organizations": "^3.1098.0",
83
- "@fjall/generator": "^35.0.0",
84
- "@fjall/util": "^35.0.0",
83
+ "@fjall/generator": "^36.0.0",
84
+ "@fjall/util": "^36.0.0",
85
85
  "constructs": "^10.7.2",
86
86
  "zod": "^4.4.3"
87
87
  },