@microagi/alchemy-gcp 0.3.0 → 0.5.1

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.
Files changed (91) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/lib/ArtifactRegistry/Operations.d.ts +47 -0
  3. package/lib/ArtifactRegistry/Operations.d.ts.map +1 -0
  4. package/lib/ArtifactRegistry/Operations.js +47 -0
  5. package/lib/ArtifactRegistry/Operations.js.map +1 -0
  6. package/lib/ArtifactRegistry/Repository.d.ts +263 -0
  7. package/lib/ArtifactRegistry/Repository.d.ts.map +1 -0
  8. package/lib/ArtifactRegistry/Repository.js +256 -0
  9. package/lib/ArtifactRegistry/Repository.js.map +1 -0
  10. package/lib/ArtifactRegistry/Types.d.ts +32 -0
  11. package/lib/ArtifactRegistry/Types.d.ts.map +1 -0
  12. package/lib/ArtifactRegistry/Types.js +12 -0
  13. package/lib/ArtifactRegistry/Types.js.map +1 -0
  14. package/lib/ArtifactRegistry/Validation.d.ts +43 -0
  15. package/lib/ArtifactRegistry/Validation.d.ts.map +1 -0
  16. package/lib/ArtifactRegistry/Validation.js +95 -0
  17. package/lib/ArtifactRegistry/Validation.js.map +1 -0
  18. package/lib/ArtifactRegistry/index.d.ts +4 -0
  19. package/lib/ArtifactRegistry/index.d.ts.map +1 -0
  20. package/lib/ArtifactRegistry/index.js +2 -0
  21. package/lib/ArtifactRegistry/index.js.map +1 -0
  22. package/lib/Auth/Credentials.d.ts +2 -1
  23. package/lib/Auth/Credentials.d.ts.map +1 -1
  24. package/lib/Auth/Credentials.js +3 -2
  25. package/lib/Auth/Credentials.js.map +1 -1
  26. package/lib/CloudResourceManager/Project.d.ts +1 -1
  27. package/lib/Compute/GlobalAddress.d.ts +1 -1
  28. package/lib/Compute/Network.d.ts +1 -1
  29. package/lib/Compute/SharedVpcHost.d.ts +1 -1
  30. package/lib/Compute/SharedVpcServiceProject.d.ts +1 -1
  31. package/lib/Compute/Subnetwork.d.ts +1 -1
  32. package/lib/Container/Cluster.d.ts +1 -1
  33. package/lib/Container/NodePool.d.ts +1 -1
  34. package/lib/ManagedLustre/Instance.d.ts +1 -1
  35. package/lib/Providers.d.ts +1 -1
  36. package/lib/Providers.d.ts.map +1 -1
  37. package/lib/Providers.js +9 -1
  38. package/lib/Providers.js.map +1 -1
  39. package/lib/Run/Job.d.ts +1 -1
  40. package/lib/Run/Service.d.ts +1 -1
  41. package/lib/ServiceNetworking/PsaConnection.d.ts +1 -1
  42. package/lib/ServiceUsage/ApiEnable.d.ts +1 -1
  43. package/lib/Sqladmin/Database.d.ts +82 -0
  44. package/lib/Sqladmin/Database.d.ts.map +1 -0
  45. package/lib/Sqladmin/Database.js +87 -0
  46. package/lib/Sqladmin/Database.js.map +1 -0
  47. package/lib/Sqladmin/Instance.d.ts +234 -0
  48. package/lib/Sqladmin/Instance.d.ts.map +1 -0
  49. package/lib/Sqladmin/Instance.js +251 -0
  50. package/lib/Sqladmin/Instance.js.map +1 -0
  51. package/lib/Sqladmin/Operations.d.ts +41 -0
  52. package/lib/Sqladmin/Operations.d.ts.map +1 -0
  53. package/lib/Sqladmin/Operations.js +42 -0
  54. package/lib/Sqladmin/Operations.js.map +1 -0
  55. package/lib/Sqladmin/Types.d.ts +82 -0
  56. package/lib/Sqladmin/Types.d.ts.map +1 -0
  57. package/lib/Sqladmin/Types.js +28 -0
  58. package/lib/Sqladmin/Types.js.map +1 -0
  59. package/lib/Sqladmin/User.d.ts +110 -0
  60. package/lib/Sqladmin/User.d.ts.map +1 -0
  61. package/lib/Sqladmin/User.js +156 -0
  62. package/lib/Sqladmin/User.js.map +1 -0
  63. package/lib/Sqladmin/Validation.d.ts +60 -0
  64. package/lib/Sqladmin/Validation.d.ts.map +1 -0
  65. package/lib/Sqladmin/Validation.js +125 -0
  66. package/lib/Sqladmin/Validation.js.map +1 -0
  67. package/lib/Sqladmin/index.d.ts +9 -0
  68. package/lib/Sqladmin/index.d.ts.map +1 -0
  69. package/lib/Sqladmin/index.js +5 -0
  70. package/lib/Sqladmin/index.js.map +1 -0
  71. package/lib/Tags.d.ts +4 -4
  72. package/lib/index.d.ts +2 -0
  73. package/lib/index.d.ts.map +1 -1
  74. package/lib/index.js +2 -0
  75. package/lib/index.js.map +1 -1
  76. package/package.json +2 -2
  77. package/src/ArtifactRegistry/Operations.ts +79 -0
  78. package/src/ArtifactRegistry/Repository.ts +628 -0
  79. package/src/ArtifactRegistry/Types.ts +49 -0
  80. package/src/ArtifactRegistry/Validation.ts +112 -0
  81. package/src/ArtifactRegistry/index.ts +10 -0
  82. package/src/Auth/Credentials.ts +3 -2
  83. package/src/Providers.ts +15 -0
  84. package/src/Sqladmin/Database.ts +209 -0
  85. package/src/Sqladmin/Instance.ts +620 -0
  86. package/src/Sqladmin/Operations.ts +73 -0
  87. package/src/Sqladmin/Types.ts +101 -0
  88. package/src/Sqladmin/User.ts +333 -0
  89. package/src/Sqladmin/Validation.ts +148 -0
  90. package/src/Sqladmin/index.ts +19 -0
  91. package/src/index.ts +2 -0
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Branded literal-union types for Cloud SQL props.
3
+ *
4
+ * Distilled's generated `databaseVersion` field is an open-tailed union
5
+ * (`"POSTGRES_15" | … | (string & {})`) so the GCP server can ship new
6
+ * versions without breaking SDK consumers. For our props we narrow to
7
+ * the subset we actually support — at the moment, the supported
8
+ * Postgres versions for greenfield instances. This gives consumers
9
+ * compile-time autocomplete and rejects typos like `"POSTGES_15"`.
10
+ *
11
+ * MySQL / SQL Server enum members are intentionally omitted — we only
12
+ * provision Postgres. Add a sibling `MySqlVersion` / `SqlServerVersion`
13
+ * type if/when those land as targets.
14
+ */
15
+
16
+ /**
17
+ * Supported Postgres major versions for new Cloud SQL instances.
18
+ *
19
+ * Cloud SQL only allows creating instances on currently-supported
20
+ * majors. As of 2026-06, that's 13–17 for greenfield; we expose 15+
21
+ * since that's what new MicroAGI stacks target. Older versions can be
22
+ * read/managed but should be migrated.
23
+ *
24
+ * @see https://cloud.google.com/sql/docs/postgres/db-versions
25
+ */
26
+ export type PostgresVersion = "POSTGRES_15" | "POSTGRES_16" | "POSTGRES_17";
27
+
28
+ /**
29
+ * Cloud SQL edition. `ENTERPRISE_PLUS` unlocks Data Cache, near-zero
30
+ * downtime planned maintenance, and longer log retention; `ENTERPRISE`
31
+ * is the standard tier.
32
+ *
33
+ * @see https://cloud.google.com/sql/docs/postgres/editions-intro
34
+ */
35
+ export type SqlEdition = "ENTERPRISE" | "ENTERPRISE_PLUS";
36
+
37
+ /**
38
+ * Data-disk type. PD_SSD is the default and what any new workload
39
+ * should pick. PD_HDD is legacy and slower; only useful for archival
40
+ * read replicas.
41
+ *
42
+ * @see https://cloud.google.com/sql/docs/postgres/instance-settings#storage-type-2ndgen
43
+ */
44
+ export type SqlDataDiskType = "PD_SSD" | "PD_HDD";
45
+
46
+ /**
47
+ * Availability type. `ZONAL` runs in one zone; `REGIONAL` runs a
48
+ * standby in a second zone for HA at ~2× the cost.
49
+ *
50
+ * @see https://cloud.google.com/sql/docs/postgres/high-availability
51
+ */
52
+ export type SqlAvailabilityType = "ZONAL" | "REGIONAL";
53
+
54
+ /**
55
+ * Activation policy. `ALWAYS` keeps the instance on; `NEVER` parks
56
+ * it (useful for occasional dev databases).
57
+ *
58
+ * @see https://cloud.google.com/sql/docs/postgres/start-stop-restart-instance
59
+ */
60
+ export type SqlActivationPolicy = "ALWAYS" | "NEVER";
61
+
62
+ /**
63
+ * Cloud SQL user auth type.
64
+ *
65
+ * - `BUILT_IN` — classic username + password Postgres role. The
66
+ * provider requires a `password` Redacted for this type.
67
+ * - `CLOUD_IAM_USER` — a human IAM principal authenticated via the
68
+ * Cloud SQL Auth Proxy / IAM database authn token exchange.
69
+ * - `CLOUD_IAM_SERVICE_ACCOUNT` — a GCP service-account principal,
70
+ * same auth mechanism as `CLOUD_IAM_USER`. The Postgres role name
71
+ * is the SA email **with the trailing `.gserviceaccount.com`
72
+ * stripped** (Cloud SQL caps role names at 63 chars; SA emails are
73
+ * longer than that).
74
+ * - Open tail `(string & {})` keeps forward compatibility for
75
+ * `CLOUD_IAM_GROUP`, `CLOUD_IAM_GROUP_SERVICE_ACCOUNT`, etc.
76
+ *
77
+ * @see https://cloud.google.com/sql/docs/postgres/iam-authentication
78
+ */
79
+ export type SqlUserType =
80
+ | "BUILT_IN"
81
+ | "CLOUD_IAM_USER"
82
+ | "CLOUD_IAM_SERVICE_ACCOUNT"
83
+ | "CLOUD_IAM_GROUP"
84
+ | (string & {});
85
+
86
+ /**
87
+ * Convenience predicate: `true` for any IAM-backed user type, where
88
+ * passwords are forbidden and the principal is a GCP identity.
89
+ *
90
+ * Used by `Validation.ts` and `User.ts` to fan out between the
91
+ * built-in password path and the IAM token-exchange path.
92
+ */
93
+ export const isIamUserType = (
94
+ type: SqlUserType | undefined,
95
+ ): boolean =>
96
+ type === "CLOUD_IAM_USER" ||
97
+ type === "CLOUD_IAM_SERVICE_ACCOUNT" ||
98
+ type === "CLOUD_IAM_GROUP" ||
99
+ type === "CLOUD_IAM_GROUP_USER" ||
100
+ type === "CLOUD_IAM_GROUP_SERVICE_ACCOUNT" ||
101
+ type === "ENTRAID_USER";
@@ -0,0 +1,333 @@
1
+ import * as sql from "@distilled.cloud/gcp/sqladmin-v1";
2
+ import { Resource } from "alchemy";
3
+ import { isResolved, somePropsAreDifferent } from "alchemy/Diff";
4
+ import * as Provider from "alchemy/Provider";
5
+ import * as Effect from "effect/Effect";
6
+ import * as Redacted from "effect/Redacted";
7
+ import type * as GCP from "../Providers.ts";
8
+ import { makeAwaitOperation } from "./Operations.ts";
9
+ import { isIamUserType, type SqlUserType } from "./Types.ts";
10
+ import { reshapeBadRequest, validateUser } from "./Validation.ts";
11
+
12
+ /**
13
+ * A Cloud SQL user (database role + auth binding).
14
+ *
15
+ * **Three auth modes:**
16
+ *
17
+ * - `BUILT_IN` — classic Postgres username/password. Requires
18
+ * `password`. The role exists only inside Postgres.
19
+ * - `CLOUD_IAM_USER` — a human IAM principal. `name` is the user's
20
+ * email. Authenticates via the Cloud SQL Auth Proxy / IAM token
21
+ * exchange; no password.
22
+ * - `CLOUD_IAM_SERVICE_ACCOUNT` — a GCP service account principal.
23
+ * `name` is the SA email with the trailing `.gserviceaccount.com`
24
+ * stripped (Postgres role names cap at 63 chars; full SA emails
25
+ * often exceed that). No password.
26
+ *
27
+ * The Postgres role itself is granted no privileges by default — wire
28
+ * up grants via SQL (psql / migrations) once the user exists. Cloud
29
+ * SQL IAM authn is independent of grant management.
30
+ *
31
+ * **Adoption.** Users carry no labels and no useful comparable shape
32
+ * beyond `(name, host, type)`. `read` returns the observed user if it
33
+ * exists at this address, `undefined` otherwise. No `Unowned` wrapper
34
+ * — adoption is implicit; the worst case is alchemy issuing a no-op
35
+ * patch / a delete on what it thinks is its own.
36
+ *
37
+ * **Replace triggers.** `project`, `instance`, `name`, `host`, `type`.
38
+ * Type changes need a drop/recreate because Cloud SQL stores the auth
39
+ * shape immutably; we surface that via replace.
40
+ *
41
+ * @example IAM service-account user (workload identity → DB role)
42
+ * ```typescript
43
+ * yield* GCP.SqlUser("AppSaUser", {
44
+ * project: instance.project,
45
+ * instance: instance.name,
46
+ * // App SA email is `app@my-project.iam.gserviceaccount.com` — strip
47
+ * // `.gserviceaccount.com` so the Postgres role name fits in 63 chars.
48
+ * name: "app@my-project.iam",
49
+ * type: "CLOUD_IAM_SERVICE_ACCOUNT",
50
+ * });
51
+ * ```
52
+ *
53
+ * @example Built-in user with a Redacted password
54
+ * ```typescript
55
+ * yield* GCP.SqlUser("MigratorUser", {
56
+ * project: instance.project,
57
+ * instance: instance.name,
58
+ * name: "migrator",
59
+ * password: Redacted.make(process.env.MIGRATOR_PASSWORD!),
60
+ * });
61
+ * ```
62
+ *
63
+ * @see https://cloud.google.com/sql/docs/postgres/admin-api/rest/v1/users
64
+ * @see https://cloud.google.com/sql/docs/postgres/iam-authentication
65
+ */
66
+ export type SqlUserProps = {
67
+ /**
68
+ * GCP project ID hosting the parent instance. Immutable — replace.
69
+ * @see https://cloud.google.com/sql/docs/postgres/admin-api/rest/v1/users#User
70
+ */
71
+ project: string;
72
+ /**
73
+ * Parent Cloud SQL instance name. Immutable — replace.
74
+ */
75
+ instance: string;
76
+ /**
77
+ * User/role name. For `BUILT_IN`, any valid Postgres identifier.
78
+ * For `CLOUD_IAM_USER`, the principal's email. For
79
+ * `CLOUD_IAM_SERVICE_ACCOUNT`, the SA email **with the trailing
80
+ * `.gserviceaccount.com` stripped** — Postgres role names cap at
81
+ * 63 chars; full SA emails are typically 70+. Immutable — replace.
82
+ */
83
+ name: string;
84
+ /**
85
+ * Host the user can connect from. PostgreSQL ignores this and
86
+ * defaults to `%` (any host). For MySQL it's load-bearing.
87
+ * Immutable — replace.
88
+ */
89
+ host?: string;
90
+ /**
91
+ * Auth backend. Defaults to `BUILT_IN` if unset.
92
+ * @see https://cloud.google.com/sql/docs/postgres/iam-authentication
93
+ */
94
+ type?: SqlUserType;
95
+ /**
96
+ * Password for `BUILT_IN` users. Required for that type; forbidden
97
+ * for any IAM type (validated at plan time).
98
+ */
99
+ password?: Redacted.Redacted<string>;
100
+ };
101
+
102
+ export type SqlUserAttributes = {
103
+ /** User name. */
104
+ name: string;
105
+ /** Parent instance name. */
106
+ instance: string;
107
+ /** Project ID. */
108
+ project: string;
109
+ /** Connecting host (defaults to `%` for Postgres). */
110
+ host: string;
111
+ /** Auth backend in effect. */
112
+ type: string;
113
+ };
114
+
115
+ export type SqlUser = Resource<
116
+ "GCP.SqlUser",
117
+ SqlUserProps,
118
+ SqlUserAttributes,
119
+ never,
120
+ GCP.Providers
121
+ >;
122
+ export const SqlUser = Resource<SqlUser>("GCP.SqlUser");
123
+
124
+ const toAttributes = (
125
+ u: sql.User,
126
+ parent: { project: string; instance: string; name: string; host: string },
127
+ ): SqlUserAttributes => ({
128
+ name: parent.name,
129
+ instance: parent.instance,
130
+ project: parent.project,
131
+ host: u.host ?? parent.host,
132
+ type: u.type ?? "BUILT_IN",
133
+ });
134
+
135
+ export const SqlUserProvider = () =>
136
+ Provider.effect(
137
+ SqlUser,
138
+ Effect.gen(function* () {
139
+ const getUsers = yield* sql.getUsers;
140
+ const listUsers = yield* sql.listUsers;
141
+ const insertUsers = yield* sql.insertUsers;
142
+ const updateUsers = yield* sql.updateUsers;
143
+ const deleteUsers = yield* sql.deleteUsers;
144
+ const getOperations = yield* sql.getOperations;
145
+ const awaitOperation = makeAwaitOperation(getOperations);
146
+
147
+ /**
148
+ * Observe a user. Cloud SQL's `getUsers` endpoint returns 404
149
+ * for IAM-typed users in some Postgres minor versions — those
150
+ * are listable but not directly addressable by `name`. Fall
151
+ * back to a list filter on 404 to be safe.
152
+ */
153
+ const observe = (
154
+ project: string,
155
+ instance: string,
156
+ name: string,
157
+ host: string | undefined,
158
+ ) =>
159
+ getUsers({
160
+ project,
161
+ instance,
162
+ name,
163
+ ...(host ? { host } : {}),
164
+ }).pipe(
165
+ Effect.catchTag("NotFound", () =>
166
+ listUsers({ project, instance }).pipe(
167
+ Effect.map((res) =>
168
+ (res.items ?? []).find(
169
+ (u) => u.name === name && (host ? u.host === host : true),
170
+ ),
171
+ ),
172
+ Effect.catchTag("NotFound", () =>
173
+ Effect.succeed(undefined as sql.User | undefined),
174
+ ),
175
+ Effect.catchTag("Forbidden", () =>
176
+ Effect.succeed(undefined as sql.User | undefined),
177
+ ),
178
+ ),
179
+ ),
180
+ Effect.catchTag("Forbidden", () =>
181
+ Effect.succeed(undefined as sql.User | undefined),
182
+ ),
183
+ );
184
+
185
+ return {
186
+ stables: ["name", "instance", "project", "host", "type"],
187
+ diff: Effect.fn(function* ({ news, olds = {} }) {
188
+ if (!isResolved(news)) return undefined;
189
+ if (
190
+ somePropsAreDifferent(olds as SqlUserProps, news, [
191
+ "project",
192
+ "instance",
193
+ "name",
194
+ "host",
195
+ "type",
196
+ ])
197
+ ) {
198
+ return { action: "replace" } as const;
199
+ }
200
+ return undefined;
201
+ }),
202
+ reconcile: Effect.fn(function* ({ news, session }) {
203
+ const desiredType = news.type ?? "BUILT_IN";
204
+ const desiredHost = news.host ?? "%";
205
+
206
+ yield* validateUser({
207
+ name: news.name,
208
+ type: desiredType,
209
+ hasPassword: news.password !== undefined,
210
+ });
211
+
212
+ let observed = yield* observe(
213
+ news.project,
214
+ news.instance,
215
+ news.name,
216
+ news.host,
217
+ );
218
+
219
+ if (!observed) {
220
+ const body: sql.User = {
221
+ name: news.name,
222
+ ...(news.host !== undefined ? { host: news.host } : {}),
223
+ type: desiredType,
224
+ ...(news.password
225
+ ? { password: Redacted.value(news.password) }
226
+ : {}),
227
+ };
228
+ const op = yield* insertUsers({
229
+ project: news.project,
230
+ instance: news.instance,
231
+ body,
232
+ }).pipe(
233
+ Effect.catchTag("Conflict", () =>
234
+ Effect.succeed(undefined as sql.Operation | undefined),
235
+ ),
236
+ Effect.catchTag(
237
+ "BadRequest",
238
+ reshapeBadRequest("User", "insert"),
239
+ ),
240
+ );
241
+ if (op?.name) yield* awaitOperation(news.project, op.name, session);
242
+ observed = yield* observe(
243
+ news.project,
244
+ news.instance,
245
+ news.name,
246
+ news.host,
247
+ );
248
+ } else if (
249
+ // Only BUILT_IN users have a rotatable password. For IAM
250
+ // users the type is fixed at insert and there's nothing
251
+ // mutable to sync. We don't store/compare BUILT_IN
252
+ // passwords (they're write-only on the API side) — if the
253
+ // user explicitly supplies one we re-issue the update so
254
+ // a rotated `Redacted` propagates, but we can't detect
255
+ // drift here so this is a deliberate "user is source of
256
+ // truth" path.
257
+ !isIamUserType(desiredType) &&
258
+ news.password
259
+ ) {
260
+ const op = yield* updateUsers({
261
+ project: news.project,
262
+ instance: news.instance,
263
+ name: news.name,
264
+ ...(news.host !== undefined ? { host: news.host } : {}),
265
+ body: {
266
+ name: news.name,
267
+ ...(news.host !== undefined ? { host: news.host } : {}),
268
+ type: desiredType,
269
+ password: Redacted.value(news.password),
270
+ },
271
+ }).pipe(
272
+ Effect.catchTag(
273
+ "BadRequest",
274
+ reshapeBadRequest("User", "update"),
275
+ ),
276
+ );
277
+ if (op.name) yield* awaitOperation(news.project, op.name, session);
278
+ }
279
+
280
+ // observed is non-undefined here: either we just created it
281
+ // and re-observed, or we found it pre-existing. If somehow
282
+ // listUsers still returns nothing (eventual consistency
283
+ // window), synthesise a minimal attribute set from inputs.
284
+ if (!observed) {
285
+ return {
286
+ name: news.name,
287
+ instance: news.instance,
288
+ project: news.project,
289
+ host: desiredHost,
290
+ type: desiredType,
291
+ } satisfies SqlUserAttributes;
292
+ }
293
+ return toAttributes(observed, {
294
+ project: news.project,
295
+ instance: news.instance,
296
+ name: news.name,
297
+ host: desiredHost,
298
+ });
299
+ }),
300
+ delete: Effect.fn(function* ({ output, session }) {
301
+ yield* deleteUsers({
302
+ project: output.project,
303
+ instance: output.instance,
304
+ name: output.name,
305
+ ...(output.host && output.host !== "%" ? { host: output.host } : {}),
306
+ }).pipe(
307
+ Effect.flatMap((op) =>
308
+ op.name
309
+ ? awaitOperation(output.project, op.name, session)
310
+ : Effect.succeed(op),
311
+ ),
312
+ Effect.catchTag("NotFound", () => Effect.void),
313
+ Effect.catchTag("BadRequest", reshapeBadRequest("User", "delete")),
314
+ );
315
+ }),
316
+ read: Effect.fn(function* ({ output, olds }) {
317
+ const project = output?.project ?? olds?.project;
318
+ const instance = output?.instance ?? olds?.instance;
319
+ const name = output?.name ?? olds?.name;
320
+ if (!project || !instance || !name) return undefined;
321
+ const host = output?.host ?? olds?.host;
322
+ const observed = yield* observe(project, instance, name, host);
323
+ if (!observed) return undefined;
324
+ return toAttributes(observed, {
325
+ project,
326
+ instance,
327
+ name,
328
+ host: host ?? "%",
329
+ });
330
+ }),
331
+ };
332
+ }),
333
+ );
@@ -0,0 +1,148 @@
1
+ import { ConfigError } from "@distilled.cloud/gcp";
2
+ import * as Effect from "effect/Effect";
3
+ import { isIamUserType, type SqlUserType } from "./Types.ts";
4
+
5
+ /**
6
+ * Plan-time validation for Cloud SQL resource props. Failing fast with
7
+ * a typed `ConfigError` is friendlier than letting the user wait for
8
+ * Cloud SQL to reject the request, and unlike server-side errors these
9
+ * surface before any state mutation happens.
10
+ *
11
+ * @see https://cloud.google.com/sql/docs/postgres/admin-api/rest/v1/instances
12
+ */
13
+
14
+ /**
15
+ * Instance name regex per
16
+ * <https://cloud.google.com/sql/docs/postgres/admin-api/rest/v1/instances#SqlInstance>:
17
+ * 1–98 lowercase letters/digits/hyphens, must begin with a letter and
18
+ * not end with a hyphen.
19
+ *
20
+ * @internal
21
+ */
22
+ const INSTANCE_NAME_RE = /^[a-z]([-a-z0-9]*[a-z0-9])?$/;
23
+
24
+ /**
25
+ * Database name length cap per
26
+ * <https://cloud.google.com/sql/docs/postgres/admin-api/rest/v1/databases#Database>:
27
+ * 1–64 chars. Postgres has additional character constraints (no NUL,
28
+ * no embedded quotes) but the API rejects those, and they'd require
29
+ * dialect-specific parsing here.
30
+ *
31
+ * @internal
32
+ */
33
+ const DATABASE_NAME_MAX = 64;
34
+
35
+ /**
36
+ * Validate a Cloud SQL instance name. Returns a `ConfigError` on the
37
+ * failure channel if the name is invalid, otherwise succeeds.
38
+ *
39
+ * @see https://cloud.google.com/sql/docs/postgres/admin-api/rest/v1/instances#SqlInstance
40
+ */
41
+ export const validateInstanceName = (
42
+ name: string,
43
+ ): Effect.Effect<void, ConfigError> => {
44
+ if (INSTANCE_NAME_RE.test(name) && name.length >= 1 && name.length <= 98) {
45
+ return Effect.void;
46
+ }
47
+ return Effect.fail(
48
+ new ConfigError({
49
+ message: `Cloud SQL instance name ${JSON.stringify(name)} is invalid: must match /^[a-z]([-a-z0-9]*[a-z0-9])?$/ (1–98 lowercase letters/digits/hyphens, start with letter, no trailing hyphen).`,
50
+ }),
51
+ );
52
+ };
53
+
54
+ /**
55
+ * Validate a Cloud SQL database name (1–64 chars, non-empty).
56
+ *
57
+ * @see https://cloud.google.com/sql/docs/postgres/admin-api/rest/v1/databases#Database
58
+ */
59
+ export const validateDatabaseName = (
60
+ name: string,
61
+ ): Effect.Effect<void, ConfigError> => {
62
+ if (name.length >= 1 && name.length <= DATABASE_NAME_MAX) return Effect.void;
63
+ return Effect.fail(
64
+ new ConfigError({
65
+ message: `Cloud SQL database name ${JSON.stringify(name)} is invalid: must be 1–${DATABASE_NAME_MAX} characters.`,
66
+ }),
67
+ );
68
+ };
69
+
70
+ /**
71
+ * Validate a Cloud SQL user shape — primarily that the password/type
72
+ * combination is consistent:
73
+ *
74
+ * - `BUILT_IN` users MUST carry a password.
75
+ * - Any IAM user type (`CLOUD_IAM_USER`, `CLOUD_IAM_SERVICE_ACCOUNT`,
76
+ * `CLOUD_IAM_GROUP*`, `ENTRAID_USER`) MUST NOT carry a password —
77
+ * the IAM principal is authenticated by token exchange.
78
+ *
79
+ * Other types from the open tail of the enum (forward-compat) skip
80
+ * the password check.
81
+ *
82
+ * @see https://cloud.google.com/sql/docs/postgres/users
83
+ * @see https://cloud.google.com/sql/docs/postgres/iam-authentication
84
+ */
85
+ export const validateUser = (args: {
86
+ name: string;
87
+ type: SqlUserType | undefined;
88
+ hasPassword: boolean;
89
+ }): Effect.Effect<void, ConfigError> => {
90
+ const type = args.type ?? "BUILT_IN";
91
+ if (isIamUserType(type) && args.hasPassword) {
92
+ return Effect.fail(
93
+ new ConfigError({
94
+ message: `Cloud SQL user ${JSON.stringify(args.name)} has type ${JSON.stringify(type)} (IAM-backed) but a password was supplied. IAM users authenticate via the Cloud SQL Auth Proxy / IAM token exchange — drop the \`password\` field.`,
95
+ }),
96
+ );
97
+ }
98
+ if (type === "BUILT_IN" && !args.hasPassword) {
99
+ return Effect.fail(
100
+ new ConfigError({
101
+ message: `Cloud SQL user ${JSON.stringify(args.name)} has type "BUILT_IN" but no password was supplied. BUILT_IN users require a password — pass \`password: Redacted.make("...")\` or switch to an IAM type.`,
102
+ }),
103
+ );
104
+ }
105
+ return Effect.void;
106
+ };
107
+
108
+ /**
109
+ * Reshape a `BadRequest` from a Cloud SQL Admin create/patch/delete
110
+ * into a `ConfigError` with a remediation hint for the top failure
111
+ * modes:
112
+ *
113
+ * 1. **Billing not enabled** — Cloud SQL refuses to provision against
114
+ * a project without billing. Common right after `ApiEnable` lands
115
+ * but before billing reconciles in a fresh project.
116
+ * 2. **Private network not set up** — `privateNetwork` references a
117
+ * VPC that has no PSA peering for `servicenetworking.googleapis.com`.
118
+ * Surface the precondition.
119
+ * 3. **Invalid tier** — typo'd `db-custom-…` or one outside the
120
+ * project's allow-list. Pass the underlying message through with
121
+ * a pointer to the tier catalogue.
122
+ *
123
+ * Anything else propagates verbatim — the underlying GCP message is
124
+ * usually clear enough.
125
+ *
126
+ * @see https://cloud.google.com/sql/docs/postgres/instance-settings
127
+ */
128
+ export const reshapeBadRequest =
129
+ (kind: "Instance" | "Database" | "User", op: "insert" | "patch" | "update" | "delete") =>
130
+ (e: { message?: string }): Effect.Effect<never, ConfigError> => {
131
+ const underlying = e.message ?? `unknown 400 from ${kind} ${op}`;
132
+ let hint = "";
133
+ if (/billing/i.test(underlying)) {
134
+ hint =
135
+ " Cloud SQL requires billing to be enabled on the project. Attach a billing account (see `GCP.Project({ billingAccount })`) and retry.";
136
+ } else if (/private.*service.*access|servicenetworking|peering/i.test(underlying)) {
137
+ hint =
138
+ " The host VPC needs a Private Services Access (PSA) peering with `servicenetworking.googleapis.com` before a private-IP Cloud SQL instance can attach. Provision a `GCP.GlobalAddress` (purpose=VPC_PEERING) and a `GCP.PsaConnection` on the VPC first.";
139
+ } else if (/tier|machine.*type/i.test(underlying)) {
140
+ hint =
141
+ " Check the tier identifier against https://cloud.google.com/sql/docs/postgres/instance-settings — and remember that Enterprise Plus tiers (`db-perf-optimized-*`) require `edition: \"ENTERPRISE_PLUS\"`.";
142
+ }
143
+ return Effect.fail(
144
+ new ConfigError({
145
+ message: `Cloud SQL ${kind} ${op} rejected: ${underlying}.${hint}`,
146
+ }),
147
+ );
148
+ };
@@ -0,0 +1,19 @@
1
+ export { SqlDatabase, SqlDatabaseProvider } from "./Database.ts";
2
+ export type { SqlDatabaseAttributes, SqlDatabaseProps } from "./Database.ts";
3
+ export { SqlInstance, SqlInstanceProvider } from "./Instance.ts";
4
+ export type {
5
+ SqlInstanceAttributes,
6
+ SqlInstanceIpAddress,
7
+ SqlInstanceProps,
8
+ } from "./Instance.ts";
9
+ export { isIamUserType } from "./Types.ts";
10
+ export type {
11
+ PostgresVersion,
12
+ SqlActivationPolicy,
13
+ SqlAvailabilityType,
14
+ SqlDataDiskType,
15
+ SqlEdition,
16
+ SqlUserType,
17
+ } from "./Types.ts";
18
+ export { SqlUser, SqlUserProvider } from "./User.ts";
19
+ export type { SqlUserAttributes, SqlUserProps } from "./User.ts";
package/src/index.ts CHANGED
@@ -7,11 +7,13 @@ export type {
7
7
  GCPAuthConfig,
8
8
  GCPResolvedCredentials,
9
9
  } from "./Auth/AuthProvider.ts";
10
+ export * from "./ArtifactRegistry/index.ts";
10
11
  export * from "./CloudResourceManager/index.ts";
11
12
  export * from "./Compute/index.ts";
12
13
  export * from "./Container/index.ts";
13
14
  export * from "./ManagedLustre/index.ts";
14
15
  export * from "./Run/index.ts";
15
16
  export * from "./ServiceNetworking/index.ts";
17
+ export * from "./Sqladmin/index.ts";
16
18
  export * from "./ServiceUsage/index.ts";
17
19
  export * from "./Providers.ts";