@infracraft/pulumi 1.22.1 → 1.24.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.
@@ -69,13 +69,30 @@ var NeonRoleResourceProvider = class {
69
69
  _pulumi_pulumi.log.warn(`Failed to delete Neon role "${props.name}" (may already be deleted)`);
70
70
  }
71
71
  }
72
+ /**
73
+ * Rotates the password in place when `passwordVersion` changes. This is the
74
+ * only updatable input (everything else replaces), so an update firing means
75
+ * a rotation was requested.
76
+ */
77
+ async update(_id, olds, news) {
78
+ let password = olds.password;
79
+ if (olds.passwordVersion !== news.passwordVersion) {
80
+ _pulumi_pulumi.log.info(`Rotating password for Neon role "${news.name}" (passwordVersion ${olds.passwordVersion ?? "unset"} → ${news.passwordVersion ?? "unset"})`);
81
+ password = await resetRolePassword(new require_neon_client.NeonClient(news.apiKey), news.projectId, news.branchId, news.name);
82
+ }
83
+ return { outs: {
84
+ ...news,
85
+ password
86
+ } };
87
+ }
72
88
  async diff(_id, olds, news) {
73
89
  const replaces = [];
74
90
  if (olds.projectId !== news.projectId) replaces.push("projectId");
75
91
  if (olds.branchId !== news.branchId) replaces.push("branchId");
76
92
  if (olds.name !== news.name) replaces.push("name");
93
+ const rotates = olds.passwordVersion !== news.passwordVersion;
77
94
  return {
78
- changes: replaces.length > 0,
95
+ changes: replaces.length > 0 || rotates,
79
96
  replaces,
80
97
  deleteBeforeReplace: true
81
98
  };
@@ -113,7 +130,8 @@ var NeonRole = class extends _pulumi_pulumi.ComponentResource {
113
130
  projectId: project.id,
114
131
  branchId: branch.id,
115
132
  name: args.name,
116
- resetPassword: args.resetPassword ?? false
133
+ resetPassword: args.resetPassword ?? false,
134
+ passwordVersion: args.passwordVersion
117
135
  }, { parent: this });
118
136
  this.password = resource.password;
119
137
  this.registerOutputs({ password: this.password });
@@ -1 +1 @@
1
- {"version":3,"file":"role.cjs","names":["NeonClient","pulumi"],"sources":["../../src/neon/role.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport type { NeonBranch } from \"./branch\";\nimport { NeonClient } from \"./client\";\nimport type { NeonProject } from \"./project\";\nimport type { NeonProvider } from \"./provider\";\n\n/** Resolved inputs for the Neon role dynamic provider. */\nexport interface NeonRoleInputs {\n\t/** Neon API key. */\n\tapiKey: string;\n\n\t/** Neon project ID. */\n\tprojectId: string;\n\n\t/** Branch ID the role belongs to. */\n\tbranchId: string;\n\n\t/** Role name (e.g. `\"neondb_owner\"`). */\n\tname: string;\n\n\t/**\n\t * When the role already exists (adopted), reset its password to a fresh value\n\t * instead of revealing the inherited one. Use on copy-on-write branches to\n\t * isolate the credential from the parent branch. Ignored when the role is\n\t * freshly created (a new role already gets its own password).\n\t */\n\tresetPassword: boolean;\n}\n\n/** Persisted state for the Neon role. */\ninterface NeonRoleOutputs extends NeonRoleInputs {\n\t/** Role password (auto-generated by Neon). */\n\tpassword: string;\n}\n\n/** Neon API response for the reveal_password endpoint. */\ninterface PasswordResponse {\n\tpassword: string;\n}\n\n/** Neon API response for the reset_password endpoint (nested under `role`). */\ninterface ResetPasswordResponse {\n\trole?: { password?: string };\n\tpassword?: string;\n}\n\n/** Neon API response for listing roles. */\ninterface RoleListResponse {\n\troles: Array<{\n\t\tname: string;\n\t}>;\n}\n\n/**\n * Checks if a role exists by name on a branch.\n */\nasync function roleExists(\n\tclient: NeonClient,\n\tprojectId: string,\n\tbranchId: string,\n\tname: string,\n): Promise<boolean> {\n\tconst result = await client.get<RoleListResponse>(\n\t\t`/projects/${projectId}/branches/${branchId}/roles`,\n\t);\n\n\treturn result.roles.some((r) => r.name === name);\n}\n\n/**\n * Reveals the password for an existing role via the dedicated API endpoint.\n */\nasync function revealPassword(\n\tclient: NeonClient,\n\tprojectId: string,\n\tbranchId: string,\n\tname: string,\n): Promise<string> {\n\tconst result = await client.get<PasswordResponse>(\n\t\t`/projects/${projectId}/branches/${branchId}/roles/${name}/reveal_password`,\n\t);\n\n\treturn result.password;\n}\n\n/**\n * Resets an existing role's password to a fresh value and returns it.\n * Used to isolate an adopted (copy-on-write) role from the parent branch's credential.\n */\nasync function resetRolePassword(\n\tclient: NeonClient,\n\tprojectId: string,\n\tbranchId: string,\n\tname: string,\n): Promise<string> {\n\tconst result = await client.post<ResetPasswordResponse>(\n\t\t`/projects/${projectId}/branches/${branchId}/roles/${name}/reset_password`,\n\t\t{},\n\t);\n\n\tconst password = result.role?.password ?? result.password;\n\n\tif (!password) {\n\t\tthrow new Error(\n\t\t\t`Neon reset_password returned no password for role \"${name}\"`,\n\t\t);\n\t}\n\n\treturn password;\n}\n\n/**\n * Dynamic provider implementing CRUD for Neon database roles.\n *\n * Uses adopt-or-create on `create()`: finds an existing role by name\n * before creating a new one.\n *\n * @internal Exported only for unit testing; not part of the public API surface.\n */\nexport class NeonRoleResourceProvider\n\timplements pulumi.dynamic.ResourceProvider\n{\n\tasync create(inputs: NeonRoleInputs): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new NeonClient(inputs.apiKey);\n\n\t\tconst exists = await roleExists(\n\t\t\tclient,\n\t\t\tinputs.projectId,\n\t\t\tinputs.branchId,\n\t\t\tinputs.name,\n\t\t);\n\n\t\tif (!exists) {\n\t\t\tawait client.post(\n\t\t\t\t`/projects/${inputs.projectId}/branches/${inputs.branchId}/roles`,\n\t\t\t\t{ role: { name: inputs.name } },\n\t\t\t);\n\t\t} else if (inputs.resetPassword) {\n\t\t\tpulumi.log.info(\n\t\t\t\t`Resetting password for adopted Neon role \"${inputs.name}\" to isolate it from the parent branch`,\n\t\t\t);\n\t\t} else {\n\t\t\tpulumi.log.info(`Adopting existing Neon role \"${inputs.name}\"`);\n\t\t}\n\n\t\tconst password =\n\t\t\texists && inputs.resetPassword\n\t\t\t\t? await resetRolePassword(\n\t\t\t\t\t\tclient,\n\t\t\t\t\t\tinputs.projectId,\n\t\t\t\t\t\tinputs.branchId,\n\t\t\t\t\t\tinputs.name,\n\t\t\t\t\t)\n\t\t\t\t: await revealPassword(\n\t\t\t\t\t\tclient,\n\t\t\t\t\t\tinputs.projectId,\n\t\t\t\t\t\tinputs.branchId,\n\t\t\t\t\t\tinputs.name,\n\t\t\t\t\t);\n\n\t\treturn {\n\t\t\tid: `${inputs.branchId}/${inputs.name}`,\n\t\t\touts: { ...inputs, password },\n\t\t};\n\t}\n\n\tasync read(\n\t\tid: string,\n\t\tprops: NeonRoleOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new NeonClient(props.apiKey);\n\n\t\tconst password = await revealPassword(\n\t\t\tclient,\n\t\t\tprops.projectId,\n\t\t\tprops.branchId,\n\t\t\tprops.name,\n\t\t);\n\n\t\treturn {\n\t\t\tid,\n\t\t\tprops: { ...props, password },\n\t\t};\n\t}\n\n\tasync delete(_id: string, props: NeonRoleOutputs): Promise<void> {\n\t\tconst client = new NeonClient(props.apiKey);\n\n\t\ttry {\n\t\t\tawait client.delete(\n\t\t\t\t`/projects/${props.projectId}/branches/${props.branchId}/roles/${props.name}`,\n\t\t\t);\n\t\t} catch {\n\t\t\tpulumi.log.warn(\n\t\t\t\t`Failed to delete Neon role \"${props.name}\" (may already be deleted)`,\n\t\t\t);\n\t\t}\n\t}\n\n\tasync diff(\n\t\t_id: string,\n\t\tolds: NeonRoleOutputs,\n\t\tnews: NeonRoleInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\treplaces.push(\"projectId\");\n\t\t}\n\n\t\tif (olds.branchId !== news.branchId) {\n\t\t\treplaces.push(\"branchId\");\n\t\t}\n\n\t\tif (olds.name !== news.name) {\n\t\t\treplaces.push(\"name\");\n\t\t}\n\n\t\treturn {\n\t\t\tchanges: replaces.length > 0,\n\t\t\treplaces,\n\t\t\tdeleteBeforeReplace: true,\n\t\t};\n\t}\n}\n\n/** Internal dynamic resource — not part of the public API. */\nclass NeonRoleResource extends pulumi.dynamic.Resource {\n\tpublic declare readonly password: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\tapiKey: pulumi.Input<string>;\n\t\t\tprojectId: pulumi.Input<string>;\n\t\t\tbranchId: pulumi.Input<string>;\n\t\t\tname: pulumi.Input<string>;\n\t\t\tresetPassword: pulumi.Input<boolean>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\t// `password` is an output-only secret. Declaring it via additionalSecretOutputs\n\t\t// (rather than a pulumi.secret(undefined) input placeholder) is the only reliable\n\t\t// way to mark a dynamic-provider output secret: the placeholder gives the property\n\t\t// both a secret-undefined input and a real output, and a downstream dynamic resource\n\t\t// consuming it can race onto the undefined, producing \"Unexpected struct type\"\n\t\t// during protobuf serialization (Pulumi #16041, #3012).\n\t\tsuper(\n\t\t\tnew NeonRoleResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, password: undefined },\n\t\t\t{ ...opts, additionalSecretOutputs: [\"password\"] },\n\t\t);\n\t}\n}\n\n/** Options type for NeonRole — replaces Pulumi's native `provider` field. */\ntype NeonRoleOptions = Omit<pulumi.ComponentResourceOptions, \"provider\"> & {\n\t/** Neon authentication context. */\n\tprovider: NeonProvider;\n\n\t/** Neon project context. */\n\tproject: NeonProject;\n\n\t/** Neon branch context. */\n\tbranch: NeonBranch;\n};\n\n/** Args for NeonRole. */\nexport interface NeonRoleArgs {\n\t/** Role name (e.g. `\"neondb_owner\"`). */\n\tname: pulumi.Input<string>;\n\n\t/**\n\t * When the role is adopted (already exists on the branch), reset its password to a\n\t * fresh value instead of inheriting the parent's. Set this on copy-on-write\n\t * (non-production) branches to isolate the credential from production. Defaults to\n\t * `false` (adopt and reveal the existing password). The reset runs once, at creation.\n\t */\n\tresetPassword?: pulumi.Input<boolean>;\n}\n\n/**\n * Manages a Neon database role with adopt-or-create semantics.\n * Exposes `password` as a secret output for connection string composition.\n *\n * @example\n * ```typescript\n * const role = new NeonRole(\"owner\", {\n * name: \"neondb_owner\",\n * }, { provider, project, branch });\n * ```\n */\nexport class NeonRole extends pulumi.ComponentResource {\n\t/** Role password (secret). */\n\tpublic readonly password: pulumi.Output<string>;\n\n\tconstructor(name: string, args: NeonRoleArgs, opts: NeonRoleOptions) {\n\t\tconst { provider, project, branch, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:neon:Role\", name, {}, pulumiOpts);\n\n\t\tconst resource = new NeonRoleResource(\n\t\t\t`${name}-resource`,\n\t\t\t{\n\t\t\t\tapiKey: provider.apiKey,\n\t\t\t\tprojectId: project.id,\n\t\t\t\tbranchId: branch.id,\n\t\t\t\tname: args.name,\n\t\t\t\tresetPassword: args.resetPassword ?? false,\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.password = resource.password;\n\n\t\tthis.registerOutputs({ password: this.password });\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"role.cjs","names":["NeonClient","pulumi"],"sources":["../../src/neon/role.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport type { NeonBranch } from \"./branch\";\nimport { NeonClient } from \"./client\";\nimport type { NeonProject } from \"./project\";\nimport type { NeonProvider } from \"./provider\";\n\n/** Resolved inputs for the Neon role dynamic provider. */\nexport interface NeonRoleInputs {\n\t/** Neon API key. */\n\tapiKey: string;\n\n\t/** Neon project ID. */\n\tprojectId: string;\n\n\t/** Branch ID the role belongs to. */\n\tbranchId: string;\n\n\t/** Role name (e.g. `\"neondb_owner\"`). */\n\tname: string;\n\n\t/**\n\t * When the role already exists (adopted), reset its password to a fresh value\n\t * instead of revealing the inherited one. Use on copy-on-write branches to\n\t * isolate the credential from the parent branch. Ignored when the role is\n\t * freshly created (a new role already gets its own password).\n\t */\n\tresetPassword: boolean;\n\n\t/**\n\t * Rotation handle: bumping this number resets the role's password IN PLACE on\n\t * the next `up` (an update, never a replace — replacing would try to delete\n\t * the role, which Neon refuses for default roles and which would drop real\n\t * grants for others). Leave unset until the first rotation is needed.\n\t */\n\tpasswordVersion?: number;\n}\n\n/** Persisted state for the Neon role. */\ninterface NeonRoleOutputs extends NeonRoleInputs {\n\t/** Role password (auto-generated by Neon). */\n\tpassword: string;\n}\n\n/** Neon API response for the reveal_password endpoint. */\ninterface PasswordResponse {\n\tpassword: string;\n}\n\n/** Neon API response for the reset_password endpoint (nested under `role`). */\ninterface ResetPasswordResponse {\n\trole?: { password?: string };\n\tpassword?: string;\n}\n\n/** Neon API response for listing roles. */\ninterface RoleListResponse {\n\troles: Array<{\n\t\tname: string;\n\t}>;\n}\n\n/**\n * Checks if a role exists by name on a branch.\n */\nasync function roleExists(\n\tclient: NeonClient,\n\tprojectId: string,\n\tbranchId: string,\n\tname: string,\n): Promise<boolean> {\n\tconst result = await client.get<RoleListResponse>(\n\t\t`/projects/${projectId}/branches/${branchId}/roles`,\n\t);\n\n\treturn result.roles.some((r) => r.name === name);\n}\n\n/**\n * Reveals the password for an existing role via the dedicated API endpoint.\n */\nasync function revealPassword(\n\tclient: NeonClient,\n\tprojectId: string,\n\tbranchId: string,\n\tname: string,\n): Promise<string> {\n\tconst result = await client.get<PasswordResponse>(\n\t\t`/projects/${projectId}/branches/${branchId}/roles/${name}/reveal_password`,\n\t);\n\n\treturn result.password;\n}\n\n/**\n * Resets an existing role's password to a fresh value and returns it.\n * Used to isolate an adopted (copy-on-write) role from the parent branch's credential.\n */\nasync function resetRolePassword(\n\tclient: NeonClient,\n\tprojectId: string,\n\tbranchId: string,\n\tname: string,\n): Promise<string> {\n\tconst result = await client.post<ResetPasswordResponse>(\n\t\t`/projects/${projectId}/branches/${branchId}/roles/${name}/reset_password`,\n\t\t{},\n\t);\n\n\tconst password = result.role?.password ?? result.password;\n\n\tif (!password) {\n\t\tthrow new Error(\n\t\t\t`Neon reset_password returned no password for role \"${name}\"`,\n\t\t);\n\t}\n\n\treturn password;\n}\n\n/**\n * Dynamic provider implementing CRUD for Neon database roles.\n *\n * Uses adopt-or-create on `create()`: finds an existing role by name\n * before creating a new one.\n *\n * @internal Exported only for unit testing; not part of the public API surface.\n */\nexport class NeonRoleResourceProvider\n\timplements pulumi.dynamic.ResourceProvider\n{\n\tasync create(inputs: NeonRoleInputs): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new NeonClient(inputs.apiKey);\n\n\t\tconst exists = await roleExists(\n\t\t\tclient,\n\t\t\tinputs.projectId,\n\t\t\tinputs.branchId,\n\t\t\tinputs.name,\n\t\t);\n\n\t\tif (!exists) {\n\t\t\tawait client.post(\n\t\t\t\t`/projects/${inputs.projectId}/branches/${inputs.branchId}/roles`,\n\t\t\t\t{ role: { name: inputs.name } },\n\t\t\t);\n\t\t} else if (inputs.resetPassword) {\n\t\t\tpulumi.log.info(\n\t\t\t\t`Resetting password for adopted Neon role \"${inputs.name}\" to isolate it from the parent branch`,\n\t\t\t);\n\t\t} else {\n\t\t\tpulumi.log.info(`Adopting existing Neon role \"${inputs.name}\"`);\n\t\t}\n\n\t\tconst password =\n\t\t\texists && inputs.resetPassword\n\t\t\t\t? await resetRolePassword(\n\t\t\t\t\t\tclient,\n\t\t\t\t\t\tinputs.projectId,\n\t\t\t\t\t\tinputs.branchId,\n\t\t\t\t\t\tinputs.name,\n\t\t\t\t\t)\n\t\t\t\t: await revealPassword(\n\t\t\t\t\t\tclient,\n\t\t\t\t\t\tinputs.projectId,\n\t\t\t\t\t\tinputs.branchId,\n\t\t\t\t\t\tinputs.name,\n\t\t\t\t\t);\n\n\t\treturn {\n\t\t\tid: `${inputs.branchId}/${inputs.name}`,\n\t\t\touts: { ...inputs, password },\n\t\t};\n\t}\n\n\tasync read(\n\t\tid: string,\n\t\tprops: NeonRoleOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new NeonClient(props.apiKey);\n\n\t\tconst password = await revealPassword(\n\t\t\tclient,\n\t\t\tprops.projectId,\n\t\t\tprops.branchId,\n\t\t\tprops.name,\n\t\t);\n\n\t\treturn {\n\t\t\tid,\n\t\t\tprops: { ...props, password },\n\t\t};\n\t}\n\n\tasync delete(_id: string, props: NeonRoleOutputs): Promise<void> {\n\t\tconst client = new NeonClient(props.apiKey);\n\n\t\ttry {\n\t\t\tawait client.delete(\n\t\t\t\t`/projects/${props.projectId}/branches/${props.branchId}/roles/${props.name}`,\n\t\t\t);\n\t\t} catch {\n\t\t\tpulumi.log.warn(\n\t\t\t\t`Failed to delete Neon role \"${props.name}\" (may already be deleted)`,\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Rotates the password in place when `passwordVersion` changes. This is the\n\t * only updatable input (everything else replaces), so an update firing means\n\t * a rotation was requested.\n\t */\n\tasync update(\n\t\t_id: string,\n\t\tolds: NeonRoleOutputs,\n\t\tnews: NeonRoleInputs,\n\t): Promise<pulumi.dynamic.UpdateResult> {\n\t\tlet password = olds.password;\n\n\t\tif (olds.passwordVersion !== news.passwordVersion) {\n\t\t\tpulumi.log.info(\n\t\t\t\t`Rotating password for Neon role \"${news.name}\" (passwordVersion ${olds.passwordVersion ?? \"unset\"} → ${news.passwordVersion ?? \"unset\"})`,\n\t\t\t);\n\n\t\t\tconst client = new NeonClient(news.apiKey);\n\n\t\t\tpassword = await resetRolePassword(\n\t\t\t\tclient,\n\t\t\t\tnews.projectId,\n\t\t\t\tnews.branchId,\n\t\t\t\tnews.name,\n\t\t\t);\n\t\t}\n\n\t\treturn { outs: { ...news, password } };\n\t}\n\n\tasync diff(\n\t\t_id: string,\n\t\tolds: NeonRoleOutputs,\n\t\tnews: NeonRoleInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\treplaces.push(\"projectId\");\n\t\t}\n\n\t\tif (olds.branchId !== news.branchId) {\n\t\t\treplaces.push(\"branchId\");\n\t\t}\n\n\t\tif (olds.name !== news.name) {\n\t\t\treplaces.push(\"name\");\n\t\t}\n\n\t\tconst rotates = olds.passwordVersion !== news.passwordVersion;\n\n\t\treturn {\n\t\t\tchanges: replaces.length > 0 || rotates,\n\t\t\treplaces,\n\t\t\tdeleteBeforeReplace: true,\n\t\t};\n\t}\n}\n\n/** Internal dynamic resource — not part of the public API. */\nclass NeonRoleResource extends pulumi.dynamic.Resource {\n\tpublic declare readonly password: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\tapiKey: pulumi.Input<string>;\n\t\t\tprojectId: pulumi.Input<string>;\n\t\t\tbranchId: pulumi.Input<string>;\n\t\t\tname: pulumi.Input<string>;\n\t\t\tresetPassword: pulumi.Input<boolean>;\n\t\t\tpasswordVersion?: pulumi.Input<number>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\t// `password` is an output-only secret. Declaring it via additionalSecretOutputs\n\t\t// (rather than a pulumi.secret(undefined) input placeholder) is the only reliable\n\t\t// way to mark a dynamic-provider output secret: the placeholder gives the property\n\t\t// both a secret-undefined input and a real output, and a downstream dynamic resource\n\t\t// consuming it can race onto the undefined, producing \"Unexpected struct type\"\n\t\t// during protobuf serialization (Pulumi #16041, #3012).\n\t\tsuper(\n\t\t\tnew NeonRoleResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, password: undefined },\n\t\t\t{ ...opts, additionalSecretOutputs: [\"password\"] },\n\t\t);\n\t}\n}\n\n/** Options type for NeonRole — replaces Pulumi's native `provider` field. */\ntype NeonRoleOptions = Omit<pulumi.ComponentResourceOptions, \"provider\"> & {\n\t/** Neon authentication context. */\n\tprovider: NeonProvider;\n\n\t/** Neon project context. */\n\tproject: NeonProject;\n\n\t/** Neon branch context. */\n\tbranch: NeonBranch;\n};\n\n/** Args for NeonRole. */\nexport interface NeonRoleArgs {\n\t/** Role name (e.g. `\"neondb_owner\"`). */\n\tname: pulumi.Input<string>;\n\n\t/**\n\t * When the role is adopted (already exists on the branch), reset its password to a\n\t * fresh value instead of inheriting the parent's. Set this on copy-on-write\n\t * (non-production) branches to isolate the credential from production. Defaults to\n\t * `false` (adopt and reveal the existing password). The reset runs once, at creation.\n\t */\n\tresetPassword?: pulumi.Input<boolean>;\n\n\t/**\n\t * Rotation handle: bump to rotate the role's password in place on the next\n\t * `up`. Everything that consumes `password` (connection strings, service env\n\t * vars, dependent redeploys) cascades automatically.\n\t */\n\tpasswordVersion?: pulumi.Input<number>;\n}\n\n/**\n * Manages a Neon database role with adopt-or-create semantics.\n * Exposes `password` as a secret output for connection string composition.\n *\n * @example\n * ```typescript\n * const role = new NeonRole(\"owner\", {\n * name: \"neondb_owner\",\n * }, { provider, project, branch });\n * ```\n */\nexport class NeonRole extends pulumi.ComponentResource {\n\t/** Role password (secret). */\n\tpublic readonly password: pulumi.Output<string>;\n\n\tconstructor(name: string, args: NeonRoleArgs, opts: NeonRoleOptions) {\n\t\tconst { provider, project, branch, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:neon:Role\", name, {}, pulumiOpts);\n\n\t\tconst resource = new NeonRoleResource(\n\t\t\t`${name}-resource`,\n\t\t\t{\n\t\t\t\tapiKey: provider.apiKey,\n\t\t\t\tprojectId: project.id,\n\t\t\t\tbranchId: branch.id,\n\t\t\t\tname: args.name,\n\t\t\t\tresetPassword: args.resetPassword ?? false,\n\t\t\t\tpasswordVersion: args.passwordVersion,\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.password = resource.password;\n\n\t\tthis.registerOutputs({ password: this.password });\n\t}\n}\n"],"mappings":"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"}
@@ -22,6 +22,13 @@ interface NeonRoleInputs {
22
22
  * freshly created (a new role already gets its own password).
23
23
  */
24
24
  resetPassword: boolean;
25
+ /**
26
+ * Rotation handle: bumping this number resets the role's password IN PLACE on
27
+ * the next `up` (an update, never a replace — replacing would try to delete
28
+ * the role, which Neon refuses for default roles and which would drop real
29
+ * grants for others). Leave unset until the first rotation is needed.
30
+ */
31
+ passwordVersion?: number;
25
32
  }
26
33
  /** Persisted state for the Neon role. */
27
34
  interface NeonRoleOutputs extends NeonRoleInputs {
@@ -40,6 +47,12 @@ declare class NeonRoleResourceProvider implements pulumi.dynamic.ResourceProvide
40
47
  create(inputs: NeonRoleInputs): Promise<pulumi.dynamic.CreateResult>;
41
48
  read(id: string, props: NeonRoleOutputs): Promise<pulumi.dynamic.ReadResult>;
42
49
  delete(_id: string, props: NeonRoleOutputs): Promise<void>;
50
+ /**
51
+ * Rotates the password in place when `passwordVersion` changes. This is the
52
+ * only updatable input (everything else replaces), so an update firing means
53
+ * a rotation was requested.
54
+ */
55
+ update(_id: string, olds: NeonRoleOutputs, news: NeonRoleInputs): Promise<pulumi.dynamic.UpdateResult>;
43
56
  diff(_id: string, olds: NeonRoleOutputs, news: NeonRoleInputs): Promise<pulumi.dynamic.DiffResult>;
44
57
  }
45
58
  /** Options type for NeonRole — replaces Pulumi's native `provider` field. */
@@ -59,6 +72,12 @@ interface NeonRoleArgs {
59
72
  * `false` (adopt and reveal the existing password). The reset runs once, at creation.
60
73
  */
61
74
  resetPassword?: pulumi.Input<boolean>;
75
+ /**
76
+ * Rotation handle: bump to rotate the role's password in place on the next
77
+ * `up`. Everything that consumes `password` (connection strings, service env
78
+ * vars, dependent redeploys) cascades automatically.
79
+ */
80
+ passwordVersion?: pulumi.Input<number>;
62
81
  }
63
82
  /**
64
83
  * Manages a Neon database role with adopt-or-create semantics.
@@ -1 +1 @@
1
- {"version":3,"file":"role.d.cts","names":[],"sources":["../../src/neon/role.ts"],"mappings":";;;;;;;;UAOiB,cAAA;;EAEhB,MAAA;EAF8B;EAK9B,SAAA;EAL8B;EAQ9B,QAAA;EAHA;EAMA,IAAA;EAAA;;;AAQa;AACb;;EADA,aAAA;AAAA;AAMQ;AAAA,UAFC,eAAA,SAAwB,cAAc;EA0F/C;EAxFA,QAAQ;AAAA;;;;;;;;;cAuFI,wBAAA,YACD,MAAA,CAAO,OAAA,CAAQ,gBAAA;EAEpB,MAAA,CAAO,MAAA,EAAQ,cAAA,GAAiB,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EA4CvD,IAAA,CACL,EAAA,UACA,KAAA,EAAO,eAAA,GACL,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;EAgBpB,MAAA,CAAO,GAAA,UAAa,KAAA,EAAO,eAAA,GAAkB,OAAA;EAc7C,IAAA,CACL,GAAA,UACA,IAAA,EAAM,eAAA,EACN,IAAA,EAAM,cAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;AAAA;;KAsDtB,eAAA,GAAkB,IAAA,CAAK,MAAA,CAAO,wBAAA;EAzIR,mCA2I1B,QAAA,EAAU,YAAA,EAzIW;EA4IrB,OAAA,EAAS,WAAA,EA5I6B;EA+ItC,MAAA,EAAQ,UAAA;AAAA;;UAIQ,YAAA;EAtGf;EAwGD,IAAA,EAAM,MAAA,CAAO,KAAA;EAvGZ;;;;;;EA+GD,aAAA,GAAgB,MAAA,CAAO,KAAK;AAAA;;;;;;;;;;;;cAchB,QAAA,SAAiB,MAAA,CAAO,iBAAA;EA1FA;EAAA,SA4FpB,QAAA,EAAU,MAAA,CAAO,MAAA;cAErB,IAAA,UAAc,IAAA,EAAM,YAAA,EAAc,IAAA,EAAM,eAAA;AAAA"}
1
+ {"version":3,"file":"role.d.cts","names":[],"sources":["../../src/neon/role.ts"],"mappings":";;;;;;;;UAOiB,cAAA;;EAEhB,MAAA;EAF8B;EAK9B,SAAA;EAL8B;EAQ9B,QAAA;EAHA;EAMA,IAAA;EAAA;;;;AAgBe;AACf;EATA,aAAA;;;AAcQ;AAuFT;;;EA7FC,eAAA;AAAA;;UAIS,eAAA,SAAwB,cAAc;EA2IpC;EAzIX,QAAQ;AAAA;;;;;;;;;cAuFI,wBAAA,YACD,MAAA,CAAO,OAAA,CAAQ,gBAAA;EAEpB,MAAA,CAAO,MAAA,EAAQ,cAAA,GAAiB,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EA4CvD,IAAA,CACL,EAAA,UACA,KAAA,EAAO,eAAA,GACL,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;EAgBpB,MAAA,CAAO,GAAA,UAAa,KAAA,EAAO,eAAA,GAAkB,OAAA;EAjExC;;;;;EAoFL,MAAA,CACL,GAAA,UACA,IAAA,EAAM,eAAA,EACN,IAAA,EAAM,cAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EAqBpB,IAAA,CACL,GAAA,UACA,IAAA,EAAM,eAAA,EACN,IAAA,EAAM,cAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;AAAA;;KAyDtB,eAAA,GAAkB,IAAA,CAAK,MAAA,CAAO,wBAAA;EA5H5B,mCA8HN,QAAA,EAAU,YAAA,EA5HF;EA+HR,OAAA,EAAS,WAAA,EA9HN;EAiIH,MAAA,EAAQ,UAAA;AAAA;;UAIQ,YAAA;EArHH;EAuHb,IAAA,EAAM,MAAA,CAAO,KAAA;EAvHa;;;;;;EA+H1B,aAAA,GAAgB,MAAA,CAAO,KAAA;EAzGtB;;;;;EAgHD,eAAA,GAAkB,MAAA,CAAO,KAAA;AAAA;;;;;;;;;;AAtFW;AAuBpC;cA6EY,QAAA,SAAiB,MAAA,CAAO,iBAAA;;WAEpB,QAAA,EAAU,MAAA,CAAO,MAAA;cAErB,IAAA,UAAc,IAAA,EAAM,YAAA,EAAc,IAAA,EAAM,eAAA;AAAA"}
@@ -22,6 +22,13 @@ interface NeonRoleInputs {
22
22
  * freshly created (a new role already gets its own password).
23
23
  */
24
24
  resetPassword: boolean;
25
+ /**
26
+ * Rotation handle: bumping this number resets the role's password IN PLACE on
27
+ * the next `up` (an update, never a replace — replacing would try to delete
28
+ * the role, which Neon refuses for default roles and which would drop real
29
+ * grants for others). Leave unset until the first rotation is needed.
30
+ */
31
+ passwordVersion?: number;
25
32
  }
26
33
  /** Persisted state for the Neon role. */
27
34
  interface NeonRoleOutputs extends NeonRoleInputs {
@@ -40,6 +47,12 @@ declare class NeonRoleResourceProvider implements pulumi.dynamic.ResourceProvide
40
47
  create(inputs: NeonRoleInputs): Promise<pulumi.dynamic.CreateResult>;
41
48
  read(id: string, props: NeonRoleOutputs): Promise<pulumi.dynamic.ReadResult>;
42
49
  delete(_id: string, props: NeonRoleOutputs): Promise<void>;
50
+ /**
51
+ * Rotates the password in place when `passwordVersion` changes. This is the
52
+ * only updatable input (everything else replaces), so an update firing means
53
+ * a rotation was requested.
54
+ */
55
+ update(_id: string, olds: NeonRoleOutputs, news: NeonRoleInputs): Promise<pulumi.dynamic.UpdateResult>;
43
56
  diff(_id: string, olds: NeonRoleOutputs, news: NeonRoleInputs): Promise<pulumi.dynamic.DiffResult>;
44
57
  }
45
58
  /** Options type for NeonRole — replaces Pulumi's native `provider` field. */
@@ -59,6 +72,12 @@ interface NeonRoleArgs {
59
72
  * `false` (adopt and reveal the existing password). The reset runs once, at creation.
60
73
  */
61
74
  resetPassword?: pulumi.Input<boolean>;
75
+ /**
76
+ * Rotation handle: bump to rotate the role's password in place on the next
77
+ * `up`. Everything that consumes `password` (connection strings, service env
78
+ * vars, dependent redeploys) cascades automatically.
79
+ */
80
+ passwordVersion?: pulumi.Input<number>;
62
81
  }
63
82
  /**
64
83
  * Manages a Neon database role with adopt-or-create semantics.
@@ -1 +1 @@
1
- {"version":3,"file":"role.d.mts","names":[],"sources":["../../src/neon/role.ts"],"mappings":";;;;;;;;UAOiB,cAAA;;EAEhB,MAAA;EAF8B;EAK9B,SAAA;EAL8B;EAQ9B,QAAA;EAHA;EAMA,IAAA;EAAA;;;AAQa;AACb;;EADA,aAAA;AAAA;AAMQ;AAAA,UAFC,eAAA,SAAwB,cAAc;EA0F/C;EAxFA,QAAQ;AAAA;;;;;;;;;cAuFI,wBAAA,YACD,MAAA,CAAO,OAAA,CAAQ,gBAAA;EAEpB,MAAA,CAAO,MAAA,EAAQ,cAAA,GAAiB,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EA4CvD,IAAA,CACL,EAAA,UACA,KAAA,EAAO,eAAA,GACL,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;EAgBpB,MAAA,CAAO,GAAA,UAAa,KAAA,EAAO,eAAA,GAAkB,OAAA;EAc7C,IAAA,CACL,GAAA,UACA,IAAA,EAAM,eAAA,EACN,IAAA,EAAM,cAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;AAAA;;KAsDtB,eAAA,GAAkB,IAAA,CAAK,MAAA,CAAO,wBAAA;EAzIR,mCA2I1B,QAAA,EAAU,YAAA,EAzIW;EA4IrB,OAAA,EAAS,WAAA,EA5I6B;EA+ItC,MAAA,EAAQ,UAAA;AAAA;;UAIQ,YAAA;EAtGf;EAwGD,IAAA,EAAM,MAAA,CAAO,KAAA;EAvGZ;;;;;;EA+GD,aAAA,GAAgB,MAAA,CAAO,KAAK;AAAA;;;;;;;;;;;;cAchB,QAAA,SAAiB,MAAA,CAAO,iBAAA;EA1FA;EAAA,SA4FpB,QAAA,EAAU,MAAA,CAAO,MAAA;cAErB,IAAA,UAAc,IAAA,EAAM,YAAA,EAAc,IAAA,EAAM,eAAA;AAAA"}
1
+ {"version":3,"file":"role.d.mts","names":[],"sources":["../../src/neon/role.ts"],"mappings":";;;;;;;;UAOiB,cAAA;;EAEhB,MAAA;EAF8B;EAK9B,SAAA;EAL8B;EAQ9B,QAAA;EAHA;EAMA,IAAA;EAAA;;;;AAgBe;AACf;EATA,aAAA;;;AAcQ;AAuFT;;;EA7FC,eAAA;AAAA;;UAIS,eAAA,SAAwB,cAAc;EA2IpC;EAzIX,QAAQ;AAAA;;;;;;;;;cAuFI,wBAAA,YACD,MAAA,CAAO,OAAA,CAAQ,gBAAA;EAEpB,MAAA,CAAO,MAAA,EAAQ,cAAA,GAAiB,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EA4CvD,IAAA,CACL,EAAA,UACA,KAAA,EAAO,eAAA,GACL,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;EAgBpB,MAAA,CAAO,GAAA,UAAa,KAAA,EAAO,eAAA,GAAkB,OAAA;EAjExC;;;;;EAoFL,MAAA,CACL,GAAA,UACA,IAAA,EAAM,eAAA,EACN,IAAA,EAAM,cAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EAqBpB,IAAA,CACL,GAAA,UACA,IAAA,EAAM,eAAA,EACN,IAAA,EAAM,cAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;AAAA;;KAyDtB,eAAA,GAAkB,IAAA,CAAK,MAAA,CAAO,wBAAA;EA5H5B,mCA8HN,QAAA,EAAU,YAAA,EA5HF;EA+HR,OAAA,EAAS,WAAA,EA9HN;EAiIH,MAAA,EAAQ,UAAA;AAAA;;UAIQ,YAAA;EArHH;EAuHb,IAAA,EAAM,MAAA,CAAO,KAAA;EAvHa;;;;;;EA+H1B,aAAA,GAAgB,MAAA,CAAO,KAAA;EAzGtB;;;;;EAgHD,eAAA,GAAkB,MAAA,CAAO,KAAA;AAAA;;;;;;;;;;AAtFW;AAuBpC;cA6EY,QAAA,SAAiB,MAAA,CAAO,iBAAA;;WAEpB,QAAA,EAAU,MAAA,CAAO,MAAA;cAErB,IAAA,UAAc,IAAA,EAAM,YAAA,EAAc,IAAA,EAAM,eAAA;AAAA"}
@@ -67,13 +67,30 @@ var NeonRoleResourceProvider = class {
67
67
  pulumi.log.warn(`Failed to delete Neon role "${props.name}" (may already be deleted)`);
68
68
  }
69
69
  }
70
+ /**
71
+ * Rotates the password in place when `passwordVersion` changes. This is the
72
+ * only updatable input (everything else replaces), so an update firing means
73
+ * a rotation was requested.
74
+ */
75
+ async update(_id, olds, news) {
76
+ let password = olds.password;
77
+ if (olds.passwordVersion !== news.passwordVersion) {
78
+ pulumi.log.info(`Rotating password for Neon role "${news.name}" (passwordVersion ${olds.passwordVersion ?? "unset"} → ${news.passwordVersion ?? "unset"})`);
79
+ password = await resetRolePassword(new NeonClient(news.apiKey), news.projectId, news.branchId, news.name);
80
+ }
81
+ return { outs: {
82
+ ...news,
83
+ password
84
+ } };
85
+ }
70
86
  async diff(_id, olds, news) {
71
87
  const replaces = [];
72
88
  if (olds.projectId !== news.projectId) replaces.push("projectId");
73
89
  if (olds.branchId !== news.branchId) replaces.push("branchId");
74
90
  if (olds.name !== news.name) replaces.push("name");
91
+ const rotates = olds.passwordVersion !== news.passwordVersion;
75
92
  return {
76
- changes: replaces.length > 0,
93
+ changes: replaces.length > 0 || rotates,
77
94
  replaces,
78
95
  deleteBeforeReplace: true
79
96
  };
@@ -111,7 +128,8 @@ var NeonRole = class extends pulumi.ComponentResource {
111
128
  projectId: project.id,
112
129
  branchId: branch.id,
113
130
  name: args.name,
114
- resetPassword: args.resetPassword ?? false
131
+ resetPassword: args.resetPassword ?? false,
132
+ passwordVersion: args.passwordVersion
115
133
  }, { parent: this });
116
134
  this.password = resource.password;
117
135
  this.registerOutputs({ password: this.password });
@@ -1 +1 @@
1
- {"version":3,"file":"role.mjs","names":[],"sources":["../../src/neon/role.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport type { NeonBranch } from \"./branch\";\nimport { NeonClient } from \"./client\";\nimport type { NeonProject } from \"./project\";\nimport type { NeonProvider } from \"./provider\";\n\n/** Resolved inputs for the Neon role dynamic provider. */\nexport interface NeonRoleInputs {\n\t/** Neon API key. */\n\tapiKey: string;\n\n\t/** Neon project ID. */\n\tprojectId: string;\n\n\t/** Branch ID the role belongs to. */\n\tbranchId: string;\n\n\t/** Role name (e.g. `\"neondb_owner\"`). */\n\tname: string;\n\n\t/**\n\t * When the role already exists (adopted), reset its password to a fresh value\n\t * instead of revealing the inherited one. Use on copy-on-write branches to\n\t * isolate the credential from the parent branch. Ignored when the role is\n\t * freshly created (a new role already gets its own password).\n\t */\n\tresetPassword: boolean;\n}\n\n/** Persisted state for the Neon role. */\ninterface NeonRoleOutputs extends NeonRoleInputs {\n\t/** Role password (auto-generated by Neon). */\n\tpassword: string;\n}\n\n/** Neon API response for the reveal_password endpoint. */\ninterface PasswordResponse {\n\tpassword: string;\n}\n\n/** Neon API response for the reset_password endpoint (nested under `role`). */\ninterface ResetPasswordResponse {\n\trole?: { password?: string };\n\tpassword?: string;\n}\n\n/** Neon API response for listing roles. */\ninterface RoleListResponse {\n\troles: Array<{\n\t\tname: string;\n\t}>;\n}\n\n/**\n * Checks if a role exists by name on a branch.\n */\nasync function roleExists(\n\tclient: NeonClient,\n\tprojectId: string,\n\tbranchId: string,\n\tname: string,\n): Promise<boolean> {\n\tconst result = await client.get<RoleListResponse>(\n\t\t`/projects/${projectId}/branches/${branchId}/roles`,\n\t);\n\n\treturn result.roles.some((r) => r.name === name);\n}\n\n/**\n * Reveals the password for an existing role via the dedicated API endpoint.\n */\nasync function revealPassword(\n\tclient: NeonClient,\n\tprojectId: string,\n\tbranchId: string,\n\tname: string,\n): Promise<string> {\n\tconst result = await client.get<PasswordResponse>(\n\t\t`/projects/${projectId}/branches/${branchId}/roles/${name}/reveal_password`,\n\t);\n\n\treturn result.password;\n}\n\n/**\n * Resets an existing role's password to a fresh value and returns it.\n * Used to isolate an adopted (copy-on-write) role from the parent branch's credential.\n */\nasync function resetRolePassword(\n\tclient: NeonClient,\n\tprojectId: string,\n\tbranchId: string,\n\tname: string,\n): Promise<string> {\n\tconst result = await client.post<ResetPasswordResponse>(\n\t\t`/projects/${projectId}/branches/${branchId}/roles/${name}/reset_password`,\n\t\t{},\n\t);\n\n\tconst password = result.role?.password ?? result.password;\n\n\tif (!password) {\n\t\tthrow new Error(\n\t\t\t`Neon reset_password returned no password for role \"${name}\"`,\n\t\t);\n\t}\n\n\treturn password;\n}\n\n/**\n * Dynamic provider implementing CRUD for Neon database roles.\n *\n * Uses adopt-or-create on `create()`: finds an existing role by name\n * before creating a new one.\n *\n * @internal Exported only for unit testing; not part of the public API surface.\n */\nexport class NeonRoleResourceProvider\n\timplements pulumi.dynamic.ResourceProvider\n{\n\tasync create(inputs: NeonRoleInputs): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new NeonClient(inputs.apiKey);\n\n\t\tconst exists = await roleExists(\n\t\t\tclient,\n\t\t\tinputs.projectId,\n\t\t\tinputs.branchId,\n\t\t\tinputs.name,\n\t\t);\n\n\t\tif (!exists) {\n\t\t\tawait client.post(\n\t\t\t\t`/projects/${inputs.projectId}/branches/${inputs.branchId}/roles`,\n\t\t\t\t{ role: { name: inputs.name } },\n\t\t\t);\n\t\t} else if (inputs.resetPassword) {\n\t\t\tpulumi.log.info(\n\t\t\t\t`Resetting password for adopted Neon role \"${inputs.name}\" to isolate it from the parent branch`,\n\t\t\t);\n\t\t} else {\n\t\t\tpulumi.log.info(`Adopting existing Neon role \"${inputs.name}\"`);\n\t\t}\n\n\t\tconst password =\n\t\t\texists && inputs.resetPassword\n\t\t\t\t? await resetRolePassword(\n\t\t\t\t\t\tclient,\n\t\t\t\t\t\tinputs.projectId,\n\t\t\t\t\t\tinputs.branchId,\n\t\t\t\t\t\tinputs.name,\n\t\t\t\t\t)\n\t\t\t\t: await revealPassword(\n\t\t\t\t\t\tclient,\n\t\t\t\t\t\tinputs.projectId,\n\t\t\t\t\t\tinputs.branchId,\n\t\t\t\t\t\tinputs.name,\n\t\t\t\t\t);\n\n\t\treturn {\n\t\t\tid: `${inputs.branchId}/${inputs.name}`,\n\t\t\touts: { ...inputs, password },\n\t\t};\n\t}\n\n\tasync read(\n\t\tid: string,\n\t\tprops: NeonRoleOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new NeonClient(props.apiKey);\n\n\t\tconst password = await revealPassword(\n\t\t\tclient,\n\t\t\tprops.projectId,\n\t\t\tprops.branchId,\n\t\t\tprops.name,\n\t\t);\n\n\t\treturn {\n\t\t\tid,\n\t\t\tprops: { ...props, password },\n\t\t};\n\t}\n\n\tasync delete(_id: string, props: NeonRoleOutputs): Promise<void> {\n\t\tconst client = new NeonClient(props.apiKey);\n\n\t\ttry {\n\t\t\tawait client.delete(\n\t\t\t\t`/projects/${props.projectId}/branches/${props.branchId}/roles/${props.name}`,\n\t\t\t);\n\t\t} catch {\n\t\t\tpulumi.log.warn(\n\t\t\t\t`Failed to delete Neon role \"${props.name}\" (may already be deleted)`,\n\t\t\t);\n\t\t}\n\t}\n\n\tasync diff(\n\t\t_id: string,\n\t\tolds: NeonRoleOutputs,\n\t\tnews: NeonRoleInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\treplaces.push(\"projectId\");\n\t\t}\n\n\t\tif (olds.branchId !== news.branchId) {\n\t\t\treplaces.push(\"branchId\");\n\t\t}\n\n\t\tif (olds.name !== news.name) {\n\t\t\treplaces.push(\"name\");\n\t\t}\n\n\t\treturn {\n\t\t\tchanges: replaces.length > 0,\n\t\t\treplaces,\n\t\t\tdeleteBeforeReplace: true,\n\t\t};\n\t}\n}\n\n/** Internal dynamic resource — not part of the public API. */\nclass NeonRoleResource extends pulumi.dynamic.Resource {\n\tpublic declare readonly password: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\tapiKey: pulumi.Input<string>;\n\t\t\tprojectId: pulumi.Input<string>;\n\t\t\tbranchId: pulumi.Input<string>;\n\t\t\tname: pulumi.Input<string>;\n\t\t\tresetPassword: pulumi.Input<boolean>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\t// `password` is an output-only secret. Declaring it via additionalSecretOutputs\n\t\t// (rather than a pulumi.secret(undefined) input placeholder) is the only reliable\n\t\t// way to mark a dynamic-provider output secret: the placeholder gives the property\n\t\t// both a secret-undefined input and a real output, and a downstream dynamic resource\n\t\t// consuming it can race onto the undefined, producing \"Unexpected struct type\"\n\t\t// during protobuf serialization (Pulumi #16041, #3012).\n\t\tsuper(\n\t\t\tnew NeonRoleResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, password: undefined },\n\t\t\t{ ...opts, additionalSecretOutputs: [\"password\"] },\n\t\t);\n\t}\n}\n\n/** Options type for NeonRole — replaces Pulumi's native `provider` field. */\ntype NeonRoleOptions = Omit<pulumi.ComponentResourceOptions, \"provider\"> & {\n\t/** Neon authentication context. */\n\tprovider: NeonProvider;\n\n\t/** Neon project context. */\n\tproject: NeonProject;\n\n\t/** Neon branch context. */\n\tbranch: NeonBranch;\n};\n\n/** Args for NeonRole. */\nexport interface NeonRoleArgs {\n\t/** Role name (e.g. `\"neondb_owner\"`). */\n\tname: pulumi.Input<string>;\n\n\t/**\n\t * When the role is adopted (already exists on the branch), reset its password to a\n\t * fresh value instead of inheriting the parent's. Set this on copy-on-write\n\t * (non-production) branches to isolate the credential from production. Defaults to\n\t * `false` (adopt and reveal the existing password). The reset runs once, at creation.\n\t */\n\tresetPassword?: pulumi.Input<boolean>;\n}\n\n/**\n * Manages a Neon database role with adopt-or-create semantics.\n * Exposes `password` as a secret output for connection string composition.\n *\n * @example\n * ```typescript\n * const role = new NeonRole(\"owner\", {\n * name: \"neondb_owner\",\n * }, { provider, project, branch });\n * ```\n */\nexport class NeonRole extends pulumi.ComponentResource {\n\t/** Role password (secret). */\n\tpublic readonly password: pulumi.Output<string>;\n\n\tconstructor(name: string, args: NeonRoleArgs, opts: NeonRoleOptions) {\n\t\tconst { provider, project, branch, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:neon:Role\", name, {}, pulumiOpts);\n\n\t\tconst resource = new NeonRoleResource(\n\t\t\t`${name}-resource`,\n\t\t\t{\n\t\t\t\tapiKey: provider.apiKey,\n\t\t\t\tprojectId: project.id,\n\t\t\t\tbranchId: branch.id,\n\t\t\t\tname: args.name,\n\t\t\t\tresetPassword: args.resetPassword ?? false,\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.password = resource.password;\n\n\t\tthis.registerOutputs({ password: this.password });\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"role.mjs","names":[],"sources":["../../src/neon/role.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport type { NeonBranch } from \"./branch\";\nimport { NeonClient } from \"./client\";\nimport type { NeonProject } from \"./project\";\nimport type { NeonProvider } from \"./provider\";\n\n/** Resolved inputs for the Neon role dynamic provider. */\nexport interface NeonRoleInputs {\n\t/** Neon API key. */\n\tapiKey: string;\n\n\t/** Neon project ID. */\n\tprojectId: string;\n\n\t/** Branch ID the role belongs to. */\n\tbranchId: string;\n\n\t/** Role name (e.g. `\"neondb_owner\"`). */\n\tname: string;\n\n\t/**\n\t * When the role already exists (adopted), reset its password to a fresh value\n\t * instead of revealing the inherited one. Use on copy-on-write branches to\n\t * isolate the credential from the parent branch. Ignored when the role is\n\t * freshly created (a new role already gets its own password).\n\t */\n\tresetPassword: boolean;\n\n\t/**\n\t * Rotation handle: bumping this number resets the role's password IN PLACE on\n\t * the next `up` (an update, never a replace — replacing would try to delete\n\t * the role, which Neon refuses for default roles and which would drop real\n\t * grants for others). Leave unset until the first rotation is needed.\n\t */\n\tpasswordVersion?: number;\n}\n\n/** Persisted state for the Neon role. */\ninterface NeonRoleOutputs extends NeonRoleInputs {\n\t/** Role password (auto-generated by Neon). */\n\tpassword: string;\n}\n\n/** Neon API response for the reveal_password endpoint. */\ninterface PasswordResponse {\n\tpassword: string;\n}\n\n/** Neon API response for the reset_password endpoint (nested under `role`). */\ninterface ResetPasswordResponse {\n\trole?: { password?: string };\n\tpassword?: string;\n}\n\n/** Neon API response for listing roles. */\ninterface RoleListResponse {\n\troles: Array<{\n\t\tname: string;\n\t}>;\n}\n\n/**\n * Checks if a role exists by name on a branch.\n */\nasync function roleExists(\n\tclient: NeonClient,\n\tprojectId: string,\n\tbranchId: string,\n\tname: string,\n): Promise<boolean> {\n\tconst result = await client.get<RoleListResponse>(\n\t\t`/projects/${projectId}/branches/${branchId}/roles`,\n\t);\n\n\treturn result.roles.some((r) => r.name === name);\n}\n\n/**\n * Reveals the password for an existing role via the dedicated API endpoint.\n */\nasync function revealPassword(\n\tclient: NeonClient,\n\tprojectId: string,\n\tbranchId: string,\n\tname: string,\n): Promise<string> {\n\tconst result = await client.get<PasswordResponse>(\n\t\t`/projects/${projectId}/branches/${branchId}/roles/${name}/reveal_password`,\n\t);\n\n\treturn result.password;\n}\n\n/**\n * Resets an existing role's password to a fresh value and returns it.\n * Used to isolate an adopted (copy-on-write) role from the parent branch's credential.\n */\nasync function resetRolePassword(\n\tclient: NeonClient,\n\tprojectId: string,\n\tbranchId: string,\n\tname: string,\n): Promise<string> {\n\tconst result = await client.post<ResetPasswordResponse>(\n\t\t`/projects/${projectId}/branches/${branchId}/roles/${name}/reset_password`,\n\t\t{},\n\t);\n\n\tconst password = result.role?.password ?? result.password;\n\n\tif (!password) {\n\t\tthrow new Error(\n\t\t\t`Neon reset_password returned no password for role \"${name}\"`,\n\t\t);\n\t}\n\n\treturn password;\n}\n\n/**\n * Dynamic provider implementing CRUD for Neon database roles.\n *\n * Uses adopt-or-create on `create()`: finds an existing role by name\n * before creating a new one.\n *\n * @internal Exported only for unit testing; not part of the public API surface.\n */\nexport class NeonRoleResourceProvider\n\timplements pulumi.dynamic.ResourceProvider\n{\n\tasync create(inputs: NeonRoleInputs): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new NeonClient(inputs.apiKey);\n\n\t\tconst exists = await roleExists(\n\t\t\tclient,\n\t\t\tinputs.projectId,\n\t\t\tinputs.branchId,\n\t\t\tinputs.name,\n\t\t);\n\n\t\tif (!exists) {\n\t\t\tawait client.post(\n\t\t\t\t`/projects/${inputs.projectId}/branches/${inputs.branchId}/roles`,\n\t\t\t\t{ role: { name: inputs.name } },\n\t\t\t);\n\t\t} else if (inputs.resetPassword) {\n\t\t\tpulumi.log.info(\n\t\t\t\t`Resetting password for adopted Neon role \"${inputs.name}\" to isolate it from the parent branch`,\n\t\t\t);\n\t\t} else {\n\t\t\tpulumi.log.info(`Adopting existing Neon role \"${inputs.name}\"`);\n\t\t}\n\n\t\tconst password =\n\t\t\texists && inputs.resetPassword\n\t\t\t\t? await resetRolePassword(\n\t\t\t\t\t\tclient,\n\t\t\t\t\t\tinputs.projectId,\n\t\t\t\t\t\tinputs.branchId,\n\t\t\t\t\t\tinputs.name,\n\t\t\t\t\t)\n\t\t\t\t: await revealPassword(\n\t\t\t\t\t\tclient,\n\t\t\t\t\t\tinputs.projectId,\n\t\t\t\t\t\tinputs.branchId,\n\t\t\t\t\t\tinputs.name,\n\t\t\t\t\t);\n\n\t\treturn {\n\t\t\tid: `${inputs.branchId}/${inputs.name}`,\n\t\t\touts: { ...inputs, password },\n\t\t};\n\t}\n\n\tasync read(\n\t\tid: string,\n\t\tprops: NeonRoleOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new NeonClient(props.apiKey);\n\n\t\tconst password = await revealPassword(\n\t\t\tclient,\n\t\t\tprops.projectId,\n\t\t\tprops.branchId,\n\t\t\tprops.name,\n\t\t);\n\n\t\treturn {\n\t\t\tid,\n\t\t\tprops: { ...props, password },\n\t\t};\n\t}\n\n\tasync delete(_id: string, props: NeonRoleOutputs): Promise<void> {\n\t\tconst client = new NeonClient(props.apiKey);\n\n\t\ttry {\n\t\t\tawait client.delete(\n\t\t\t\t`/projects/${props.projectId}/branches/${props.branchId}/roles/${props.name}`,\n\t\t\t);\n\t\t} catch {\n\t\t\tpulumi.log.warn(\n\t\t\t\t`Failed to delete Neon role \"${props.name}\" (may already be deleted)`,\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Rotates the password in place when `passwordVersion` changes. This is the\n\t * only updatable input (everything else replaces), so an update firing means\n\t * a rotation was requested.\n\t */\n\tasync update(\n\t\t_id: string,\n\t\tolds: NeonRoleOutputs,\n\t\tnews: NeonRoleInputs,\n\t): Promise<pulumi.dynamic.UpdateResult> {\n\t\tlet password = olds.password;\n\n\t\tif (olds.passwordVersion !== news.passwordVersion) {\n\t\t\tpulumi.log.info(\n\t\t\t\t`Rotating password for Neon role \"${news.name}\" (passwordVersion ${olds.passwordVersion ?? \"unset\"} → ${news.passwordVersion ?? \"unset\"})`,\n\t\t\t);\n\n\t\t\tconst client = new NeonClient(news.apiKey);\n\n\t\t\tpassword = await resetRolePassword(\n\t\t\t\tclient,\n\t\t\t\tnews.projectId,\n\t\t\t\tnews.branchId,\n\t\t\t\tnews.name,\n\t\t\t);\n\t\t}\n\n\t\treturn { outs: { ...news, password } };\n\t}\n\n\tasync diff(\n\t\t_id: string,\n\t\tolds: NeonRoleOutputs,\n\t\tnews: NeonRoleInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\treplaces.push(\"projectId\");\n\t\t}\n\n\t\tif (olds.branchId !== news.branchId) {\n\t\t\treplaces.push(\"branchId\");\n\t\t}\n\n\t\tif (olds.name !== news.name) {\n\t\t\treplaces.push(\"name\");\n\t\t}\n\n\t\tconst rotates = olds.passwordVersion !== news.passwordVersion;\n\n\t\treturn {\n\t\t\tchanges: replaces.length > 0 || rotates,\n\t\t\treplaces,\n\t\t\tdeleteBeforeReplace: true,\n\t\t};\n\t}\n}\n\n/** Internal dynamic resource — not part of the public API. */\nclass NeonRoleResource extends pulumi.dynamic.Resource {\n\tpublic declare readonly password: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\tapiKey: pulumi.Input<string>;\n\t\t\tprojectId: pulumi.Input<string>;\n\t\t\tbranchId: pulumi.Input<string>;\n\t\t\tname: pulumi.Input<string>;\n\t\t\tresetPassword: pulumi.Input<boolean>;\n\t\t\tpasswordVersion?: pulumi.Input<number>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\t// `password` is an output-only secret. Declaring it via additionalSecretOutputs\n\t\t// (rather than a pulumi.secret(undefined) input placeholder) is the only reliable\n\t\t// way to mark a dynamic-provider output secret: the placeholder gives the property\n\t\t// both a secret-undefined input and a real output, and a downstream dynamic resource\n\t\t// consuming it can race onto the undefined, producing \"Unexpected struct type\"\n\t\t// during protobuf serialization (Pulumi #16041, #3012).\n\t\tsuper(\n\t\t\tnew NeonRoleResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, password: undefined },\n\t\t\t{ ...opts, additionalSecretOutputs: [\"password\"] },\n\t\t);\n\t}\n}\n\n/** Options type for NeonRole — replaces Pulumi's native `provider` field. */\ntype NeonRoleOptions = Omit<pulumi.ComponentResourceOptions, \"provider\"> & {\n\t/** Neon authentication context. */\n\tprovider: NeonProvider;\n\n\t/** Neon project context. */\n\tproject: NeonProject;\n\n\t/** Neon branch context. */\n\tbranch: NeonBranch;\n};\n\n/** Args for NeonRole. */\nexport interface NeonRoleArgs {\n\t/** Role name (e.g. `\"neondb_owner\"`). */\n\tname: pulumi.Input<string>;\n\n\t/**\n\t * When the role is adopted (already exists on the branch), reset its password to a\n\t * fresh value instead of inheriting the parent's. Set this on copy-on-write\n\t * (non-production) branches to isolate the credential from production. Defaults to\n\t * `false` (adopt and reveal the existing password). The reset runs once, at creation.\n\t */\n\tresetPassword?: pulumi.Input<boolean>;\n\n\t/**\n\t * Rotation handle: bump to rotate the role's password in place on the next\n\t * `up`. Everything that consumes `password` (connection strings, service env\n\t * vars, dependent redeploys) cascades automatically.\n\t */\n\tpasswordVersion?: pulumi.Input<number>;\n}\n\n/**\n * Manages a Neon database role with adopt-or-create semantics.\n * Exposes `password` as a secret output for connection string composition.\n *\n * @example\n * ```typescript\n * const role = new NeonRole(\"owner\", {\n * name: \"neondb_owner\",\n * }, { provider, project, branch });\n * ```\n */\nexport class NeonRole extends pulumi.ComponentResource {\n\t/** Role password (secret). */\n\tpublic readonly password: pulumi.Output<string>;\n\n\tconstructor(name: string, args: NeonRoleArgs, opts: NeonRoleOptions) {\n\t\tconst { provider, project, branch, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:neon:Role\", name, {}, pulumiOpts);\n\n\t\tconst resource = new NeonRoleResource(\n\t\t\t`${name}-resource`,\n\t\t\t{\n\t\t\t\tapiKey: provider.apiKey,\n\t\t\t\tprojectId: project.id,\n\t\t\t\tbranchId: branch.id,\n\t\t\t\tname: args.name,\n\t\t\t\tresetPassword: args.resetPassword ?? false,\n\t\t\t\tpasswordVersion: args.passwordVersion,\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.password = resource.password;\n\n\t\tthis.registerOutputs({ password: this.password });\n\t}\n}\n"],"mappings":"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"}
@@ -87,12 +87,35 @@ const SERVICE_CONNECT = `
87
87
  }
88
88
  }
89
89
  `;
90
+ const SERVICE_INSTANCE_DEPLOY = `
91
+ mutation($serviceId: String!, $environmentId: String!) {
92
+ serviceInstanceDeployV2(serviceId: $serviceId, environmentId: $environmentId)
93
+ }
94
+ `;
95
+ /**
96
+ * Triggers a deployment of the service instance in the target environment.
97
+ * Image-sourced services never deploy there on their own: `serviceCreate`'s
98
+ * auto-deploy only reaches the project's DEFAULT environment, and
99
+ * `serviceInstanceUpdate` applies config without redeploying — so a service
100
+ * in any other environment stays undeployed forever and its private DNS name
101
+ * never registers. `environmentTriggersDeploy` is no alternative: it returns
102
+ * success without creating anything for a service that has never deployed in
103
+ * that environment (proven live, 2026-07-06).
104
+ */
105
+ async function deployServiceInstance(client, serviceId, environmentId) {
106
+ const result = await client.query(SERVICE_INSTANCE_DEPLOY, {
107
+ serviceId,
108
+ environmentId
109
+ });
110
+ _pulumi_pulumi.log.info(`[infracraft] serviceInstanceDeployV2 created deployment ${result.serviceInstanceDeployV2}`);
111
+ }
90
112
  /**
91
113
  * Applies service instance configuration (builder, commands, healthcheck).
92
114
  * Retries without healthcheck fields if the first call fails.
93
115
  */
94
116
  async function applyInstanceConfig(client, serviceId, environmentId, inputs) {
95
117
  const instanceInput = {};
118
+ if (inputs.source) instanceInput.source = { image: inputs.source.image };
96
119
  if (inputs.builder) instanceInput.builder = inputs.builder;
97
120
  if (inputs.buildCommand) instanceInput.buildCommand = inputs.buildCommand;
98
121
  if (inputs.startCommand) instanceInput.startCommand = inputs.startCommand;
@@ -154,6 +177,7 @@ var RailwayServiceResourceProvider = class {
154
177
  });
155
178
  }
156
179
  await applyInstanceConfig(client, serviceId, inputs.environmentId, inputs);
180
+ if (inputs.source) await deployServiceInstance(client, serviceId, inputs.environmentId);
157
181
  const outs = {
158
182
  ...inputs,
159
183
  serviceId
@@ -176,6 +200,7 @@ var RailwayServiceResourceProvider = class {
176
200
  input: updateInput
177
201
  });
178
202
  await applyInstanceConfig(client, id, news.environmentId, news);
203
+ if (news.source) await deployServiceInstance(client, id, news.environmentId);
179
204
  return { outs: {
180
205
  ...news,
181
206
  serviceId: id
@@ -1 +1 @@
1
- {"version":3,"file":"service.cjs","names":["RailwayClient","pulumi"],"sources":["../../src/railway/service.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport { RailwayClient } from \"./client\";\nimport type { RailwayEnvironment } from \"./environment\";\nimport type { RailwayProject } from \"./project\";\nimport type { RailwayProvider } from \"./provider\";\n\n/**\n * Railway build system. Enum keys are UPPERCASE per convention; values are\n * Railway's required UPPERCASE wire literals.\n * Note: HEROKU and PAKETO were deprecated Feb 21 2025 and auto-migrated to\n * NIXPACKS by Railway, but remain in the schema and are accepted by the API.\n */\nexport enum RailwayBuilder {\n\tRAILPACK = \"RAILPACK\",\n\tNIXPACKS = \"NIXPACKS\",\n\tDOCKERFILE = \"DOCKERFILE\",\n\tHEROKU = \"HEROKU\",\n\tPAKETO = \"PAKETO\",\n}\n\n/**\n * Railway service restart policy. Controls when Railway restarts the service\n * container after it exits. Default is ON_FAILURE.\n */\nexport enum RailwayRestartPolicy {\n\tON_FAILURE = \"ON_FAILURE\",\n\tALWAYS = \"ALWAYS\",\n\tNEVER = \"NEVER\",\n}\n\n/** Docker image source for a Railway service (e.g. `redis:8-alpine`). */\ninterface RailwayServiceSource {\n\t/** Full Docker image reference including tag. */\n\timage: string;\n}\n\n/** Resolved inputs for the Railway service dynamic provider. */\ninterface RailwayServiceInputs {\n\t/** Railway API bearer token. */\n\ttoken: string;\n\n\t/** Railway project UUID. */\n\tprojectId: string;\n\n\t/** Railway environment UUID (e.g. production). */\n\tenvironmentId: string;\n\n\t/** Human-readable service name used for adopt-or-create matching. */\n\tname: string;\n\n\t/** SVG icon URL displayed in the Railway dashboard. */\n\ticon?: string;\n\n\t/** Docker image source for image-based services. */\n\tsource?: RailwayServiceSource;\n\n\t/** Build system to use when building the service. */\n\tbuilder?: RailwayBuilder;\n\n\t/** Shell command executed during the build phase. */\n\tbuildCommand?: string;\n\n\t/** Shell command executed to start the service at runtime. */\n\tstartCommand?: string;\n\n\t/** Restart behavior for the service container. */\n\trestartPolicyType?: RailwayRestartPolicy;\n\n\t/** HTTP path polled for health checks (e.g. `\"/health-check\"`). */\n\thealthcheckPath?: string;\n\n\t/** Seconds to wait for a healthy response before marking unhealthy. */\n\thealthcheckTimeout?: number;\n\n\t/** Shell command executed before the main deploy (e.g. migrations). */\n\tpreDeployCommand?: string;\n}\n\n/** Persisted state for the Railway service, extending inputs with the Railway-assigned ID. */\ninterface RailwayServiceOutputs extends RailwayServiceInputs {\n\t/** Railway-assigned service UUID. */\n\tserviceId: string;\n}\n\nconst SERVICES_QUERY = `\n query($projectId: String!) {\n project(id: $projectId) {\n services {\n edges {\n node {\n id\n name\n }\n }\n }\n }\n }\n`;\n\nconst SERVICE_QUERY = `\n query($serviceId: String!) {\n service(id: $serviceId) {\n id\n name\n }\n }\n`;\n\nconst SERVICE_CREATE = `\n mutation($input: ServiceCreateInput!) {\n serviceCreate(input: $input) {\n id\n name\n }\n }\n`;\n\nconst SERVICE_UPDATE = `\n mutation($id: String!, $input: ServiceUpdateInput!) {\n serviceUpdate(id: $id, input: $input) {\n id\n name\n }\n }\n`;\n\nconst SERVICE_INSTANCE_UPDATE = `\n mutation(\n $serviceId: String!\n $environmentId: String!\n $input: ServiceInstanceUpdateInput!\n ) {\n serviceInstanceUpdate(\n serviceId: $serviceId\n environmentId: $environmentId\n input: $input\n )\n }\n`;\n\nconst SERVICE_CONNECT = `\n mutation($id: String!, $input: ServiceConnectInput!) {\n serviceConnect(id: $id, input: $input) {\n id\n }\n }\n`;\n\n/**\n * Applies service instance configuration (builder, commands, healthcheck).\n * Retries without healthcheck fields if the first call fails.\n */\nasync function applyInstanceConfig(\n\tclient: RailwayClient,\n\tserviceId: string,\n\tenvironmentId: string,\n\tinputs: RailwayServiceInputs,\n): Promise<void> {\n\tconst instanceInput: Record<string, unknown> = {};\n\n\tif (inputs.builder) {\n\t\tinstanceInput.builder = inputs.builder;\n\t}\n\n\tif (inputs.buildCommand) {\n\t\tinstanceInput.buildCommand = inputs.buildCommand;\n\t}\n\n\tif (inputs.startCommand) {\n\t\tinstanceInput.startCommand = inputs.startCommand;\n\t}\n\n\tif (inputs.restartPolicyType) {\n\t\tinstanceInput.restartPolicyType = inputs.restartPolicyType;\n\t}\n\n\tif (inputs.healthcheckPath) {\n\t\tinstanceInput.healthcheckPath = inputs.healthcheckPath;\n\t}\n\n\tif (inputs.healthcheckTimeout) {\n\t\tinstanceInput.healthcheckTimeout = inputs.healthcheckTimeout;\n\t}\n\n\tif (inputs.preDeployCommand) {\n\t\tinstanceInput.preDeployCommand = inputs.preDeployCommand;\n\t}\n\n\tif (Object.keys(instanceInput).length === 0) {\n\t\treturn;\n\t}\n\n\ttry {\n\t\tawait client.query(SERVICE_INSTANCE_UPDATE, {\n\t\t\tserviceId,\n\t\t\tenvironmentId,\n\t\t\tinput: instanceInput,\n\t\t});\n\t} catch (error) {\n\t\tpulumi.log.warn(\n\t\t\t`serviceInstanceUpdate failed, retrying without healthcheck fields: ${error}`,\n\t\t);\n\n\t\tdelete instanceInput.healthcheckPath;\n\t\tdelete instanceInput.healthcheckTimeout;\n\n\t\tif (Object.keys(instanceInput).length > 0) {\n\t\t\tawait client.query(SERVICE_INSTANCE_UPDATE, {\n\t\t\t\tserviceId,\n\t\t\t\tenvironmentId,\n\t\t\t\tinput: instanceInput,\n\t\t\t});\n\t\t}\n\t}\n}\n\n/**\n * Dynamic provider implementing CRUD for Railway services.\n *\n * Uses adopt-or-create on `create()`: queries services by project ID and name\n * before creating a new one. Service instance configuration (builder, commands,\n * healthcheck) is applied via `serviceInstanceUpdate` after create or update.\n *\n * @internal Exported only for unit testing; not part of the public API surface.\n */\nexport class RailwayServiceResourceProvider\n\timplements pulumi.dynamic.ResourceProvider\n{\n\t/**\n\t * Creates or adopts a Railway service by name, then applies instance config.\n\t */\n\tasync create(\n\t\tinputs: RailwayServiceInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new RailwayClient(inputs.token);\n\n\t\tconst result = await client.query<{\n\t\t\tproject: {\n\t\t\t\tservices: { edges: Array<{ node: { id: string; name: string } }> };\n\t\t\t};\n\t\t}>(SERVICES_QUERY, { projectId: inputs.projectId });\n\n\t\tlet serviceId = result.project.services.edges.find(\n\t\t\t(edge) => edge.node.name === inputs.name,\n\t\t)?.node.id;\n\n\t\tif (serviceId) {\n\t\t\tpulumi.log.info(\n\t\t\t\t`Adopted existing Railway service \"${inputs.name}\" (${serviceId})`,\n\t\t\t);\n\t\t} else {\n\t\t\tconst createInput: Record<string, unknown> = {\n\t\t\t\tprojectId: inputs.projectId,\n\t\t\t\tenvironmentId: inputs.environmentId,\n\t\t\t\tname: inputs.name,\n\t\t\t};\n\n\t\t\tif (inputs.source) {\n\t\t\t\tcreateInput.source = { image: inputs.source.image };\n\t\t\t}\n\n\t\t\tconst created = await client.query<{\n\t\t\t\tserviceCreate: { id: string; name: string };\n\t\t\t}>(SERVICE_CREATE, { input: createInput });\n\n\t\t\tserviceId = created.serviceCreate.id;\n\n\t\t\tpulumi.log.info(\n\t\t\t\t`Created Railway service \"${inputs.name}\" (${serviceId})`,\n\t\t\t);\n\n\t\t\tif (inputs.source) {\n\t\t\t\tawait client.query(SERVICE_CONNECT, {\n\t\t\t\t\tid: serviceId,\n\t\t\t\t\tinput: { image: inputs.source.image },\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (inputs.icon) {\n\t\t\t\tawait client.query(SERVICE_UPDATE, {\n\t\t\t\t\tid: serviceId,\n\t\t\t\t\tinput: { icon: inputs.icon },\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tawait applyInstanceConfig(client, serviceId, inputs.environmentId, inputs);\n\n\t\tconst outs: RailwayServiceOutputs = { ...inputs, serviceId };\n\n\t\treturn { id: serviceId, outs };\n\t}\n\n\t/**\n\t * Updates service name/icon and re-applies instance configuration.\n\t */\n\tasync update(\n\t\tid: string,\n\t\tolds: RailwayServiceOutputs,\n\t\tnews: RailwayServiceInputs,\n\t): Promise<pulumi.dynamic.UpdateResult> {\n\t\tconst client = new RailwayClient(news.token);\n\n\t\tconst updateInput: Record<string, unknown> = {};\n\n\t\tif (olds.name !== news.name) {\n\t\t\tupdateInput.name = news.name;\n\t\t}\n\n\t\tif (news.icon && olds.icon !== news.icon) {\n\t\t\tupdateInput.icon = news.icon;\n\t\t}\n\n\t\tif (Object.keys(updateInput).length > 0) {\n\t\t\tawait client.query(SERVICE_UPDATE, { id, input: updateInput });\n\t\t}\n\n\t\tawait applyInstanceConfig(client, id, news.environmentId, news);\n\n\t\tconst outs: RailwayServiceOutputs = { ...news, serviceId: id };\n\n\t\treturn { outs };\n\t}\n\n\t/**\n\t * Reads current state for `pulumi refresh` by querying the service by ID.\n\t */\n\tasync read(\n\t\tid: string,\n\t\tprops: RailwayServiceOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\ttry {\n\t\t\tawait client.query<{ service: { id: string; name: string } }>(\n\t\t\t\tSERVICE_QUERY,\n\t\t\t\t{ serviceId: id },\n\t\t\t);\n\t\t} catch {\n\t\t\tthrow new Error(`Railway service \"${props.name}\" (${id}) not found`);\n\t\t}\n\n\t\treturn { id, props: { ...props, serviceId: id } };\n\t}\n\n\t/**\n\t * Deletion is a no-op. A Railway service is a project-level resource shared\n\t * across environments (forked environments adopt it by name), so a single\n\t * environment's destroy must never delete it. Deleting the *environment*\n\t * removes that environment's service instances instead.\n\t */\n\tasync delete(): Promise<void> {\n\t\tpulumi.log.warn(\n\t\t\t\"Railway service deletion skipped — services are project-level; delete the environment to remove its instances\",\n\t\t);\n\t}\n\n\t/**\n\t * Compares old and new inputs to determine what changed.\n\t *\n\t * ProjectId and environmentId changes trigger replacement.\n\t */\n\tasync diff(\n\t\t_id: string,\n\t\tolds: RailwayServiceOutputs,\n\t\tnews: RailwayServiceInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\t\tconst changes: string[] = [];\n\n\t\tif (olds.name !== news.name) {\n\t\t\tchanges.push(\"name\");\n\t\t}\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\treplaces.push(\"projectId\");\n\t\t}\n\n\t\tif (olds.environmentId !== news.environmentId) {\n\t\t\treplaces.push(\"environmentId\");\n\t\t}\n\n\t\tif (olds.builder !== news.builder) {\n\t\t\tchanges.push(\"builder\");\n\t\t}\n\n\t\tif (olds.buildCommand !== news.buildCommand) {\n\t\t\tchanges.push(\"buildCommand\");\n\t\t}\n\n\t\tif (olds.startCommand !== news.startCommand) {\n\t\t\tchanges.push(\"startCommand\");\n\t\t}\n\n\t\tif (olds.restartPolicyType !== news.restartPolicyType) {\n\t\t\tchanges.push(\"restartPolicyType\");\n\t\t}\n\n\t\tif (olds.healthcheckPath !== news.healthcheckPath) {\n\t\t\tchanges.push(\"healthcheckPath\");\n\t\t}\n\n\t\tif (olds.healthcheckTimeout !== news.healthcheckTimeout) {\n\t\t\tchanges.push(\"healthcheckTimeout\");\n\t\t}\n\n\t\tif (olds.preDeployCommand !== news.preDeployCommand) {\n\t\t\tchanges.push(\"preDeployCommand\");\n\t\t}\n\n\t\tif (olds.icon !== news.icon) {\n\t\t\tchanges.push(\"icon\");\n\t\t}\n\n\t\treturn {\n\t\t\tchanges: replaces.length > 0 || changes.length > 0,\n\t\t\treplaces,\n\t\t\tdeleteBeforeReplace: true,\n\t\t};\n\t}\n}\n\n/** Internal dynamic resource — not part of the public API. */\nclass RailwayServiceResource extends pulumi.dynamic.Resource {\n\tpublic declare readonly serviceId: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\ttoken: pulumi.Input<string>;\n\t\t\tprojectId: pulumi.Input<string>;\n\t\t\tenvironmentId: pulumi.Input<string>;\n\t\t\tname: pulumi.Input<string>;\n\t\t\ticon?: pulumi.Input<string>;\n\t\t\tsource?: pulumi.Input<{ image: pulumi.Input<string> }>;\n\t\t\tbuilder?: pulumi.Input<RailwayBuilder>;\n\t\t\tbuildCommand?: pulumi.Input<string>;\n\t\t\tstartCommand?: pulumi.Input<string>;\n\t\t\trestartPolicyType?: pulumi.Input<RailwayRestartPolicy>;\n\t\t\thealthcheckPath?: pulumi.Input<string>;\n\t\t\thealthcheckTimeout?: pulumi.Input<number>;\n\t\t\tpreDeployCommand?: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew RailwayServiceResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, serviceId: undefined },\n\t\t\topts,\n\t\t);\n\t}\n}\n\n/** Options type for RailwayService — replaces Pulumi's native `provider` field. */\ntype RailwayServiceOptions = Omit<\n\tpulumi.ComponentResourceOptions,\n\t\"provider\"\n> & {\n\t/** Railway authentication context. */\n\tprovider: RailwayProvider;\n\n\t/** Railway project context. */\n\tproject: RailwayProject;\n\n\t/** Railway environment context. */\n\tenvironment: RailwayEnvironment;\n};\n\n/** Args for RailwayService. */\nexport interface RailwayServiceArgs {\n\t/** Human-readable service name used for adopt-or-create matching. */\n\tname: pulumi.Input<string>;\n\n\t/** SVG icon URL displayed in the Railway dashboard. */\n\ticon?: pulumi.Input<string>;\n\n\t/** Docker image source for image-based services. */\n\tsource?: pulumi.Input<{ image: pulumi.Input<string> }>;\n\n\t/** Build system to use when building the service. */\n\tbuilder?: pulumi.Input<RailwayBuilder>;\n\n\t/** Shell command executed during the build phase. */\n\tbuildCommand?: pulumi.Input<string>;\n\n\t/** Shell command executed to start the service at runtime. */\n\tstartCommand?: pulumi.Input<string>;\n\n\t/** Restart behavior for the service container. */\n\trestartPolicyType?: pulumi.Input<RailwayRestartPolicy>;\n\n\t/** HTTP path polled for health checks (e.g. `\"/health-check\"`). */\n\thealthcheckPath?: pulumi.Input<string>;\n\n\t/** Seconds to wait for a healthy response before marking unhealthy. */\n\thealthcheckTimeout?: pulumi.Input<number>;\n\n\t/** Shell command executed before the main deploy (e.g. migrations). */\n\tpreDeployCommand?: pulumi.Input<string>;\n}\n\n/**\n * Manages a Railway service with adopt-or-create semantics.\n *\n * @example\n * ```typescript\n * const service = new RailwayService(\"api\", {\n * name: \"api\",\n * builder: RailwayBuilder.RAILPACK,\n * startCommand: \"node dist/index.js\",\n * healthcheckPath: \"/health\",\n * }, { provider, project, environment });\n *\n * // Use serviceId downstream\n * new RailwayVariable(\"api-vars\", {\n * variables: { DATABASE_URL: dbUrl },\n * }, { provider, project, environment, service });\n * ```\n */\nexport class RailwayService extends pulumi.ComponentResource {\n\t/** Railway service UUID. */\n\tpublic readonly id: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: RailwayServiceArgs,\n\t\topts: RailwayServiceOptions,\n\t) {\n\t\tconst { provider, project, environment, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:railway:Service\", name, {}, pulumiOpts);\n\n\t\tconst resource = new RailwayServiceResource(\n\t\t\t`${name}-resource`,\n\t\t\t{\n\t\t\t\ttoken: provider.token,\n\t\t\t\tprojectId: project.id,\n\t\t\t\tenvironmentId: environment.id,\n\t\t\t\t...args,\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.id = resource.serviceId;\n\n\t\tthis.registerOutputs({ id: this.id });\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"service.cjs","names":["RailwayClient","pulumi"],"sources":["../../src/railway/service.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport { RailwayClient } from \"./client\";\nimport type { RailwayEnvironment } from \"./environment\";\nimport type { RailwayProject } from \"./project\";\nimport type { RailwayProvider } from \"./provider\";\n\n/**\n * Railway build system. Enum keys are UPPERCASE per convention; values are\n * Railway's required UPPERCASE wire literals.\n * Note: HEROKU and PAKETO were deprecated Feb 21 2025 and auto-migrated to\n * NIXPACKS by Railway, but remain in the schema and are accepted by the API.\n */\nexport enum RailwayBuilder {\n\tRAILPACK = \"RAILPACK\",\n\tNIXPACKS = \"NIXPACKS\",\n\tDOCKERFILE = \"DOCKERFILE\",\n\tHEROKU = \"HEROKU\",\n\tPAKETO = \"PAKETO\",\n}\n\n/**\n * Railway service restart policy. Controls when Railway restarts the service\n * container after it exits. Default is ON_FAILURE.\n */\nexport enum RailwayRestartPolicy {\n\tON_FAILURE = \"ON_FAILURE\",\n\tALWAYS = \"ALWAYS\",\n\tNEVER = \"NEVER\",\n}\n\n/** Docker image source for a Railway service (e.g. `redis:8-alpine`). */\ninterface RailwayServiceSource {\n\t/** Full Docker image reference including tag. */\n\timage: string;\n}\n\n/** Resolved inputs for the Railway service dynamic provider. */\ninterface RailwayServiceInputs {\n\t/** Railway API bearer token. */\n\ttoken: string;\n\n\t/** Railway project UUID. */\n\tprojectId: string;\n\n\t/** Railway environment UUID (e.g. production). */\n\tenvironmentId: string;\n\n\t/** Human-readable service name used for adopt-or-create matching. */\n\tname: string;\n\n\t/** SVG icon URL displayed in the Railway dashboard. */\n\ticon?: string;\n\n\t/** Docker image source for image-based services. */\n\tsource?: RailwayServiceSource;\n\n\t/** Build system to use when building the service. */\n\tbuilder?: RailwayBuilder;\n\n\t/** Shell command executed during the build phase. */\n\tbuildCommand?: string;\n\n\t/** Shell command executed to start the service at runtime. */\n\tstartCommand?: string;\n\n\t/** Restart behavior for the service container. */\n\trestartPolicyType?: RailwayRestartPolicy;\n\n\t/** HTTP path polled for health checks (e.g. `\"/health-check\"`). */\n\thealthcheckPath?: string;\n\n\t/** Seconds to wait for a healthy response before marking unhealthy. */\n\thealthcheckTimeout?: number;\n\n\t/** Shell command executed before the main deploy (e.g. migrations). */\n\tpreDeployCommand?: string;\n}\n\n/** Persisted state for the Railway service, extending inputs with the Railway-assigned ID. */\ninterface RailwayServiceOutputs extends RailwayServiceInputs {\n\t/** Railway-assigned service UUID. */\n\tserviceId: string;\n}\n\nconst SERVICES_QUERY = `\n query($projectId: String!) {\n project(id: $projectId) {\n services {\n edges {\n node {\n id\n name\n }\n }\n }\n }\n }\n`;\n\nconst SERVICE_QUERY = `\n query($serviceId: String!) {\n service(id: $serviceId) {\n id\n name\n }\n }\n`;\n\nconst SERVICE_CREATE = `\n mutation($input: ServiceCreateInput!) {\n serviceCreate(input: $input) {\n id\n name\n }\n }\n`;\n\nconst SERVICE_UPDATE = `\n mutation($id: String!, $input: ServiceUpdateInput!) {\n serviceUpdate(id: $id, input: $input) {\n id\n name\n }\n }\n`;\n\nconst SERVICE_INSTANCE_UPDATE = `\n mutation(\n $serviceId: String!\n $environmentId: String!\n $input: ServiceInstanceUpdateInput!\n ) {\n serviceInstanceUpdate(\n serviceId: $serviceId\n environmentId: $environmentId\n input: $input\n )\n }\n`;\n\nconst SERVICE_CONNECT = `\n mutation($id: String!, $input: ServiceConnectInput!) {\n serviceConnect(id: $id, input: $input) {\n id\n }\n }\n`;\n\nconst SERVICE_INSTANCE_DEPLOY = `\n mutation($serviceId: String!, $environmentId: String!) {\n serviceInstanceDeployV2(serviceId: $serviceId, environmentId: $environmentId)\n }\n`;\n\n/**\n * Triggers a deployment of the service instance in the target environment.\n * Image-sourced services never deploy there on their own: `serviceCreate`'s\n * auto-deploy only reaches the project's DEFAULT environment, and\n * `serviceInstanceUpdate` applies config without redeploying — so a service\n * in any other environment stays undeployed forever and its private DNS name\n * never registers. `environmentTriggersDeploy` is no alternative: it returns\n * success without creating anything for a service that has never deployed in\n * that environment (proven live, 2026-07-06).\n */\nasync function deployServiceInstance(\n\tclient: RailwayClient,\n\tserviceId: string,\n\tenvironmentId: string,\n): Promise<void> {\n\tconst result = await client.query<{ serviceInstanceDeployV2: string }>(\n\t\tSERVICE_INSTANCE_DEPLOY,\n\t\t{ serviceId, environmentId },\n\t);\n\n\tpulumi.log.info(\n\t\t`[infracraft] serviceInstanceDeployV2 created deployment ${result.serviceInstanceDeployV2}`,\n\t);\n}\n\n/**\n * Applies service instance configuration (builder, commands, healthcheck).\n * Retries without healthcheck fields if the first call fails.\n */\nasync function applyInstanceConfig(\n\tclient: RailwayClient,\n\tserviceId: string,\n\tenvironmentId: string,\n\tinputs: RailwayServiceInputs,\n): Promise<void> {\n\tconst instanceInput: Record<string, unknown> = {};\n\n\t// Source must be applied PER ENVIRONMENT: `ServiceCreateInput.source` only\n\t// configures the instance of the environment passed at create time, and every\n\t// other environment's instance is born with source=null — deploy triggers\n\t// then no-op silently because there is nothing to deploy.\n\tif (inputs.source) {\n\t\tinstanceInput.source = { image: inputs.source.image };\n\t}\n\n\tif (inputs.builder) {\n\t\tinstanceInput.builder = inputs.builder;\n\t}\n\n\tif (inputs.buildCommand) {\n\t\tinstanceInput.buildCommand = inputs.buildCommand;\n\t}\n\n\tif (inputs.startCommand) {\n\t\tinstanceInput.startCommand = inputs.startCommand;\n\t}\n\n\tif (inputs.restartPolicyType) {\n\t\tinstanceInput.restartPolicyType = inputs.restartPolicyType;\n\t}\n\n\tif (inputs.healthcheckPath) {\n\t\tinstanceInput.healthcheckPath = inputs.healthcheckPath;\n\t}\n\n\tif (inputs.healthcheckTimeout) {\n\t\tinstanceInput.healthcheckTimeout = inputs.healthcheckTimeout;\n\t}\n\n\tif (inputs.preDeployCommand) {\n\t\tinstanceInput.preDeployCommand = inputs.preDeployCommand;\n\t}\n\n\tif (Object.keys(instanceInput).length === 0) {\n\t\treturn;\n\t}\n\n\ttry {\n\t\tawait client.query(SERVICE_INSTANCE_UPDATE, {\n\t\t\tserviceId,\n\t\t\tenvironmentId,\n\t\t\tinput: instanceInput,\n\t\t});\n\t} catch (error) {\n\t\tpulumi.log.warn(\n\t\t\t`serviceInstanceUpdate failed, retrying without healthcheck fields: ${error}`,\n\t\t);\n\n\t\tdelete instanceInput.healthcheckPath;\n\t\tdelete instanceInput.healthcheckTimeout;\n\n\t\tif (Object.keys(instanceInput).length > 0) {\n\t\t\tawait client.query(SERVICE_INSTANCE_UPDATE, {\n\t\t\t\tserviceId,\n\t\t\t\tenvironmentId,\n\t\t\t\tinput: instanceInput,\n\t\t\t});\n\t\t}\n\t}\n}\n\n/**\n * Dynamic provider implementing CRUD for Railway services.\n *\n * Uses adopt-or-create on `create()`: queries services by project ID and name\n * before creating a new one. Service instance configuration (builder, commands,\n * healthcheck) is applied via `serviceInstanceUpdate` after create or update.\n *\n * @internal Exported only for unit testing; not part of the public API surface.\n */\nexport class RailwayServiceResourceProvider\n\timplements pulumi.dynamic.ResourceProvider\n{\n\t/**\n\t * Creates or adopts a Railway service by name, then applies instance config.\n\t */\n\tasync create(\n\t\tinputs: RailwayServiceInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new RailwayClient(inputs.token);\n\n\t\tconst result = await client.query<{\n\t\t\tproject: {\n\t\t\t\tservices: { edges: Array<{ node: { id: string; name: string } }> };\n\t\t\t};\n\t\t}>(SERVICES_QUERY, { projectId: inputs.projectId });\n\n\t\tlet serviceId = result.project.services.edges.find(\n\t\t\t(edge) => edge.node.name === inputs.name,\n\t\t)?.node.id;\n\n\t\tif (serviceId) {\n\t\t\tpulumi.log.info(\n\t\t\t\t`Adopted existing Railway service \"${inputs.name}\" (${serviceId})`,\n\t\t\t);\n\t\t} else {\n\t\t\tconst createInput: Record<string, unknown> = {\n\t\t\t\tprojectId: inputs.projectId,\n\t\t\t\tenvironmentId: inputs.environmentId,\n\t\t\t\tname: inputs.name,\n\t\t\t};\n\n\t\t\tif (inputs.source) {\n\t\t\t\tcreateInput.source = { image: inputs.source.image };\n\t\t\t}\n\n\t\t\tconst created = await client.query<{\n\t\t\t\tserviceCreate: { id: string; name: string };\n\t\t\t}>(SERVICE_CREATE, { input: createInput });\n\n\t\t\tserviceId = created.serviceCreate.id;\n\n\t\t\tpulumi.log.info(\n\t\t\t\t`Created Railway service \"${inputs.name}\" (${serviceId})`,\n\t\t\t);\n\n\t\t\tif (inputs.source) {\n\t\t\t\tawait client.query(SERVICE_CONNECT, {\n\t\t\t\t\tid: serviceId,\n\t\t\t\t\tinput: { image: inputs.source.image },\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (inputs.icon) {\n\t\t\t\tawait client.query(SERVICE_UPDATE, {\n\t\t\t\t\tid: serviceId,\n\t\t\t\t\tinput: { icon: inputs.icon },\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tawait applyInstanceConfig(client, serviceId, inputs.environmentId, inputs);\n\n\t\t// Image services have no `railway up` step (see RailwayDeploy for code\n\t\t// services) — the provider owns their deploy.\n\t\tif (inputs.source) {\n\t\t\tawait deployServiceInstance(client, serviceId, inputs.environmentId);\n\t\t}\n\n\t\tconst outs: RailwayServiceOutputs = { ...inputs, serviceId };\n\n\t\treturn { id: serviceId, outs };\n\t}\n\n\t/**\n\t * Updates service name/icon and re-applies instance configuration.\n\t */\n\tasync update(\n\t\tid: string,\n\t\tolds: RailwayServiceOutputs,\n\t\tnews: RailwayServiceInputs,\n\t): Promise<pulumi.dynamic.UpdateResult> {\n\t\tconst client = new RailwayClient(news.token);\n\n\t\tconst updateInput: Record<string, unknown> = {};\n\n\t\tif (olds.name !== news.name) {\n\t\t\tupdateInput.name = news.name;\n\t\t}\n\n\t\tif (news.icon && olds.icon !== news.icon) {\n\t\t\tupdateInput.icon = news.icon;\n\t\t}\n\n\t\tif (Object.keys(updateInput).length > 0) {\n\t\t\tawait client.query(SERVICE_UPDATE, { id, input: updateInput });\n\t\t}\n\n\t\tawait applyInstanceConfig(client, id, news.environmentId, news);\n\n\t\t// Instance config changes (source, startCommand, …) only take effect on\n\t\t// the next deployment; image services get none unless the provider\n\t\t// triggers it.\n\t\tif (news.source) {\n\t\t\tawait deployServiceInstance(client, id, news.environmentId);\n\t\t}\n\n\t\tconst outs: RailwayServiceOutputs = { ...news, serviceId: id };\n\n\t\treturn { outs };\n\t}\n\n\t/**\n\t * Reads current state for `pulumi refresh` by querying the service by ID.\n\t */\n\tasync read(\n\t\tid: string,\n\t\tprops: RailwayServiceOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\ttry {\n\t\t\tawait client.query<{ service: { id: string; name: string } }>(\n\t\t\t\tSERVICE_QUERY,\n\t\t\t\t{ serviceId: id },\n\t\t\t);\n\t\t} catch {\n\t\t\tthrow new Error(`Railway service \"${props.name}\" (${id}) not found`);\n\t\t}\n\n\t\treturn { id, props: { ...props, serviceId: id } };\n\t}\n\n\t/**\n\t * Deletion is a no-op. A Railway service is a project-level resource shared\n\t * across environments (forked environments adopt it by name), so a single\n\t * environment's destroy must never delete it. Deleting the *environment*\n\t * removes that environment's service instances instead.\n\t */\n\tasync delete(): Promise<void> {\n\t\tpulumi.log.warn(\n\t\t\t\"Railway service deletion skipped — services are project-level; delete the environment to remove its instances\",\n\t\t);\n\t}\n\n\t/**\n\t * Compares old and new inputs to determine what changed.\n\t *\n\t * ProjectId and environmentId changes trigger replacement.\n\t */\n\tasync diff(\n\t\t_id: string,\n\t\tolds: RailwayServiceOutputs,\n\t\tnews: RailwayServiceInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\t\tconst changes: string[] = [];\n\n\t\tif (olds.name !== news.name) {\n\t\t\tchanges.push(\"name\");\n\t\t}\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\treplaces.push(\"projectId\");\n\t\t}\n\n\t\tif (olds.environmentId !== news.environmentId) {\n\t\t\treplaces.push(\"environmentId\");\n\t\t}\n\n\t\tif (olds.builder !== news.builder) {\n\t\t\tchanges.push(\"builder\");\n\t\t}\n\n\t\tif (olds.buildCommand !== news.buildCommand) {\n\t\t\tchanges.push(\"buildCommand\");\n\t\t}\n\n\t\tif (olds.startCommand !== news.startCommand) {\n\t\t\tchanges.push(\"startCommand\");\n\t\t}\n\n\t\tif (olds.restartPolicyType !== news.restartPolicyType) {\n\t\t\tchanges.push(\"restartPolicyType\");\n\t\t}\n\n\t\tif (olds.healthcheckPath !== news.healthcheckPath) {\n\t\t\tchanges.push(\"healthcheckPath\");\n\t\t}\n\n\t\tif (olds.healthcheckTimeout !== news.healthcheckTimeout) {\n\t\t\tchanges.push(\"healthcheckTimeout\");\n\t\t}\n\n\t\tif (olds.preDeployCommand !== news.preDeployCommand) {\n\t\t\tchanges.push(\"preDeployCommand\");\n\t\t}\n\n\t\tif (olds.icon !== news.icon) {\n\t\t\tchanges.push(\"icon\");\n\t\t}\n\n\t\treturn {\n\t\t\tchanges: replaces.length > 0 || changes.length > 0,\n\t\t\treplaces,\n\t\t\tdeleteBeforeReplace: true,\n\t\t};\n\t}\n}\n\n/** Internal dynamic resource — not part of the public API. */\nclass RailwayServiceResource extends pulumi.dynamic.Resource {\n\tpublic declare readonly serviceId: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\ttoken: pulumi.Input<string>;\n\t\t\tprojectId: pulumi.Input<string>;\n\t\t\tenvironmentId: pulumi.Input<string>;\n\t\t\tname: pulumi.Input<string>;\n\t\t\ticon?: pulumi.Input<string>;\n\t\t\tsource?: pulumi.Input<{ image: pulumi.Input<string> }>;\n\t\t\tbuilder?: pulumi.Input<RailwayBuilder>;\n\t\t\tbuildCommand?: pulumi.Input<string>;\n\t\t\tstartCommand?: pulumi.Input<string>;\n\t\t\trestartPolicyType?: pulumi.Input<RailwayRestartPolicy>;\n\t\t\thealthcheckPath?: pulumi.Input<string>;\n\t\t\thealthcheckTimeout?: pulumi.Input<number>;\n\t\t\tpreDeployCommand?: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew RailwayServiceResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, serviceId: undefined },\n\t\t\topts,\n\t\t);\n\t}\n}\n\n/** Options type for RailwayService — replaces Pulumi's native `provider` field. */\ntype RailwayServiceOptions = Omit<\n\tpulumi.ComponentResourceOptions,\n\t\"provider\"\n> & {\n\t/** Railway authentication context. */\n\tprovider: RailwayProvider;\n\n\t/** Railway project context. */\n\tproject: RailwayProject;\n\n\t/** Railway environment context. */\n\tenvironment: RailwayEnvironment;\n};\n\n/** Args for RailwayService. */\nexport interface RailwayServiceArgs {\n\t/** Human-readable service name used for adopt-or-create matching. */\n\tname: pulumi.Input<string>;\n\n\t/** SVG icon URL displayed in the Railway dashboard. */\n\ticon?: pulumi.Input<string>;\n\n\t/** Docker image source for image-based services. */\n\tsource?: pulumi.Input<{ image: pulumi.Input<string> }>;\n\n\t/** Build system to use when building the service. */\n\tbuilder?: pulumi.Input<RailwayBuilder>;\n\n\t/** Shell command executed during the build phase. */\n\tbuildCommand?: pulumi.Input<string>;\n\n\t/** Shell command executed to start the service at runtime. */\n\tstartCommand?: pulumi.Input<string>;\n\n\t/** Restart behavior for the service container. */\n\trestartPolicyType?: pulumi.Input<RailwayRestartPolicy>;\n\n\t/** HTTP path polled for health checks (e.g. `\"/health-check\"`). */\n\thealthcheckPath?: pulumi.Input<string>;\n\n\t/** Seconds to wait for a healthy response before marking unhealthy. */\n\thealthcheckTimeout?: pulumi.Input<number>;\n\n\t/** Shell command executed before the main deploy (e.g. migrations). */\n\tpreDeployCommand?: pulumi.Input<string>;\n}\n\n/**\n * Manages a Railway service with adopt-or-create semantics.\n *\n * @example\n * ```typescript\n * const service = new RailwayService(\"api\", {\n * name: \"api\",\n * builder: RailwayBuilder.RAILPACK,\n * startCommand: \"node dist/index.js\",\n * healthcheckPath: \"/health\",\n * }, { provider, project, environment });\n *\n * // Use serviceId downstream\n * new RailwayVariable(\"api-vars\", {\n * variables: { DATABASE_URL: dbUrl },\n * }, { provider, project, environment, service });\n * ```\n */\nexport class RailwayService extends pulumi.ComponentResource {\n\t/** Railway service UUID. */\n\tpublic readonly id: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: RailwayServiceArgs,\n\t\topts: RailwayServiceOptions,\n\t) {\n\t\tconst { provider, project, environment, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:railway:Service\", name, {}, pulumiOpts);\n\n\t\tconst resource = new RailwayServiceResource(\n\t\t\t`${name}-resource`,\n\t\t\t{\n\t\t\t\ttoken: provider.token,\n\t\t\t\tprojectId: project.id,\n\t\t\t\tenvironmentId: environment.id,\n\t\t\t\t...args,\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.id = resource.serviceId;\n\n\t\tthis.registerOutputs({ id: this.id });\n\t}\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.cts","names":[],"sources":["../../src/railway/service.ts"],"mappings":";;;;;;;;;;AAYA;;;aAAY,cAAA;EACX,QAAA;EACA,QAAA;EACA,UAAA;EACA,MAAA;EACA,MAAA;AAAA;AAAM;AAOP;;;AAPO,aAOK,oBAAA;EACX,UAAA;EACA,MAAA;EACA,KAAA;AAAA;AAAK;AAAA,UAII,oBAAA;EAAoB;EAE7B,KAAK;AAAA;AAAA;AAAA,UAII,oBAAA;EAAoB;EAE7B,KAAA;EAeS;EAZT,SAAA;EAwBoB;EArBpB,aAAA;EAqBwC;EAlBxC,IAAA;EANA;EASA,IAAA;EAHA;EAMA,MAAA,GAAS,oBAAA;EAAT;EAGA,OAAA,GAAU,cAAA;EAAV;EAGA,YAAA;EAAA;EAGA,YAAA;EAGA;EAAA,iBAAA,GAAoB,oBAAA;EAGpB;EAAA,eAAA;EAMA;EAHA,kBAAA;EAGgB;EAAhB,gBAAA;AAAA;;UAIS,qBAAA,SAA8B,oBAAoB;EAElD;EAAT,SAAS;AAAA;;;;;;;;;;cAgJG,8BAAA,YACD,MAAA,CAAO,OAAA,CAAQ,gBAAA;EAwGvB;;;EAnGG,MAAA,CACL,MAAA,EAAQ,oBAAA,GACN,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EAqIf;;;EAtEL,MAAA,CACL,EAAA,UACA,IAAA,EAAM,qBAAA,EACN,IAAA,EAAM,oBAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EA1EgB;;;EAqGpC,IAAA,CACL,EAAA,UACA,KAAA,EAAO,qBAAA,GACL,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;EAnGpB;;;;;;EAwHA,MAAA,CAAA,GAAU,OAAA;EAvDV;;;;;EAkEA,IAAA,CACL,GAAA,UACA,IAAA,EAAM,qBAAA,EACN,IAAA,EAAM,oBAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;AAAA;;KAyFtB,qBAAA,GAAwB,IAAA,CAC5B,MAAA,CAAO,wBAAA;EA5JmB,sCAgK1B,QAAA,EAAU,eAAA,EApIT;EAuID,OAAA,EAAS,cAAA,EAtIR;EAyID,WAAA,EAAa,kBAAA;AAAA;;UAIG,kBAAA;EAvHV;EAyHN,IAAA,EAAM,MAAA,CAAO,KAAA;EA9GP;EAiHN,IAAA,GAAO,MAAA,CAAO,KAAA;EA/GP;EAkHP,MAAA,GAAS,MAAA,CAAO,KAAA;IAAQ,KAAA,EAAO,MAAA,CAAO,KAAA;EAAA;EAhHnC;EAmHH,OAAA,GAAU,MAAA,CAAO,KAAA,CAAM,cAAA;EAnHL;EAsHlB,YAAA,GAAe,MAAA,CAAO,KAAA;EAtHc;EAyHpC,YAAA,GAAe,MAAA,CAAO,KAAA;EAhClB;EAmCJ,iBAAA,GAAoB,MAAA,CAAO,KAAA,CAAM,oBAAA;;EAGjC,eAAA,GAAkB,MAAA,CAAO,KAAA;EAtCG;EAyC5B,kBAAA,GAAqB,MAAA,CAAO,KAAA;EAjCnB;EAoCT,gBAAA,GAAmB,MAAA,CAAO,KAAA;AAAA;;;;;;;;;;;;AAjCK;AAIhC;;;;;;cAkDa,cAAA,SAAuB,MAAA,CAAO,iBAAA;EAvCnB;EAAA,SAyCP,EAAA,EAAI,MAAA,CAAO,MAAA;cAG1B,IAAA,UACA,IAAA,EAAM,kBAAA,EACN,IAAA,EAAM,qBAAA;AAAA"}
1
+ {"version":3,"file":"service.d.cts","names":[],"sources":["../../src/railway/service.ts"],"mappings":";;;;;;;;;;AAYA;;;aAAY,cAAA;EACX,QAAA;EACA,QAAA;EACA,UAAA;EACA,MAAA;EACA,MAAA;AAAA;AAAM;AAOP;;;AAPO,aAOK,oBAAA;EACX,UAAA;EACA,MAAA;EACA,KAAA;AAAA;AAAK;AAAA,UAII,oBAAA;EAAoB;EAE7B,KAAK;AAAA;AAAA;AAAA,UAII,oBAAA;EAAoB;EAE7B,KAAA;EAeS;EAZT,SAAA;EAwBoB;EArBpB,aAAA;EAqBwC;EAlBxC,IAAA;EANA;EASA,IAAA;EAHA;EAMA,MAAA,GAAS,oBAAA;EAAT;EAGA,OAAA,GAAU,cAAA;EAAV;EAGA,YAAA;EAAA;EAGA,YAAA;EAGA;EAAA,iBAAA,GAAoB,oBAAA;EAGpB;EAAA,eAAA;EAMA;EAHA,kBAAA;EAGgB;EAAhB,gBAAA;AAAA;;UAIS,qBAAA,SAA8B,oBAAoB;EAElD;EAAT,SAAS;AAAA;;;;;;;;;;cAuLG,8BAAA,YACD,MAAA,CAAO,OAAA,CAAQ,gBAAA;EAqHvB;;;EAhHG,MAAA,CACL,MAAA,EAAQ,oBAAA,GACN,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EAkJf;;;EA7EL,MAAA,CACL,EAAA,UACA,IAAA,EAAM,qBAAA,EACN,IAAA,EAAM,oBAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EAhFgB;;;EAkHpC,IAAA,CACL,EAAA,UACA,KAAA,EAAO,qBAAA,GACL,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;EAhHpB;;;;;;EAqIA,MAAA,CAAA,GAAU,OAAA;EA9DV;;;;;EAyEA,IAAA,CACL,GAAA,UACA,IAAA,EAAM,qBAAA,EACN,IAAA,EAAM,oBAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;AAAA;;KAyFtB,qBAAA,GAAwB,IAAA,CAC5B,MAAA,CAAO,wBAAA;EAnKmB,sCAuK1B,QAAA,EAAU,eAAA,EApIT;EAuID,OAAA,EAAS,cAAA,EAtIR;EAyID,WAAA,EAAa,kBAAA;AAAA;;UAIG,kBAAA;EAvHV;EAyHN,IAAA,EAAM,MAAA,CAAO,KAAA;EA9GP;EAiHN,IAAA,GAAO,MAAA,CAAO,KAAA;EA/GP;EAkHP,MAAA,GAAS,MAAA,CAAO,KAAA;IAAQ,KAAA,EAAO,MAAA,CAAO,KAAA;EAAA;EAhHnC;EAmHH,OAAA,GAAU,MAAA,CAAO,KAAA,CAAM,cAAA;EAnHL;EAsHlB,YAAA,GAAe,MAAA,CAAO,KAAA;EAtHc;EAyHpC,YAAA,GAAe,MAAA,CAAO,KAAA;EAhClB;EAmCJ,iBAAA,GAAoB,MAAA,CAAO,KAAA,CAAM,oBAAA;;EAGjC,eAAA,GAAkB,MAAA,CAAO,KAAA;EAtCG;EAyC5B,kBAAA,GAAqB,MAAA,CAAO,KAAA;EAjCnB;EAoCT,gBAAA,GAAmB,MAAA,CAAO,KAAA;AAAA;;;;;;;;;;;;AAjCK;AAIhC;;;;;;cAkDa,cAAA,SAAuB,MAAA,CAAO,iBAAA;EAvCnB;EAAA,SAyCP,EAAA,EAAI,MAAA,CAAO,MAAA;cAG1B,IAAA,UACA,IAAA,EAAM,kBAAA,EACN,IAAA,EAAM,qBAAA;AAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.mts","names":[],"sources":["../../src/railway/service.ts"],"mappings":";;;;;;;;;;AAYA;;;aAAY,cAAA;EACX,QAAA;EACA,QAAA;EACA,UAAA;EACA,MAAA;EACA,MAAA;AAAA;AAAM;AAOP;;;AAPO,aAOK,oBAAA;EACX,UAAA;EACA,MAAA;EACA,KAAA;AAAA;AAAK;AAAA,UAII,oBAAA;EAAoB;EAE7B,KAAK;AAAA;AAAA;AAAA,UAII,oBAAA;EAAoB;EAE7B,KAAA;EAeS;EAZT,SAAA;EAwBoB;EArBpB,aAAA;EAqBwC;EAlBxC,IAAA;EANA;EASA,IAAA;EAHA;EAMA,MAAA,GAAS,oBAAA;EAAT;EAGA,OAAA,GAAU,cAAA;EAAV;EAGA,YAAA;EAAA;EAGA,YAAA;EAGA;EAAA,iBAAA,GAAoB,oBAAA;EAGpB;EAAA,eAAA;EAMA;EAHA,kBAAA;EAGgB;EAAhB,gBAAA;AAAA;;UAIS,qBAAA,SAA8B,oBAAoB;EAElD;EAAT,SAAS;AAAA;;;;;;;;;;cAgJG,8BAAA,YACD,MAAA,CAAO,OAAA,CAAQ,gBAAA;EAwGvB;;;EAnGG,MAAA,CACL,MAAA,EAAQ,oBAAA,GACN,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EAqIf;;;EAtEL,MAAA,CACL,EAAA,UACA,IAAA,EAAM,qBAAA,EACN,IAAA,EAAM,oBAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EA1EgB;;;EAqGpC,IAAA,CACL,EAAA,UACA,KAAA,EAAO,qBAAA,GACL,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;EAnGpB;;;;;;EAwHA,MAAA,CAAA,GAAU,OAAA;EAvDV;;;;;EAkEA,IAAA,CACL,GAAA,UACA,IAAA,EAAM,qBAAA,EACN,IAAA,EAAM,oBAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;AAAA;;KAyFtB,qBAAA,GAAwB,IAAA,CAC5B,MAAA,CAAO,wBAAA;EA5JmB,sCAgK1B,QAAA,EAAU,eAAA,EApIT;EAuID,OAAA,EAAS,cAAA,EAtIR;EAyID,WAAA,EAAa,kBAAA;AAAA;;UAIG,kBAAA;EAvHV;EAyHN,IAAA,EAAM,MAAA,CAAO,KAAA;EA9GP;EAiHN,IAAA,GAAO,MAAA,CAAO,KAAA;EA/GP;EAkHP,MAAA,GAAS,MAAA,CAAO,KAAA;IAAQ,KAAA,EAAO,MAAA,CAAO,KAAA;EAAA;EAhHnC;EAmHH,OAAA,GAAU,MAAA,CAAO,KAAA,CAAM,cAAA;EAnHL;EAsHlB,YAAA,GAAe,MAAA,CAAO,KAAA;EAtHc;EAyHpC,YAAA,GAAe,MAAA,CAAO,KAAA;EAhClB;EAmCJ,iBAAA,GAAoB,MAAA,CAAO,KAAA,CAAM,oBAAA;;EAGjC,eAAA,GAAkB,MAAA,CAAO,KAAA;EAtCG;EAyC5B,kBAAA,GAAqB,MAAA,CAAO,KAAA;EAjCnB;EAoCT,gBAAA,GAAmB,MAAA,CAAO,KAAA;AAAA;;;;;;;;;;;;AAjCK;AAIhC;;;;;;cAkDa,cAAA,SAAuB,MAAA,CAAO,iBAAA;EAvCnB;EAAA,SAyCP,EAAA,EAAI,MAAA,CAAO,MAAA;cAG1B,IAAA,UACA,IAAA,EAAM,kBAAA,EACN,IAAA,EAAM,qBAAA;AAAA"}
1
+ {"version":3,"file":"service.d.mts","names":[],"sources":["../../src/railway/service.ts"],"mappings":";;;;;;;;;;AAYA;;;aAAY,cAAA;EACX,QAAA;EACA,QAAA;EACA,UAAA;EACA,MAAA;EACA,MAAA;AAAA;AAAM;AAOP;;;AAPO,aAOK,oBAAA;EACX,UAAA;EACA,MAAA;EACA,KAAA;AAAA;AAAK;AAAA,UAII,oBAAA;EAAoB;EAE7B,KAAK;AAAA;AAAA;AAAA,UAII,oBAAA;EAAoB;EAE7B,KAAA;EAeS;EAZT,SAAA;EAwBoB;EArBpB,aAAA;EAqBwC;EAlBxC,IAAA;EANA;EASA,IAAA;EAHA;EAMA,MAAA,GAAS,oBAAA;EAAT;EAGA,OAAA,GAAU,cAAA;EAAV;EAGA,YAAA;EAAA;EAGA,YAAA;EAGA;EAAA,iBAAA,GAAoB,oBAAA;EAGpB;EAAA,eAAA;EAMA;EAHA,kBAAA;EAGgB;EAAhB,gBAAA;AAAA;;UAIS,qBAAA,SAA8B,oBAAoB;EAElD;EAAT,SAAS;AAAA;;;;;;;;;;cAuLG,8BAAA,YACD,MAAA,CAAO,OAAA,CAAQ,gBAAA;EAqHvB;;;EAhHG,MAAA,CACL,MAAA,EAAQ,oBAAA,GACN,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EAkJf;;;EA7EL,MAAA,CACL,EAAA,UACA,IAAA,EAAM,qBAAA,EACN,IAAA,EAAM,oBAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EAhFgB;;;EAkHpC,IAAA,CACL,EAAA,UACA,KAAA,EAAO,qBAAA,GACL,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;EAhHpB;;;;;;EAqIA,MAAA,CAAA,GAAU,OAAA;EA9DV;;;;;EAyEA,IAAA,CACL,GAAA,UACA,IAAA,EAAM,qBAAA,EACN,IAAA,EAAM,oBAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;AAAA;;KAyFtB,qBAAA,GAAwB,IAAA,CAC5B,MAAA,CAAO,wBAAA;EAnKmB,sCAuK1B,QAAA,EAAU,eAAA,EApIT;EAuID,OAAA,EAAS,cAAA,EAtIR;EAyID,WAAA,EAAa,kBAAA;AAAA;;UAIG,kBAAA;EAvHV;EAyHN,IAAA,EAAM,MAAA,CAAO,KAAA;EA9GP;EAiHN,IAAA,GAAO,MAAA,CAAO,KAAA;EA/GP;EAkHP,MAAA,GAAS,MAAA,CAAO,KAAA;IAAQ,KAAA,EAAO,MAAA,CAAO,KAAA;EAAA;EAhHnC;EAmHH,OAAA,GAAU,MAAA,CAAO,KAAA,CAAM,cAAA;EAnHL;EAsHlB,YAAA,GAAe,MAAA,CAAO,KAAA;EAtHc;EAyHpC,YAAA,GAAe,MAAA,CAAO,KAAA;EAhClB;EAmCJ,iBAAA,GAAoB,MAAA,CAAO,KAAA,CAAM,oBAAA;;EAGjC,eAAA,GAAkB,MAAA,CAAO,KAAA;EAtCG;EAyC5B,kBAAA,GAAqB,MAAA,CAAO,KAAA;EAjCnB;EAoCT,gBAAA,GAAmB,MAAA,CAAO,KAAA;AAAA;;;;;;;;;;;;AAjCK;AAIhC;;;;;;cAkDa,cAAA,SAAuB,MAAA,CAAO,iBAAA;EAvCnB;EAAA,SAyCP,EAAA,EAAI,MAAA,CAAO,MAAA;cAG1B,IAAA,UACA,IAAA,EAAM,kBAAA,EACN,IAAA,EAAM,qBAAA;AAAA"}
@@ -85,12 +85,35 @@ const SERVICE_CONNECT = `
85
85
  }
86
86
  }
87
87
  `;
88
+ const SERVICE_INSTANCE_DEPLOY = `
89
+ mutation($serviceId: String!, $environmentId: String!) {
90
+ serviceInstanceDeployV2(serviceId: $serviceId, environmentId: $environmentId)
91
+ }
92
+ `;
93
+ /**
94
+ * Triggers a deployment of the service instance in the target environment.
95
+ * Image-sourced services never deploy there on their own: `serviceCreate`'s
96
+ * auto-deploy only reaches the project's DEFAULT environment, and
97
+ * `serviceInstanceUpdate` applies config without redeploying — so a service
98
+ * in any other environment stays undeployed forever and its private DNS name
99
+ * never registers. `environmentTriggersDeploy` is no alternative: it returns
100
+ * success without creating anything for a service that has never deployed in
101
+ * that environment (proven live, 2026-07-06).
102
+ */
103
+ async function deployServiceInstance(client, serviceId, environmentId) {
104
+ const result = await client.query(SERVICE_INSTANCE_DEPLOY, {
105
+ serviceId,
106
+ environmentId
107
+ });
108
+ pulumi.log.info(`[infracraft] serviceInstanceDeployV2 created deployment ${result.serviceInstanceDeployV2}`);
109
+ }
88
110
  /**
89
111
  * Applies service instance configuration (builder, commands, healthcheck).
90
112
  * Retries without healthcheck fields if the first call fails.
91
113
  */
92
114
  async function applyInstanceConfig(client, serviceId, environmentId, inputs) {
93
115
  const instanceInput = {};
116
+ if (inputs.source) instanceInput.source = { image: inputs.source.image };
94
117
  if (inputs.builder) instanceInput.builder = inputs.builder;
95
118
  if (inputs.buildCommand) instanceInput.buildCommand = inputs.buildCommand;
96
119
  if (inputs.startCommand) instanceInput.startCommand = inputs.startCommand;
@@ -152,6 +175,7 @@ var RailwayServiceResourceProvider = class {
152
175
  });
153
176
  }
154
177
  await applyInstanceConfig(client, serviceId, inputs.environmentId, inputs);
178
+ if (inputs.source) await deployServiceInstance(client, serviceId, inputs.environmentId);
155
179
  const outs = {
156
180
  ...inputs,
157
181
  serviceId
@@ -174,6 +198,7 @@ var RailwayServiceResourceProvider = class {
174
198
  input: updateInput
175
199
  });
176
200
  await applyInstanceConfig(client, id, news.environmentId, news);
201
+ if (news.source) await deployServiceInstance(client, id, news.environmentId);
177
202
  return { outs: {
178
203
  ...news,
179
204
  serviceId: id
@@ -1 +1 @@
1
- {"version":3,"file":"service.mjs","names":[],"sources":["../../src/railway/service.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport { RailwayClient } from \"./client\";\nimport type { RailwayEnvironment } from \"./environment\";\nimport type { RailwayProject } from \"./project\";\nimport type { RailwayProvider } from \"./provider\";\n\n/**\n * Railway build system. Enum keys are UPPERCASE per convention; values are\n * Railway's required UPPERCASE wire literals.\n * Note: HEROKU and PAKETO were deprecated Feb 21 2025 and auto-migrated to\n * NIXPACKS by Railway, but remain in the schema and are accepted by the API.\n */\nexport enum RailwayBuilder {\n\tRAILPACK = \"RAILPACK\",\n\tNIXPACKS = \"NIXPACKS\",\n\tDOCKERFILE = \"DOCKERFILE\",\n\tHEROKU = \"HEROKU\",\n\tPAKETO = \"PAKETO\",\n}\n\n/**\n * Railway service restart policy. Controls when Railway restarts the service\n * container after it exits. Default is ON_FAILURE.\n */\nexport enum RailwayRestartPolicy {\n\tON_FAILURE = \"ON_FAILURE\",\n\tALWAYS = \"ALWAYS\",\n\tNEVER = \"NEVER\",\n}\n\n/** Docker image source for a Railway service (e.g. `redis:8-alpine`). */\ninterface RailwayServiceSource {\n\t/** Full Docker image reference including tag. */\n\timage: string;\n}\n\n/** Resolved inputs for the Railway service dynamic provider. */\ninterface RailwayServiceInputs {\n\t/** Railway API bearer token. */\n\ttoken: string;\n\n\t/** Railway project UUID. */\n\tprojectId: string;\n\n\t/** Railway environment UUID (e.g. production). */\n\tenvironmentId: string;\n\n\t/** Human-readable service name used for adopt-or-create matching. */\n\tname: string;\n\n\t/** SVG icon URL displayed in the Railway dashboard. */\n\ticon?: string;\n\n\t/** Docker image source for image-based services. */\n\tsource?: RailwayServiceSource;\n\n\t/** Build system to use when building the service. */\n\tbuilder?: RailwayBuilder;\n\n\t/** Shell command executed during the build phase. */\n\tbuildCommand?: string;\n\n\t/** Shell command executed to start the service at runtime. */\n\tstartCommand?: string;\n\n\t/** Restart behavior for the service container. */\n\trestartPolicyType?: RailwayRestartPolicy;\n\n\t/** HTTP path polled for health checks (e.g. `\"/health-check\"`). */\n\thealthcheckPath?: string;\n\n\t/** Seconds to wait for a healthy response before marking unhealthy. */\n\thealthcheckTimeout?: number;\n\n\t/** Shell command executed before the main deploy (e.g. migrations). */\n\tpreDeployCommand?: string;\n}\n\n/** Persisted state for the Railway service, extending inputs with the Railway-assigned ID. */\ninterface RailwayServiceOutputs extends RailwayServiceInputs {\n\t/** Railway-assigned service UUID. */\n\tserviceId: string;\n}\n\nconst SERVICES_QUERY = `\n query($projectId: String!) {\n project(id: $projectId) {\n services {\n edges {\n node {\n id\n name\n }\n }\n }\n }\n }\n`;\n\nconst SERVICE_QUERY = `\n query($serviceId: String!) {\n service(id: $serviceId) {\n id\n name\n }\n }\n`;\n\nconst SERVICE_CREATE = `\n mutation($input: ServiceCreateInput!) {\n serviceCreate(input: $input) {\n id\n name\n }\n }\n`;\n\nconst SERVICE_UPDATE = `\n mutation($id: String!, $input: ServiceUpdateInput!) {\n serviceUpdate(id: $id, input: $input) {\n id\n name\n }\n }\n`;\n\nconst SERVICE_INSTANCE_UPDATE = `\n mutation(\n $serviceId: String!\n $environmentId: String!\n $input: ServiceInstanceUpdateInput!\n ) {\n serviceInstanceUpdate(\n serviceId: $serviceId\n environmentId: $environmentId\n input: $input\n )\n }\n`;\n\nconst SERVICE_CONNECT = `\n mutation($id: String!, $input: ServiceConnectInput!) {\n serviceConnect(id: $id, input: $input) {\n id\n }\n }\n`;\n\n/**\n * Applies service instance configuration (builder, commands, healthcheck).\n * Retries without healthcheck fields if the first call fails.\n */\nasync function applyInstanceConfig(\n\tclient: RailwayClient,\n\tserviceId: string,\n\tenvironmentId: string,\n\tinputs: RailwayServiceInputs,\n): Promise<void> {\n\tconst instanceInput: Record<string, unknown> = {};\n\n\tif (inputs.builder) {\n\t\tinstanceInput.builder = inputs.builder;\n\t}\n\n\tif (inputs.buildCommand) {\n\t\tinstanceInput.buildCommand = inputs.buildCommand;\n\t}\n\n\tif (inputs.startCommand) {\n\t\tinstanceInput.startCommand = inputs.startCommand;\n\t}\n\n\tif (inputs.restartPolicyType) {\n\t\tinstanceInput.restartPolicyType = inputs.restartPolicyType;\n\t}\n\n\tif (inputs.healthcheckPath) {\n\t\tinstanceInput.healthcheckPath = inputs.healthcheckPath;\n\t}\n\n\tif (inputs.healthcheckTimeout) {\n\t\tinstanceInput.healthcheckTimeout = inputs.healthcheckTimeout;\n\t}\n\n\tif (inputs.preDeployCommand) {\n\t\tinstanceInput.preDeployCommand = inputs.preDeployCommand;\n\t}\n\n\tif (Object.keys(instanceInput).length === 0) {\n\t\treturn;\n\t}\n\n\ttry {\n\t\tawait client.query(SERVICE_INSTANCE_UPDATE, {\n\t\t\tserviceId,\n\t\t\tenvironmentId,\n\t\t\tinput: instanceInput,\n\t\t});\n\t} catch (error) {\n\t\tpulumi.log.warn(\n\t\t\t`serviceInstanceUpdate failed, retrying without healthcheck fields: ${error}`,\n\t\t);\n\n\t\tdelete instanceInput.healthcheckPath;\n\t\tdelete instanceInput.healthcheckTimeout;\n\n\t\tif (Object.keys(instanceInput).length > 0) {\n\t\t\tawait client.query(SERVICE_INSTANCE_UPDATE, {\n\t\t\t\tserviceId,\n\t\t\t\tenvironmentId,\n\t\t\t\tinput: instanceInput,\n\t\t\t});\n\t\t}\n\t}\n}\n\n/**\n * Dynamic provider implementing CRUD for Railway services.\n *\n * Uses adopt-or-create on `create()`: queries services by project ID and name\n * before creating a new one. Service instance configuration (builder, commands,\n * healthcheck) is applied via `serviceInstanceUpdate` after create or update.\n *\n * @internal Exported only for unit testing; not part of the public API surface.\n */\nexport class RailwayServiceResourceProvider\n\timplements pulumi.dynamic.ResourceProvider\n{\n\t/**\n\t * Creates or adopts a Railway service by name, then applies instance config.\n\t */\n\tasync create(\n\t\tinputs: RailwayServiceInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new RailwayClient(inputs.token);\n\n\t\tconst result = await client.query<{\n\t\t\tproject: {\n\t\t\t\tservices: { edges: Array<{ node: { id: string; name: string } }> };\n\t\t\t};\n\t\t}>(SERVICES_QUERY, { projectId: inputs.projectId });\n\n\t\tlet serviceId = result.project.services.edges.find(\n\t\t\t(edge) => edge.node.name === inputs.name,\n\t\t)?.node.id;\n\n\t\tif (serviceId) {\n\t\t\tpulumi.log.info(\n\t\t\t\t`Adopted existing Railway service \"${inputs.name}\" (${serviceId})`,\n\t\t\t);\n\t\t} else {\n\t\t\tconst createInput: Record<string, unknown> = {\n\t\t\t\tprojectId: inputs.projectId,\n\t\t\t\tenvironmentId: inputs.environmentId,\n\t\t\t\tname: inputs.name,\n\t\t\t};\n\n\t\t\tif (inputs.source) {\n\t\t\t\tcreateInput.source = { image: inputs.source.image };\n\t\t\t}\n\n\t\t\tconst created = await client.query<{\n\t\t\t\tserviceCreate: { id: string; name: string };\n\t\t\t}>(SERVICE_CREATE, { input: createInput });\n\n\t\t\tserviceId = created.serviceCreate.id;\n\n\t\t\tpulumi.log.info(\n\t\t\t\t`Created Railway service \"${inputs.name}\" (${serviceId})`,\n\t\t\t);\n\n\t\t\tif (inputs.source) {\n\t\t\t\tawait client.query(SERVICE_CONNECT, {\n\t\t\t\t\tid: serviceId,\n\t\t\t\t\tinput: { image: inputs.source.image },\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (inputs.icon) {\n\t\t\t\tawait client.query(SERVICE_UPDATE, {\n\t\t\t\t\tid: serviceId,\n\t\t\t\t\tinput: { icon: inputs.icon },\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tawait applyInstanceConfig(client, serviceId, inputs.environmentId, inputs);\n\n\t\tconst outs: RailwayServiceOutputs = { ...inputs, serviceId };\n\n\t\treturn { id: serviceId, outs };\n\t}\n\n\t/**\n\t * Updates service name/icon and re-applies instance configuration.\n\t */\n\tasync update(\n\t\tid: string,\n\t\tolds: RailwayServiceOutputs,\n\t\tnews: RailwayServiceInputs,\n\t): Promise<pulumi.dynamic.UpdateResult> {\n\t\tconst client = new RailwayClient(news.token);\n\n\t\tconst updateInput: Record<string, unknown> = {};\n\n\t\tif (olds.name !== news.name) {\n\t\t\tupdateInput.name = news.name;\n\t\t}\n\n\t\tif (news.icon && olds.icon !== news.icon) {\n\t\t\tupdateInput.icon = news.icon;\n\t\t}\n\n\t\tif (Object.keys(updateInput).length > 0) {\n\t\t\tawait client.query(SERVICE_UPDATE, { id, input: updateInput });\n\t\t}\n\n\t\tawait applyInstanceConfig(client, id, news.environmentId, news);\n\n\t\tconst outs: RailwayServiceOutputs = { ...news, serviceId: id };\n\n\t\treturn { outs };\n\t}\n\n\t/**\n\t * Reads current state for `pulumi refresh` by querying the service by ID.\n\t */\n\tasync read(\n\t\tid: string,\n\t\tprops: RailwayServiceOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\ttry {\n\t\t\tawait client.query<{ service: { id: string; name: string } }>(\n\t\t\t\tSERVICE_QUERY,\n\t\t\t\t{ serviceId: id },\n\t\t\t);\n\t\t} catch {\n\t\t\tthrow new Error(`Railway service \"${props.name}\" (${id}) not found`);\n\t\t}\n\n\t\treturn { id, props: { ...props, serviceId: id } };\n\t}\n\n\t/**\n\t * Deletion is a no-op. A Railway service is a project-level resource shared\n\t * across environments (forked environments adopt it by name), so a single\n\t * environment's destroy must never delete it. Deleting the *environment*\n\t * removes that environment's service instances instead.\n\t */\n\tasync delete(): Promise<void> {\n\t\tpulumi.log.warn(\n\t\t\t\"Railway service deletion skipped — services are project-level; delete the environment to remove its instances\",\n\t\t);\n\t}\n\n\t/**\n\t * Compares old and new inputs to determine what changed.\n\t *\n\t * ProjectId and environmentId changes trigger replacement.\n\t */\n\tasync diff(\n\t\t_id: string,\n\t\tolds: RailwayServiceOutputs,\n\t\tnews: RailwayServiceInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\t\tconst changes: string[] = [];\n\n\t\tif (olds.name !== news.name) {\n\t\t\tchanges.push(\"name\");\n\t\t}\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\treplaces.push(\"projectId\");\n\t\t}\n\n\t\tif (olds.environmentId !== news.environmentId) {\n\t\t\treplaces.push(\"environmentId\");\n\t\t}\n\n\t\tif (olds.builder !== news.builder) {\n\t\t\tchanges.push(\"builder\");\n\t\t}\n\n\t\tif (olds.buildCommand !== news.buildCommand) {\n\t\t\tchanges.push(\"buildCommand\");\n\t\t}\n\n\t\tif (olds.startCommand !== news.startCommand) {\n\t\t\tchanges.push(\"startCommand\");\n\t\t}\n\n\t\tif (olds.restartPolicyType !== news.restartPolicyType) {\n\t\t\tchanges.push(\"restartPolicyType\");\n\t\t}\n\n\t\tif (olds.healthcheckPath !== news.healthcheckPath) {\n\t\t\tchanges.push(\"healthcheckPath\");\n\t\t}\n\n\t\tif (olds.healthcheckTimeout !== news.healthcheckTimeout) {\n\t\t\tchanges.push(\"healthcheckTimeout\");\n\t\t}\n\n\t\tif (olds.preDeployCommand !== news.preDeployCommand) {\n\t\t\tchanges.push(\"preDeployCommand\");\n\t\t}\n\n\t\tif (olds.icon !== news.icon) {\n\t\t\tchanges.push(\"icon\");\n\t\t}\n\n\t\treturn {\n\t\t\tchanges: replaces.length > 0 || changes.length > 0,\n\t\t\treplaces,\n\t\t\tdeleteBeforeReplace: true,\n\t\t};\n\t}\n}\n\n/** Internal dynamic resource — not part of the public API. */\nclass RailwayServiceResource extends pulumi.dynamic.Resource {\n\tpublic declare readonly serviceId: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\ttoken: pulumi.Input<string>;\n\t\t\tprojectId: pulumi.Input<string>;\n\t\t\tenvironmentId: pulumi.Input<string>;\n\t\t\tname: pulumi.Input<string>;\n\t\t\ticon?: pulumi.Input<string>;\n\t\t\tsource?: pulumi.Input<{ image: pulumi.Input<string> }>;\n\t\t\tbuilder?: pulumi.Input<RailwayBuilder>;\n\t\t\tbuildCommand?: pulumi.Input<string>;\n\t\t\tstartCommand?: pulumi.Input<string>;\n\t\t\trestartPolicyType?: pulumi.Input<RailwayRestartPolicy>;\n\t\t\thealthcheckPath?: pulumi.Input<string>;\n\t\t\thealthcheckTimeout?: pulumi.Input<number>;\n\t\t\tpreDeployCommand?: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew RailwayServiceResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, serviceId: undefined },\n\t\t\topts,\n\t\t);\n\t}\n}\n\n/** Options type for RailwayService — replaces Pulumi's native `provider` field. */\ntype RailwayServiceOptions = Omit<\n\tpulumi.ComponentResourceOptions,\n\t\"provider\"\n> & {\n\t/** Railway authentication context. */\n\tprovider: RailwayProvider;\n\n\t/** Railway project context. */\n\tproject: RailwayProject;\n\n\t/** Railway environment context. */\n\tenvironment: RailwayEnvironment;\n};\n\n/** Args for RailwayService. */\nexport interface RailwayServiceArgs {\n\t/** Human-readable service name used for adopt-or-create matching. */\n\tname: pulumi.Input<string>;\n\n\t/** SVG icon URL displayed in the Railway dashboard. */\n\ticon?: pulumi.Input<string>;\n\n\t/** Docker image source for image-based services. */\n\tsource?: pulumi.Input<{ image: pulumi.Input<string> }>;\n\n\t/** Build system to use when building the service. */\n\tbuilder?: pulumi.Input<RailwayBuilder>;\n\n\t/** Shell command executed during the build phase. */\n\tbuildCommand?: pulumi.Input<string>;\n\n\t/** Shell command executed to start the service at runtime. */\n\tstartCommand?: pulumi.Input<string>;\n\n\t/** Restart behavior for the service container. */\n\trestartPolicyType?: pulumi.Input<RailwayRestartPolicy>;\n\n\t/** HTTP path polled for health checks (e.g. `\"/health-check\"`). */\n\thealthcheckPath?: pulumi.Input<string>;\n\n\t/** Seconds to wait for a healthy response before marking unhealthy. */\n\thealthcheckTimeout?: pulumi.Input<number>;\n\n\t/** Shell command executed before the main deploy (e.g. migrations). */\n\tpreDeployCommand?: pulumi.Input<string>;\n}\n\n/**\n * Manages a Railway service with adopt-or-create semantics.\n *\n * @example\n * ```typescript\n * const service = new RailwayService(\"api\", {\n * name: \"api\",\n * builder: RailwayBuilder.RAILPACK,\n * startCommand: \"node dist/index.js\",\n * healthcheckPath: \"/health\",\n * }, { provider, project, environment });\n *\n * // Use serviceId downstream\n * new RailwayVariable(\"api-vars\", {\n * variables: { DATABASE_URL: dbUrl },\n * }, { provider, project, environment, service });\n * ```\n */\nexport class RailwayService extends pulumi.ComponentResource {\n\t/** Railway service UUID. */\n\tpublic readonly id: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: RailwayServiceArgs,\n\t\topts: RailwayServiceOptions,\n\t) {\n\t\tconst { provider, project, environment, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:railway:Service\", name, {}, pulumiOpts);\n\n\t\tconst resource = new RailwayServiceResource(\n\t\t\t`${name}-resource`,\n\t\t\t{\n\t\t\t\ttoken: provider.token,\n\t\t\t\tprojectId: project.id,\n\t\t\t\tenvironmentId: environment.id,\n\t\t\t\t...args,\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.id = resource.serviceId;\n\n\t\tthis.registerOutputs({ id: this.id });\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"service.mjs","names":[],"sources":["../../src/railway/service.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport { RailwayClient } from \"./client\";\nimport type { RailwayEnvironment } from \"./environment\";\nimport type { RailwayProject } from \"./project\";\nimport type { RailwayProvider } from \"./provider\";\n\n/**\n * Railway build system. Enum keys are UPPERCASE per convention; values are\n * Railway's required UPPERCASE wire literals.\n * Note: HEROKU and PAKETO were deprecated Feb 21 2025 and auto-migrated to\n * NIXPACKS by Railway, but remain in the schema and are accepted by the API.\n */\nexport enum RailwayBuilder {\n\tRAILPACK = \"RAILPACK\",\n\tNIXPACKS = \"NIXPACKS\",\n\tDOCKERFILE = \"DOCKERFILE\",\n\tHEROKU = \"HEROKU\",\n\tPAKETO = \"PAKETO\",\n}\n\n/**\n * Railway service restart policy. Controls when Railway restarts the service\n * container after it exits. Default is ON_FAILURE.\n */\nexport enum RailwayRestartPolicy {\n\tON_FAILURE = \"ON_FAILURE\",\n\tALWAYS = \"ALWAYS\",\n\tNEVER = \"NEVER\",\n}\n\n/** Docker image source for a Railway service (e.g. `redis:8-alpine`). */\ninterface RailwayServiceSource {\n\t/** Full Docker image reference including tag. */\n\timage: string;\n}\n\n/** Resolved inputs for the Railway service dynamic provider. */\ninterface RailwayServiceInputs {\n\t/** Railway API bearer token. */\n\ttoken: string;\n\n\t/** Railway project UUID. */\n\tprojectId: string;\n\n\t/** Railway environment UUID (e.g. production). */\n\tenvironmentId: string;\n\n\t/** Human-readable service name used for adopt-or-create matching. */\n\tname: string;\n\n\t/** SVG icon URL displayed in the Railway dashboard. */\n\ticon?: string;\n\n\t/** Docker image source for image-based services. */\n\tsource?: RailwayServiceSource;\n\n\t/** Build system to use when building the service. */\n\tbuilder?: RailwayBuilder;\n\n\t/** Shell command executed during the build phase. */\n\tbuildCommand?: string;\n\n\t/** Shell command executed to start the service at runtime. */\n\tstartCommand?: string;\n\n\t/** Restart behavior for the service container. */\n\trestartPolicyType?: RailwayRestartPolicy;\n\n\t/** HTTP path polled for health checks (e.g. `\"/health-check\"`). */\n\thealthcheckPath?: string;\n\n\t/** Seconds to wait for a healthy response before marking unhealthy. */\n\thealthcheckTimeout?: number;\n\n\t/** Shell command executed before the main deploy (e.g. migrations). */\n\tpreDeployCommand?: string;\n}\n\n/** Persisted state for the Railway service, extending inputs with the Railway-assigned ID. */\ninterface RailwayServiceOutputs extends RailwayServiceInputs {\n\t/** Railway-assigned service UUID. */\n\tserviceId: string;\n}\n\nconst SERVICES_QUERY = `\n query($projectId: String!) {\n project(id: $projectId) {\n services {\n edges {\n node {\n id\n name\n }\n }\n }\n }\n }\n`;\n\nconst SERVICE_QUERY = `\n query($serviceId: String!) {\n service(id: $serviceId) {\n id\n name\n }\n }\n`;\n\nconst SERVICE_CREATE = `\n mutation($input: ServiceCreateInput!) {\n serviceCreate(input: $input) {\n id\n name\n }\n }\n`;\n\nconst SERVICE_UPDATE = `\n mutation($id: String!, $input: ServiceUpdateInput!) {\n serviceUpdate(id: $id, input: $input) {\n id\n name\n }\n }\n`;\n\nconst SERVICE_INSTANCE_UPDATE = `\n mutation(\n $serviceId: String!\n $environmentId: String!\n $input: ServiceInstanceUpdateInput!\n ) {\n serviceInstanceUpdate(\n serviceId: $serviceId\n environmentId: $environmentId\n input: $input\n )\n }\n`;\n\nconst SERVICE_CONNECT = `\n mutation($id: String!, $input: ServiceConnectInput!) {\n serviceConnect(id: $id, input: $input) {\n id\n }\n }\n`;\n\nconst SERVICE_INSTANCE_DEPLOY = `\n mutation($serviceId: String!, $environmentId: String!) {\n serviceInstanceDeployV2(serviceId: $serviceId, environmentId: $environmentId)\n }\n`;\n\n/**\n * Triggers a deployment of the service instance in the target environment.\n * Image-sourced services never deploy there on their own: `serviceCreate`'s\n * auto-deploy only reaches the project's DEFAULT environment, and\n * `serviceInstanceUpdate` applies config without redeploying — so a service\n * in any other environment stays undeployed forever and its private DNS name\n * never registers. `environmentTriggersDeploy` is no alternative: it returns\n * success without creating anything for a service that has never deployed in\n * that environment (proven live, 2026-07-06).\n */\nasync function deployServiceInstance(\n\tclient: RailwayClient,\n\tserviceId: string,\n\tenvironmentId: string,\n): Promise<void> {\n\tconst result = await client.query<{ serviceInstanceDeployV2: string }>(\n\t\tSERVICE_INSTANCE_DEPLOY,\n\t\t{ serviceId, environmentId },\n\t);\n\n\tpulumi.log.info(\n\t\t`[infracraft] serviceInstanceDeployV2 created deployment ${result.serviceInstanceDeployV2}`,\n\t);\n}\n\n/**\n * Applies service instance configuration (builder, commands, healthcheck).\n * Retries without healthcheck fields if the first call fails.\n */\nasync function applyInstanceConfig(\n\tclient: RailwayClient,\n\tserviceId: string,\n\tenvironmentId: string,\n\tinputs: RailwayServiceInputs,\n): Promise<void> {\n\tconst instanceInput: Record<string, unknown> = {};\n\n\t// Source must be applied PER ENVIRONMENT: `ServiceCreateInput.source` only\n\t// configures the instance of the environment passed at create time, and every\n\t// other environment's instance is born with source=null — deploy triggers\n\t// then no-op silently because there is nothing to deploy.\n\tif (inputs.source) {\n\t\tinstanceInput.source = { image: inputs.source.image };\n\t}\n\n\tif (inputs.builder) {\n\t\tinstanceInput.builder = inputs.builder;\n\t}\n\n\tif (inputs.buildCommand) {\n\t\tinstanceInput.buildCommand = inputs.buildCommand;\n\t}\n\n\tif (inputs.startCommand) {\n\t\tinstanceInput.startCommand = inputs.startCommand;\n\t}\n\n\tif (inputs.restartPolicyType) {\n\t\tinstanceInput.restartPolicyType = inputs.restartPolicyType;\n\t}\n\n\tif (inputs.healthcheckPath) {\n\t\tinstanceInput.healthcheckPath = inputs.healthcheckPath;\n\t}\n\n\tif (inputs.healthcheckTimeout) {\n\t\tinstanceInput.healthcheckTimeout = inputs.healthcheckTimeout;\n\t}\n\n\tif (inputs.preDeployCommand) {\n\t\tinstanceInput.preDeployCommand = inputs.preDeployCommand;\n\t}\n\n\tif (Object.keys(instanceInput).length === 0) {\n\t\treturn;\n\t}\n\n\ttry {\n\t\tawait client.query(SERVICE_INSTANCE_UPDATE, {\n\t\t\tserviceId,\n\t\t\tenvironmentId,\n\t\t\tinput: instanceInput,\n\t\t});\n\t} catch (error) {\n\t\tpulumi.log.warn(\n\t\t\t`serviceInstanceUpdate failed, retrying without healthcheck fields: ${error}`,\n\t\t);\n\n\t\tdelete instanceInput.healthcheckPath;\n\t\tdelete instanceInput.healthcheckTimeout;\n\n\t\tif (Object.keys(instanceInput).length > 0) {\n\t\t\tawait client.query(SERVICE_INSTANCE_UPDATE, {\n\t\t\t\tserviceId,\n\t\t\t\tenvironmentId,\n\t\t\t\tinput: instanceInput,\n\t\t\t});\n\t\t}\n\t}\n}\n\n/**\n * Dynamic provider implementing CRUD for Railway services.\n *\n * Uses adopt-or-create on `create()`: queries services by project ID and name\n * before creating a new one. Service instance configuration (builder, commands,\n * healthcheck) is applied via `serviceInstanceUpdate` after create or update.\n *\n * @internal Exported only for unit testing; not part of the public API surface.\n */\nexport class RailwayServiceResourceProvider\n\timplements pulumi.dynamic.ResourceProvider\n{\n\t/**\n\t * Creates or adopts a Railway service by name, then applies instance config.\n\t */\n\tasync create(\n\t\tinputs: RailwayServiceInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new RailwayClient(inputs.token);\n\n\t\tconst result = await client.query<{\n\t\t\tproject: {\n\t\t\t\tservices: { edges: Array<{ node: { id: string; name: string } }> };\n\t\t\t};\n\t\t}>(SERVICES_QUERY, { projectId: inputs.projectId });\n\n\t\tlet serviceId = result.project.services.edges.find(\n\t\t\t(edge) => edge.node.name === inputs.name,\n\t\t)?.node.id;\n\n\t\tif (serviceId) {\n\t\t\tpulumi.log.info(\n\t\t\t\t`Adopted existing Railway service \"${inputs.name}\" (${serviceId})`,\n\t\t\t);\n\t\t} else {\n\t\t\tconst createInput: Record<string, unknown> = {\n\t\t\t\tprojectId: inputs.projectId,\n\t\t\t\tenvironmentId: inputs.environmentId,\n\t\t\t\tname: inputs.name,\n\t\t\t};\n\n\t\t\tif (inputs.source) {\n\t\t\t\tcreateInput.source = { image: inputs.source.image };\n\t\t\t}\n\n\t\t\tconst created = await client.query<{\n\t\t\t\tserviceCreate: { id: string; name: string };\n\t\t\t}>(SERVICE_CREATE, { input: createInput });\n\n\t\t\tserviceId = created.serviceCreate.id;\n\n\t\t\tpulumi.log.info(\n\t\t\t\t`Created Railway service \"${inputs.name}\" (${serviceId})`,\n\t\t\t);\n\n\t\t\tif (inputs.source) {\n\t\t\t\tawait client.query(SERVICE_CONNECT, {\n\t\t\t\t\tid: serviceId,\n\t\t\t\t\tinput: { image: inputs.source.image },\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (inputs.icon) {\n\t\t\t\tawait client.query(SERVICE_UPDATE, {\n\t\t\t\t\tid: serviceId,\n\t\t\t\t\tinput: { icon: inputs.icon },\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tawait applyInstanceConfig(client, serviceId, inputs.environmentId, inputs);\n\n\t\t// Image services have no `railway up` step (see RailwayDeploy for code\n\t\t// services) — the provider owns their deploy.\n\t\tif (inputs.source) {\n\t\t\tawait deployServiceInstance(client, serviceId, inputs.environmentId);\n\t\t}\n\n\t\tconst outs: RailwayServiceOutputs = { ...inputs, serviceId };\n\n\t\treturn { id: serviceId, outs };\n\t}\n\n\t/**\n\t * Updates service name/icon and re-applies instance configuration.\n\t */\n\tasync update(\n\t\tid: string,\n\t\tolds: RailwayServiceOutputs,\n\t\tnews: RailwayServiceInputs,\n\t): Promise<pulumi.dynamic.UpdateResult> {\n\t\tconst client = new RailwayClient(news.token);\n\n\t\tconst updateInput: Record<string, unknown> = {};\n\n\t\tif (olds.name !== news.name) {\n\t\t\tupdateInput.name = news.name;\n\t\t}\n\n\t\tif (news.icon && olds.icon !== news.icon) {\n\t\t\tupdateInput.icon = news.icon;\n\t\t}\n\n\t\tif (Object.keys(updateInput).length > 0) {\n\t\t\tawait client.query(SERVICE_UPDATE, { id, input: updateInput });\n\t\t}\n\n\t\tawait applyInstanceConfig(client, id, news.environmentId, news);\n\n\t\t// Instance config changes (source, startCommand, …) only take effect on\n\t\t// the next deployment; image services get none unless the provider\n\t\t// triggers it.\n\t\tif (news.source) {\n\t\t\tawait deployServiceInstance(client, id, news.environmentId);\n\t\t}\n\n\t\tconst outs: RailwayServiceOutputs = { ...news, serviceId: id };\n\n\t\treturn { outs };\n\t}\n\n\t/**\n\t * Reads current state for `pulumi refresh` by querying the service by ID.\n\t */\n\tasync read(\n\t\tid: string,\n\t\tprops: RailwayServiceOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\ttry {\n\t\t\tawait client.query<{ service: { id: string; name: string } }>(\n\t\t\t\tSERVICE_QUERY,\n\t\t\t\t{ serviceId: id },\n\t\t\t);\n\t\t} catch {\n\t\t\tthrow new Error(`Railway service \"${props.name}\" (${id}) not found`);\n\t\t}\n\n\t\treturn { id, props: { ...props, serviceId: id } };\n\t}\n\n\t/**\n\t * Deletion is a no-op. A Railway service is a project-level resource shared\n\t * across environments (forked environments adopt it by name), so a single\n\t * environment's destroy must never delete it. Deleting the *environment*\n\t * removes that environment's service instances instead.\n\t */\n\tasync delete(): Promise<void> {\n\t\tpulumi.log.warn(\n\t\t\t\"Railway service deletion skipped — services are project-level; delete the environment to remove its instances\",\n\t\t);\n\t}\n\n\t/**\n\t * Compares old and new inputs to determine what changed.\n\t *\n\t * ProjectId and environmentId changes trigger replacement.\n\t */\n\tasync diff(\n\t\t_id: string,\n\t\tolds: RailwayServiceOutputs,\n\t\tnews: RailwayServiceInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\t\tconst changes: string[] = [];\n\n\t\tif (olds.name !== news.name) {\n\t\t\tchanges.push(\"name\");\n\t\t}\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\treplaces.push(\"projectId\");\n\t\t}\n\n\t\tif (olds.environmentId !== news.environmentId) {\n\t\t\treplaces.push(\"environmentId\");\n\t\t}\n\n\t\tif (olds.builder !== news.builder) {\n\t\t\tchanges.push(\"builder\");\n\t\t}\n\n\t\tif (olds.buildCommand !== news.buildCommand) {\n\t\t\tchanges.push(\"buildCommand\");\n\t\t}\n\n\t\tif (olds.startCommand !== news.startCommand) {\n\t\t\tchanges.push(\"startCommand\");\n\t\t}\n\n\t\tif (olds.restartPolicyType !== news.restartPolicyType) {\n\t\t\tchanges.push(\"restartPolicyType\");\n\t\t}\n\n\t\tif (olds.healthcheckPath !== news.healthcheckPath) {\n\t\t\tchanges.push(\"healthcheckPath\");\n\t\t}\n\n\t\tif (olds.healthcheckTimeout !== news.healthcheckTimeout) {\n\t\t\tchanges.push(\"healthcheckTimeout\");\n\t\t}\n\n\t\tif (olds.preDeployCommand !== news.preDeployCommand) {\n\t\t\tchanges.push(\"preDeployCommand\");\n\t\t}\n\n\t\tif (olds.icon !== news.icon) {\n\t\t\tchanges.push(\"icon\");\n\t\t}\n\n\t\treturn {\n\t\t\tchanges: replaces.length > 0 || changes.length > 0,\n\t\t\treplaces,\n\t\t\tdeleteBeforeReplace: true,\n\t\t};\n\t}\n}\n\n/** Internal dynamic resource — not part of the public API. */\nclass RailwayServiceResource extends pulumi.dynamic.Resource {\n\tpublic declare readonly serviceId: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\ttoken: pulumi.Input<string>;\n\t\t\tprojectId: pulumi.Input<string>;\n\t\t\tenvironmentId: pulumi.Input<string>;\n\t\t\tname: pulumi.Input<string>;\n\t\t\ticon?: pulumi.Input<string>;\n\t\t\tsource?: pulumi.Input<{ image: pulumi.Input<string> }>;\n\t\t\tbuilder?: pulumi.Input<RailwayBuilder>;\n\t\t\tbuildCommand?: pulumi.Input<string>;\n\t\t\tstartCommand?: pulumi.Input<string>;\n\t\t\trestartPolicyType?: pulumi.Input<RailwayRestartPolicy>;\n\t\t\thealthcheckPath?: pulumi.Input<string>;\n\t\t\thealthcheckTimeout?: pulumi.Input<number>;\n\t\t\tpreDeployCommand?: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew RailwayServiceResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, serviceId: undefined },\n\t\t\topts,\n\t\t);\n\t}\n}\n\n/** Options type for RailwayService — replaces Pulumi's native `provider` field. */\ntype RailwayServiceOptions = Omit<\n\tpulumi.ComponentResourceOptions,\n\t\"provider\"\n> & {\n\t/** Railway authentication context. */\n\tprovider: RailwayProvider;\n\n\t/** Railway project context. */\n\tproject: RailwayProject;\n\n\t/** Railway environment context. */\n\tenvironment: RailwayEnvironment;\n};\n\n/** Args for RailwayService. */\nexport interface RailwayServiceArgs {\n\t/** Human-readable service name used for adopt-or-create matching. */\n\tname: pulumi.Input<string>;\n\n\t/** SVG icon URL displayed in the Railway dashboard. */\n\ticon?: pulumi.Input<string>;\n\n\t/** Docker image source for image-based services. */\n\tsource?: pulumi.Input<{ image: pulumi.Input<string> }>;\n\n\t/** Build system to use when building the service. */\n\tbuilder?: pulumi.Input<RailwayBuilder>;\n\n\t/** Shell command executed during the build phase. */\n\tbuildCommand?: pulumi.Input<string>;\n\n\t/** Shell command executed to start the service at runtime. */\n\tstartCommand?: pulumi.Input<string>;\n\n\t/** Restart behavior for the service container. */\n\trestartPolicyType?: pulumi.Input<RailwayRestartPolicy>;\n\n\t/** HTTP path polled for health checks (e.g. `\"/health-check\"`). */\n\thealthcheckPath?: pulumi.Input<string>;\n\n\t/** Seconds to wait for a healthy response before marking unhealthy. */\n\thealthcheckTimeout?: pulumi.Input<number>;\n\n\t/** Shell command executed before the main deploy (e.g. migrations). */\n\tpreDeployCommand?: pulumi.Input<string>;\n}\n\n/**\n * Manages a Railway service with adopt-or-create semantics.\n *\n * @example\n * ```typescript\n * const service = new RailwayService(\"api\", {\n * name: \"api\",\n * builder: RailwayBuilder.RAILPACK,\n * startCommand: \"node dist/index.js\",\n * healthcheckPath: \"/health\",\n * }, { provider, project, environment });\n *\n * // Use serviceId downstream\n * new RailwayVariable(\"api-vars\", {\n * variables: { DATABASE_URL: dbUrl },\n * }, { provider, project, environment, service });\n * ```\n */\nexport class RailwayService extends pulumi.ComponentResource {\n\t/** Railway service UUID. */\n\tpublic readonly id: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: RailwayServiceArgs,\n\t\topts: RailwayServiceOptions,\n\t) {\n\t\tconst { provider, project, environment, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:railway:Service\", name, {}, pulumiOpts);\n\n\t\tconst resource = new RailwayServiceResource(\n\t\t\t`${name}-resource`,\n\t\t\t{\n\t\t\t\ttoken: provider.token,\n\t\t\t\tprojectId: project.id,\n\t\t\t\tenvironmentId: environment.id,\n\t\t\t\t...args,\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.id = resource.serviceId;\n\n\t\tthis.registerOutputs({ id: this.id });\n\t}\n}\n"],"mappings":"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"}
@@ -13,6 +13,11 @@ const VOLUME_CREATE = `
13
13
  }
14
14
  }
15
15
  `;
16
+ const VOLUME_ATTACH_DEPLOY = `
17
+ mutation($serviceId: String!, $environmentId: String!) {
18
+ serviceInstanceDeployV2(serviceId: $serviceId, environmentId: $environmentId)
19
+ }
20
+ `;
16
21
  const VOLUME_DELETE = `
17
22
  mutation($volumeId: String!) {
18
23
  volumeDelete(volumeId: $volumeId)
@@ -59,12 +64,22 @@ var RailwayVolumeResourceProvider = class {
59
64
  const client = new require_railway_client.RailwayClient(inputs.token);
60
65
  let volumeId = await findVolumeByService(client, inputs.projectId, inputs.serviceId);
61
66
  if (volumeId) _pulumi_pulumi.log.info(`Adopting existing volume for service (${volumeId})`);
62
- else volumeId = (await client.query(VOLUME_CREATE, { input: {
63
- projectId: inputs.projectId,
64
- serviceId: inputs.serviceId,
65
- environmentId: inputs.environmentId,
66
- mountPath: inputs.mountPath
67
- } })).volumeCreate.id;
67
+ else {
68
+ volumeId = (await client.query(VOLUME_CREATE, { input: {
69
+ projectId: inputs.projectId,
70
+ serviceId: inputs.serviceId,
71
+ environmentId: inputs.environmentId,
72
+ mountPath: inputs.mountPath
73
+ } })).volumeCreate.id;
74
+ try {
75
+ await client.query(VOLUME_ATTACH_DEPLOY, {
76
+ serviceId: inputs.serviceId,
77
+ environmentId: inputs.environmentId
78
+ });
79
+ } catch (error) {
80
+ _pulumi_pulumi.log.warn(`Volume attached; redeploy skipped (service not deployable yet?): ${String(error)}`);
81
+ }
82
+ }
68
83
  return {
69
84
  id: volumeId,
70
85
  outs: {
@@ -1 +1 @@
1
- {"version":3,"file":"volume.cjs","names":["RailwayClient","pulumi"],"sources":["../../src/railway/volume.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport { RailwayClient } from \"./client\";\nimport type { RailwayEnvironment } from \"./environment\";\nimport type { RailwayProject } from \"./project\";\nimport type { RailwayProvider } from \"./provider\";\nimport type { RailwayService } from \"./service\";\n\n/** Resolved inputs for the Railway volume dynamic provider. */\nexport interface RailwayVolumeInputs {\n\t/** Railway API bearer token. */\n\ttoken: string;\n\n\t/** Railway project UUID. */\n\tprojectId: string;\n\n\t/** Railway service UUID to attach the volume to. */\n\tserviceId: string;\n\n\t/** Railway environment UUID (e.g. production). */\n\tenvironmentId: string;\n\n\t/** Absolute path inside the container where the volume is mounted (e.g. `\"/data\"`). */\n\tmountPath: string;\n}\n\n/** Persisted state for the Railway volume, extending inputs with the Railway-assigned ID. */\ninterface RailwayVolumeOutputs extends RailwayVolumeInputs {\n\t/** Railway-assigned volume UUID (set after create or adopt). */\n\tvolumeId: string;\n}\n\nconst VOLUME_CREATE = `\n mutation($input: VolumeCreateInput!) {\n volumeCreate(input: $input) {\n id\n name\n }\n }\n`;\n\nconst VOLUME_DELETE = `\n mutation($volumeId: String!) {\n volumeDelete(volumeId: $volumeId)\n }\n`;\n\nconst VOLUMES_QUERY = `\n query($projectId: String!) {\n project(id: $projectId) {\n volumes {\n edges {\n node {\n id\n name\n volumeInstances {\n edges {\n node {\n id\n mountPath\n serviceId\n }\n }\n }\n }\n }\n }\n }\n }\n`;\n\n/**\n * Finds an existing volume attached to a specific service within a project.\n */\nasync function findVolumeByService(\n\tclient: RailwayClient,\n\tprojectId: string,\n\tserviceId: string,\n): Promise<string | undefined> {\n\tconst result = await client.query<{\n\t\tproject: {\n\t\t\tvolumes: {\n\t\t\t\tedges: Array<{\n\t\t\t\t\tnode: {\n\t\t\t\t\t\tid: string;\n\t\t\t\t\t\tvolumeInstances: {\n\t\t\t\t\t\t\tedges: Array<{\n\t\t\t\t\t\t\t\tnode: { serviceId: string };\n\t\t\t\t\t\t\t}>;\n\t\t\t\t\t\t};\n\t\t\t\t\t};\n\t\t\t\t}>;\n\t\t\t};\n\t\t};\n\t}>(VOLUMES_QUERY, { projectId });\n\n\tconst match = result.project.volumes.edges.find((edge) =>\n\t\tedge.node.volumeInstances.edges.some(\n\t\t\t(vi) => vi.node.serviceId === serviceId,\n\t\t),\n\t);\n\n\treturn match?.node.id;\n}\n\n/**\n * Dynamic provider implementing CRUD for Railway persistent volumes.\n *\n * Uses adopt-or-create on `create()`: finds an existing volume by service\n * before creating a new one. Volumes are immutable after creation — changing\n * `serviceId`, `mountPath`, `environmentId`, or `projectId` triggers replacement.\n */\nexport class RailwayVolumeResourceProvider\n\timplements pulumi.dynamic.ResourceProvider\n{\n\tasync create(\n\t\tinputs: RailwayVolumeInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new RailwayClient(inputs.token);\n\n\t\tlet volumeId = await findVolumeByService(\n\t\t\tclient,\n\t\t\tinputs.projectId,\n\t\t\tinputs.serviceId,\n\t\t);\n\n\t\tif (volumeId) {\n\t\t\tpulumi.log.info(`Adopting existing volume for service (${volumeId})`);\n\t\t} else {\n\t\t\tconst result = await client.query<{\n\t\t\t\tvolumeCreate: { id: string };\n\t\t\t}>(VOLUME_CREATE, {\n\t\t\t\tinput: {\n\t\t\t\t\tprojectId: inputs.projectId,\n\t\t\t\t\tserviceId: inputs.serviceId,\n\t\t\t\t\tenvironmentId: inputs.environmentId,\n\t\t\t\t\tmountPath: inputs.mountPath,\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tvolumeId = result.volumeCreate.id;\n\t\t}\n\n\t\treturn {\n\t\t\tid: volumeId,\n\t\t\touts: { ...inputs, volumeId },\n\t\t};\n\t}\n\n\tasync read(\n\t\tid: string,\n\t\tprops: RailwayVolumeOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\tlet volumeId: string | undefined;\n\n\t\ttry {\n\t\t\tvolumeId = await findVolumeByService(\n\t\t\t\tclient,\n\t\t\t\tprops.projectId,\n\t\t\t\tprops.serviceId,\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tpulumi.log.warn(\n\t\t\t\t`Railway volume refresh lookup failed; keeping existing state: ${String(error)}`,\n\t\t\t);\n\t\t}\n\n\t\t// A project-level service's volume can be momentarily unresolvable at\n\t\t// refresh (eventual consistency / environment-scoped volume instances), so\n\t\t// fall back to the stored id rather than throwing — that turned a healthy\n\t\t// volume into a refresh error. A genuinely-deleted volume is re-adopted or\n\t\t// recreated on the next `up` (create is adopt-or-create). `read` never\n\t\t// fabricates drift and never hard-fails.\n\t\tconst resolvedId = volumeId ?? props.volumeId ?? id;\n\n\t\treturn { id: resolvedId, props: { ...props, volumeId: resolvedId } };\n\t}\n\n\tasync delete(id: string, props: RailwayVolumeOutputs): Promise<void> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\ttry {\n\t\t\tawait client.query(VOLUME_DELETE, { volumeId: id });\n\t\t} catch {\n\t\t\tpulumi.log.warn(\n\t\t\t\t\"Failed to delete Railway volume (may already be deleted)\",\n\t\t\t);\n\t\t}\n\t}\n\n\tasync diff(\n\t\t_id: string,\n\t\tolds: RailwayVolumeOutputs,\n\t\tnews: RailwayVolumeInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\n\t\tif (olds.serviceId !== news.serviceId) {\n\t\t\treplaces.push(\"serviceId\");\n\t\t}\n\n\t\tif (olds.mountPath !== news.mountPath) {\n\t\t\treplaces.push(\"mountPath\");\n\t\t}\n\n\t\tif (olds.environmentId !== news.environmentId) {\n\t\t\treplaces.push(\"environmentId\");\n\t\t}\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\treplaces.push(\"projectId\");\n\t\t}\n\n\t\treturn {\n\t\t\tchanges: replaces.length > 0,\n\t\t\treplaces,\n\t\t\tdeleteBeforeReplace: true,\n\t\t};\n\t}\n}\n\n/** Internal dynamic resource — not part of the public API. */\nclass RailwayVolumeResource extends pulumi.dynamic.Resource {\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\ttoken: pulumi.Input<string>;\n\t\t\tprojectId: pulumi.Input<string>;\n\t\t\tserviceId: pulumi.Input<string>;\n\t\t\tenvironmentId: pulumi.Input<string>;\n\t\t\tmountPath: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew RailwayVolumeResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, volumeId: undefined },\n\t\t\topts,\n\t\t);\n\t}\n}\n\n/** Options type for RailwayVolume — replaces Pulumi's native `provider` field. */\ntype RailwayVolumeOptions = Omit<\n\tpulumi.ComponentResourceOptions,\n\t\"provider\"\n> & {\n\t/** Railway authentication context. */\n\tprovider: RailwayProvider;\n\n\t/** Railway project context. */\n\tproject: RailwayProject;\n\n\t/** Railway environment context. */\n\tenvironment: RailwayEnvironment;\n\n\t/** Railway service context. */\n\tservice: RailwayService;\n};\n\n/** Args for RailwayVolume. */\nexport interface RailwayVolumeArgs {\n\t/** Absolute path inside the container where the volume is mounted. */\n\tmountPath: pulumi.Input<string>;\n}\n\n/**\n * Manages a Railway persistent volume with adopt-or-create semantics.\n *\n * @example\n * ```typescript\n * new RailwayVolume(\"api-data\", {\n * mountPath: \"/data\",\n * }, { provider, project, environment, service });\n * ```\n */\nexport class RailwayVolume extends pulumi.ComponentResource {\n\tconstructor(\n\t\tname: string,\n\t\targs: RailwayVolumeArgs,\n\t\topts: RailwayVolumeOptions,\n\t) {\n\t\tconst { provider, project, environment, service, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:railway:Volume\", name, {}, pulumiOpts);\n\n\t\tnew RailwayVolumeResource(\n\t\t\t`${name}-resource`,\n\t\t\t{\n\t\t\t\ttoken: provider.token,\n\t\t\t\tprojectId: project.id,\n\t\t\t\tserviceId: service.id,\n\t\t\t\tenvironmentId: environment.id,\n\t\t\t\tmountPath: args.mountPath,\n\t\t\t},\n\t\t\t// Forward the consumer's resource options to the underlying resource. Pulumi\n\t\t\t// auto-inherits `provider`/`protect` from the parent component, but NOT options\n\t\t\t// like `retainOnDelete` — without this pass-through, setting `retainOnDelete` on a\n\t\t\t// RailwayVolume would silently never reach the actual cloud volume.\n\t\t\tpulumi.mergeOptions(pulumiOpts, { parent: this }),\n\t\t);\n\n\t\tthis.registerOutputs({});\n\t}\n}\n"],"mappings":";;;;;;;AA+BA,MAAM,gBAAgB;;;;;;;;AAStB,MAAM,gBAAgB;;;;;AAMtB,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BtB,eAAe,oBACd,QACA,WACA,WAC8B;CAwB9B,QANc,MAjBO,OAAO,MAezB,eAAe,EAAE,UAAU,CAAC,GAEV,QAAQ,QAAQ,MAAM,MAAM,SAChD,KAAK,KAAK,gBAAgB,MAAM,MAC9B,OAAO,GAAG,KAAK,cAAc,SAC/B,CAGU,GAAG,KAAK;AACpB;;;;;;;;AASA,IAAa,gCAAb,MAEA;CACC,MAAM,OACL,QACuC;EACvC,MAAM,SAAS,IAAIA,qCAAc,OAAO,KAAK;EAE7C,IAAI,WAAW,MAAM,oBACpB,QACA,OAAO,WACP,OAAO,SACR;EAEA,IAAI,UACH,eAAO,IAAI,KAAK,yCAAyC,SAAS,EAAE;OAapE,YAAW,MAXU,OAAO,MAEzB,eAAe,EACjB,OAAO;GACN,WAAW,OAAO;GAClB,WAAW,OAAO;GAClB,eAAe,OAAO;GACtB,WAAW,OAAO;EACnB,EACD,CAAC,GAEiB,aAAa;EAGhC,OAAO;GACN,IAAI;GACJ,MAAM;IAAE,GAAG;IAAQ;GAAS;EAC7B;CACD;CAEA,MAAM,KACL,IACA,OACqC;EACrC,MAAM,SAAS,IAAIA,qCAAc,MAAM,KAAK;EAE5C,IAAI;EAEJ,IAAI;GACH,WAAW,MAAM,oBAChB,QACA,MAAM,WACN,MAAM,SACP;EACD,SAAS,OAAO;GACf,eAAO,IAAI,KACV,iEAAiE,OAAO,KAAK,GAC9E;EACD;EAQA,MAAM,aAAa,YAAY,MAAM,YAAY;EAEjD,OAAO;GAAE,IAAI;GAAY,OAAO;IAAE,GAAG;IAAO,UAAU;GAAW;EAAE;CACpE;CAEA,MAAM,OAAO,IAAY,OAA4C;EACpE,MAAM,SAAS,IAAIA,qCAAc,MAAM,KAAK;EAE5C,IAAI;GACH,MAAM,OAAO,MAAM,eAAe,EAAE,UAAU,GAAG,CAAC;EACnD,QAAQ;GACP,eAAO,IAAI,KACV,0DACD;EACD;CACD;CAEA,MAAM,KACL,KACA,MACA,MACqC;EACrC,MAAM,WAAqB,CAAC;EAE5B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,IAAI,KAAK,kBAAkB,KAAK,eAC/B,SAAS,KAAK,eAAe;EAG9B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,OAAO;GACN,SAAS,SAAS,SAAS;GAC3B;GACA,qBAAqB;EACtB;CACD;AACD;;AAGA,IAAM,wBAAN,cAAoCC,eAAO,QAAQ,SAAS;CAC3D,YACC,MACA,MAOA,MACC;EACD,MACC,IAAI,8BAA8B,GAClC,MACA;GAAE,GAAG;GAAM,UAAU;EAAU,GAC/B,IACD;CACD;AACD;;;;;;;;;;;AAoCA,IAAa,gBAAb,cAAmCA,eAAO,kBAAkB;CAC3D,YACC,MACA,MACA,MACC;EACD,MAAM,EAAE,UAAU,SAAS,aAAa,SAAS,GAAG,eAAe;EAEnE,MAAM,6BAA6B,MAAM,CAAC,GAAG,UAAU;EAEvD,IAAI,sBACH,GAAG,KAAK,YACR;GACC,OAAO,SAAS;GAChB,WAAW,QAAQ;GACnB,WAAW,QAAQ;GACnB,eAAe,YAAY;GAC3B,WAAW,KAAK;EACjB,GAKAA,eAAO,aAAa,YAAY,EAAE,QAAQ,KAAK,CAAC,CACjD;EAEA,KAAK,gBAAgB,CAAC,CAAC;CACxB;AACD"}
1
+ {"version":3,"file":"volume.cjs","names":["RailwayClient","pulumi"],"sources":["../../src/railway/volume.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport { RailwayClient } from \"./client\";\nimport type { RailwayEnvironment } from \"./environment\";\nimport type { RailwayProject } from \"./project\";\nimport type { RailwayProvider } from \"./provider\";\nimport type { RailwayService } from \"./service\";\n\n/** Resolved inputs for the Railway volume dynamic provider. */\nexport interface RailwayVolumeInputs {\n\t/** Railway API bearer token. */\n\ttoken: string;\n\n\t/** Railway project UUID. */\n\tprojectId: string;\n\n\t/** Railway service UUID to attach the volume to. */\n\tserviceId: string;\n\n\t/** Railway environment UUID (e.g. production). */\n\tenvironmentId: string;\n\n\t/** Absolute path inside the container where the volume is mounted (e.g. `\"/data\"`). */\n\tmountPath: string;\n}\n\n/** Persisted state for the Railway volume, extending inputs with the Railway-assigned ID. */\ninterface RailwayVolumeOutputs extends RailwayVolumeInputs {\n\t/** Railway-assigned volume UUID (set after create or adopt). */\n\tvolumeId: string;\n}\n\nconst VOLUME_CREATE = `\n mutation($input: VolumeCreateInput!) {\n volumeCreate(input: $input) {\n id\n name\n }\n }\n`;\n\nconst VOLUME_ATTACH_DEPLOY = `\n mutation($serviceId: String!, $environmentId: String!) {\n serviceInstanceDeployV2(serviceId: $serviceId, environmentId: $environmentId)\n }\n`;\n\nconst VOLUME_DELETE = `\n mutation($volumeId: String!) {\n volumeDelete(volumeId: $volumeId)\n }\n`;\n\nconst VOLUMES_QUERY = `\n query($projectId: String!) {\n project(id: $projectId) {\n volumes {\n edges {\n node {\n id\n name\n volumeInstances {\n edges {\n node {\n id\n mountPath\n serviceId\n }\n }\n }\n }\n }\n }\n }\n }\n`;\n\n/**\n * Finds an existing volume attached to a specific service within a project.\n */\nasync function findVolumeByService(\n\tclient: RailwayClient,\n\tprojectId: string,\n\tserviceId: string,\n): Promise<string | undefined> {\n\tconst result = await client.query<{\n\t\tproject: {\n\t\t\tvolumes: {\n\t\t\t\tedges: Array<{\n\t\t\t\t\tnode: {\n\t\t\t\t\t\tid: string;\n\t\t\t\t\t\tvolumeInstances: {\n\t\t\t\t\t\t\tedges: Array<{\n\t\t\t\t\t\t\t\tnode: { serviceId: string };\n\t\t\t\t\t\t\t}>;\n\t\t\t\t\t\t};\n\t\t\t\t\t};\n\t\t\t\t}>;\n\t\t\t};\n\t\t};\n\t}>(VOLUMES_QUERY, { projectId });\n\n\tconst match = result.project.volumes.edges.find((edge) =>\n\t\tedge.node.volumeInstances.edges.some(\n\t\t\t(vi) => vi.node.serviceId === serviceId,\n\t\t),\n\t);\n\n\treturn match?.node.id;\n}\n\n/**\n * Dynamic provider implementing CRUD for Railway persistent volumes.\n *\n * Uses adopt-or-create on `create()`: finds an existing volume by service\n * before creating a new one. Volumes are immutable after creation — changing\n * `serviceId`, `mountPath`, `environmentId`, or `projectId` triggers replacement.\n */\nexport class RailwayVolumeResourceProvider\n\timplements pulumi.dynamic.ResourceProvider\n{\n\tasync create(\n\t\tinputs: RailwayVolumeInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new RailwayClient(inputs.token);\n\n\t\tlet volumeId = await findVolumeByService(\n\t\t\tclient,\n\t\t\tinputs.projectId,\n\t\t\tinputs.serviceId,\n\t\t);\n\n\t\tif (volumeId) {\n\t\t\tpulumi.log.info(`Adopting existing volume for service (${volumeId})`);\n\t\t} else {\n\t\t\tconst result = await client.query<{\n\t\t\t\tvolumeCreate: { id: string };\n\t\t\t}>(VOLUME_CREATE, {\n\t\t\t\tinput: {\n\t\t\t\t\tprojectId: inputs.projectId,\n\t\t\t\t\tserviceId: inputs.serviceId,\n\t\t\t\t\tenvironmentId: inputs.environmentId,\n\t\t\t\t\tmountPath: inputs.mountPath,\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tvolumeId = result.volumeCreate.id;\n\n\t\t\t// A volume mounts into the container only on the NEXT deployment —\n\t\t\t// attaching alone changes nothing for a running service (the dashboard\n\t\t\t// redeploys after attach for the same reason). Best-effort: a service\n\t\t\t// with no deployable source yet (code service before its first\n\t\t\t// `railway up`) has nothing to redeploy, and its mount lands with that\n\t\t\t// first deploy anyway.\n\t\t\ttry {\n\t\t\t\tawait client.query(VOLUME_ATTACH_DEPLOY, {\n\t\t\t\t\tserviceId: inputs.serviceId,\n\t\t\t\t\tenvironmentId: inputs.environmentId,\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\tpulumi.log.warn(\n\t\t\t\t\t`Volume attached; redeploy skipped (service not deployable yet?): ${String(error)}`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\tid: volumeId,\n\t\t\touts: { ...inputs, volumeId },\n\t\t};\n\t}\n\n\tasync read(\n\t\tid: string,\n\t\tprops: RailwayVolumeOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\tlet volumeId: string | undefined;\n\n\t\ttry {\n\t\t\tvolumeId = await findVolumeByService(\n\t\t\t\tclient,\n\t\t\t\tprops.projectId,\n\t\t\t\tprops.serviceId,\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tpulumi.log.warn(\n\t\t\t\t`Railway volume refresh lookup failed; keeping existing state: ${String(error)}`,\n\t\t\t);\n\t\t}\n\n\t\t// A project-level service's volume can be momentarily unresolvable at\n\t\t// refresh (eventual consistency / environment-scoped volume instances), so\n\t\t// fall back to the stored id rather than throwing — that turned a healthy\n\t\t// volume into a refresh error. A genuinely-deleted volume is re-adopted or\n\t\t// recreated on the next `up` (create is adopt-or-create). `read` never\n\t\t// fabricates drift and never hard-fails.\n\t\tconst resolvedId = volumeId ?? props.volumeId ?? id;\n\n\t\treturn { id: resolvedId, props: { ...props, volumeId: resolvedId } };\n\t}\n\n\tasync delete(id: string, props: RailwayVolumeOutputs): Promise<void> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\ttry {\n\t\t\tawait client.query(VOLUME_DELETE, { volumeId: id });\n\t\t} catch {\n\t\t\tpulumi.log.warn(\n\t\t\t\t\"Failed to delete Railway volume (may already be deleted)\",\n\t\t\t);\n\t\t}\n\t}\n\n\tasync diff(\n\t\t_id: string,\n\t\tolds: RailwayVolumeOutputs,\n\t\tnews: RailwayVolumeInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\n\t\tif (olds.serviceId !== news.serviceId) {\n\t\t\treplaces.push(\"serviceId\");\n\t\t}\n\n\t\tif (olds.mountPath !== news.mountPath) {\n\t\t\treplaces.push(\"mountPath\");\n\t\t}\n\n\t\tif (olds.environmentId !== news.environmentId) {\n\t\t\treplaces.push(\"environmentId\");\n\t\t}\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\treplaces.push(\"projectId\");\n\t\t}\n\n\t\treturn {\n\t\t\tchanges: replaces.length > 0,\n\t\t\treplaces,\n\t\t\tdeleteBeforeReplace: true,\n\t\t};\n\t}\n}\n\n/** Internal dynamic resource — not part of the public API. */\nclass RailwayVolumeResource extends pulumi.dynamic.Resource {\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\ttoken: pulumi.Input<string>;\n\t\t\tprojectId: pulumi.Input<string>;\n\t\t\tserviceId: pulumi.Input<string>;\n\t\t\tenvironmentId: pulumi.Input<string>;\n\t\t\tmountPath: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew RailwayVolumeResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, volumeId: undefined },\n\t\t\topts,\n\t\t);\n\t}\n}\n\n/** Options type for RailwayVolume — replaces Pulumi's native `provider` field. */\ntype RailwayVolumeOptions = Omit<\n\tpulumi.ComponentResourceOptions,\n\t\"provider\"\n> & {\n\t/** Railway authentication context. */\n\tprovider: RailwayProvider;\n\n\t/** Railway project context. */\n\tproject: RailwayProject;\n\n\t/** Railway environment context. */\n\tenvironment: RailwayEnvironment;\n\n\t/** Railway service context. */\n\tservice: RailwayService;\n};\n\n/** Args for RailwayVolume. */\nexport interface RailwayVolumeArgs {\n\t/** Absolute path inside the container where the volume is mounted. */\n\tmountPath: pulumi.Input<string>;\n}\n\n/**\n * Manages a Railway persistent volume with adopt-or-create semantics.\n *\n * @example\n * ```typescript\n * new RailwayVolume(\"api-data\", {\n * mountPath: \"/data\",\n * }, { provider, project, environment, service });\n * ```\n */\nexport class RailwayVolume extends pulumi.ComponentResource {\n\tconstructor(\n\t\tname: string,\n\t\targs: RailwayVolumeArgs,\n\t\topts: RailwayVolumeOptions,\n\t) {\n\t\tconst { provider, project, environment, service, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:railway:Volume\", name, {}, pulumiOpts);\n\n\t\tnew RailwayVolumeResource(\n\t\t\t`${name}-resource`,\n\t\t\t{\n\t\t\t\ttoken: provider.token,\n\t\t\t\tprojectId: project.id,\n\t\t\t\tserviceId: service.id,\n\t\t\t\tenvironmentId: environment.id,\n\t\t\t\tmountPath: args.mountPath,\n\t\t\t},\n\t\t\t// Forward the consumer's resource options to the underlying resource. Pulumi\n\t\t\t// auto-inherits `provider`/`protect` from the parent component, but NOT options\n\t\t\t// like `retainOnDelete` — without this pass-through, setting `retainOnDelete` on a\n\t\t\t// RailwayVolume would silently never reach the actual cloud volume.\n\t\t\tpulumi.mergeOptions(pulumiOpts, { parent: this }),\n\t\t);\n\n\t\tthis.registerOutputs({});\n\t}\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"volume.d.cts","names":[],"sources":["../../src/railway/volume.ts"],"mappings":";;;;;;;;;UAQiB,mBAAA;;EAEhB,KAAA;EAFmC;EAKnC,SAAA;EALmC;EAQnC,SAAA;EAHA;EAMA,aAAA;EAAA;EAGA,SAAA;AAAA;AAAS;AAAA,UAIA,oBAAA,SAA6B,mBAAmB;EAA3B;EAE9B,QAAQ;AAAA;AAAA;AAmFT;;;;;;AAnFS,cAmFI,6BAAA,YACD,MAAA,CAAO,OAAA,CAAQ,gBAAA;EAEpB,MAAA,CACL,MAAA,EAAQ,mBAAA,GACN,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EAgCpB,IAAA,CACL,EAAA,UACA,KAAA,EAAO,oBAAA,GACL,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;EA4BpB,MAAA,CAAO,EAAA,UAAY,KAAA,EAAO,oBAAA,GAAuB,OAAA;EAYjD,IAAA,CACL,GAAA,UACA,IAAA,EAAM,oBAAA,EACN,IAAA,EAAM,mBAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;AAAA;;KAkDtB,oBAAA,GAAuB,IAAA,CAC3B,MAAA,CAAO,wBAAA;EAnDJ,sCAuDH,QAAA,EAAU,eAAA,EA1IgC;EA6I1C,OAAA,EAAS,cAAA,EA7IE;EAgJX,WAAA,EAAa,kBAAA,EAhJa;EAmJ1B,OAAA,EAAS,cAAA;AAAA;;UAIO,iBAAA;EAnJL;EAqJX,SAAA,EAAW,MAAA,CAAO,KAAK;AAAA;;;;;;;;;;;cAaX,aAAA,SAAsB,MAAA,CAAO,iBAAA;cAExC,IAAA,UACA,IAAA,EAAM,iBAAA,EACN,IAAA,EAAM,oBAAA;AAAA"}
1
+ {"version":3,"file":"volume.d.cts","names":[],"sources":["../../src/railway/volume.ts"],"mappings":";;;;;;;;;UAQiB,mBAAA;;EAEhB,KAAA;EAFmC;EAKnC,SAAA;EALmC;EAQnC,SAAA;EAHA;EAMA,aAAA;EAAA;EAGA,SAAA;AAAA;AAAS;AAAA,UAIA,oBAAA,SAA6B,mBAAmB;EAA3B;EAE9B,QAAQ;AAAA;AAAA;AAyFT;;;;;;AAzFS,cAyFI,6BAAA,YACD,MAAA,CAAO,OAAA,CAAQ,gBAAA;EAEpB,MAAA,CACL,MAAA,EAAQ,mBAAA,GACN,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EAiDpB,IAAA,CACL,EAAA,UACA,KAAA,EAAO,oBAAA,GACL,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;EA4BpB,MAAA,CAAO,EAAA,UAAY,KAAA,EAAO,oBAAA,GAAuB,OAAA;EAYjD,IAAA,CACL,GAAA,UACA,IAAA,EAAM,oBAAA,EACN,IAAA,EAAM,mBAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;AAAA;;KAkDtB,oBAAA,GAAuB,IAAA,CAC3B,MAAA,CAAO,wBAAA;EAnDJ,sCAuDH,QAAA,EAAU,eAAA,EA3JgC;EA8J1C,OAAA,EAAS,cAAA,EA9JE;EAiKX,WAAA,EAAa,kBAAA,EAjKa;EAoK1B,OAAA,EAAS,cAAA;AAAA;;UAIO,iBAAA;EApKL;EAsKX,SAAA,EAAW,MAAA,CAAO,KAAK;AAAA;;;;;;;;;;;cAaX,aAAA,SAAsB,MAAA,CAAO,iBAAA;cAExC,IAAA,UACA,IAAA,EAAM,iBAAA,EACN,IAAA,EAAM,oBAAA;AAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"volume.d.mts","names":[],"sources":["../../src/railway/volume.ts"],"mappings":";;;;;;;;;UAQiB,mBAAA;;EAEhB,KAAA;EAFmC;EAKnC,SAAA;EALmC;EAQnC,SAAA;EAHA;EAMA,aAAA;EAAA;EAGA,SAAA;AAAA;AAAS;AAAA,UAIA,oBAAA,SAA6B,mBAAmB;EAA3B;EAE9B,QAAQ;AAAA;AAAA;AAmFT;;;;;;AAnFS,cAmFI,6BAAA,YACD,MAAA,CAAO,OAAA,CAAQ,gBAAA;EAEpB,MAAA,CACL,MAAA,EAAQ,mBAAA,GACN,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EAgCpB,IAAA,CACL,EAAA,UACA,KAAA,EAAO,oBAAA,GACL,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;EA4BpB,MAAA,CAAO,EAAA,UAAY,KAAA,EAAO,oBAAA,GAAuB,OAAA;EAYjD,IAAA,CACL,GAAA,UACA,IAAA,EAAM,oBAAA,EACN,IAAA,EAAM,mBAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;AAAA;;KAkDtB,oBAAA,GAAuB,IAAA,CAC3B,MAAA,CAAO,wBAAA;EAnDJ,sCAuDH,QAAA,EAAU,eAAA,EA1IgC;EA6I1C,OAAA,EAAS,cAAA,EA7IE;EAgJX,WAAA,EAAa,kBAAA,EAhJa;EAmJ1B,OAAA,EAAS,cAAA;AAAA;;UAIO,iBAAA;EAnJL;EAqJX,SAAA,EAAW,MAAA,CAAO,KAAK;AAAA;;;;;;;;;;;cAaX,aAAA,SAAsB,MAAA,CAAO,iBAAA;cAExC,IAAA,UACA,IAAA,EAAM,iBAAA,EACN,IAAA,EAAM,oBAAA;AAAA"}
1
+ {"version":3,"file":"volume.d.mts","names":[],"sources":["../../src/railway/volume.ts"],"mappings":";;;;;;;;;UAQiB,mBAAA;;EAEhB,KAAA;EAFmC;EAKnC,SAAA;EALmC;EAQnC,SAAA;EAHA;EAMA,aAAA;EAAA;EAGA,SAAA;AAAA;AAAS;AAAA,UAIA,oBAAA,SAA6B,mBAAmB;EAA3B;EAE9B,QAAQ;AAAA;AAAA;AAyFT;;;;;;AAzFS,cAyFI,6BAAA,YACD,MAAA,CAAO,OAAA,CAAQ,gBAAA;EAEpB,MAAA,CACL,MAAA,EAAQ,mBAAA,GACN,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EAiDpB,IAAA,CACL,EAAA,UACA,KAAA,EAAO,oBAAA,GACL,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;EA4BpB,MAAA,CAAO,EAAA,UAAY,KAAA,EAAO,oBAAA,GAAuB,OAAA;EAYjD,IAAA,CACL,GAAA,UACA,IAAA,EAAM,oBAAA,EACN,IAAA,EAAM,mBAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;AAAA;;KAkDtB,oBAAA,GAAuB,IAAA,CAC3B,MAAA,CAAO,wBAAA;EAnDJ,sCAuDH,QAAA,EAAU,eAAA,EA3JgC;EA8J1C,OAAA,EAAS,cAAA,EA9JE;EAiKX,WAAA,EAAa,kBAAA,EAjKa;EAoK1B,OAAA,EAAS,cAAA;AAAA;;UAIO,iBAAA;EApKL;EAsKX,SAAA,EAAW,MAAA,CAAO,KAAK;AAAA;;;;;;;;;;;cAaX,aAAA,SAAsB,MAAA,CAAO,iBAAA;cAExC,IAAA,UACA,IAAA,EAAM,iBAAA,EACN,IAAA,EAAM,oBAAA;AAAA"}
@@ -11,6 +11,11 @@ const VOLUME_CREATE = `
11
11
  }
12
12
  }
13
13
  `;
14
+ const VOLUME_ATTACH_DEPLOY = `
15
+ mutation($serviceId: String!, $environmentId: String!) {
16
+ serviceInstanceDeployV2(serviceId: $serviceId, environmentId: $environmentId)
17
+ }
18
+ `;
14
19
  const VOLUME_DELETE = `
15
20
  mutation($volumeId: String!) {
16
21
  volumeDelete(volumeId: $volumeId)
@@ -57,12 +62,22 @@ var RailwayVolumeResourceProvider = class {
57
62
  const client = new RailwayClient(inputs.token);
58
63
  let volumeId = await findVolumeByService(client, inputs.projectId, inputs.serviceId);
59
64
  if (volumeId) pulumi.log.info(`Adopting existing volume for service (${volumeId})`);
60
- else volumeId = (await client.query(VOLUME_CREATE, { input: {
61
- projectId: inputs.projectId,
62
- serviceId: inputs.serviceId,
63
- environmentId: inputs.environmentId,
64
- mountPath: inputs.mountPath
65
- } })).volumeCreate.id;
65
+ else {
66
+ volumeId = (await client.query(VOLUME_CREATE, { input: {
67
+ projectId: inputs.projectId,
68
+ serviceId: inputs.serviceId,
69
+ environmentId: inputs.environmentId,
70
+ mountPath: inputs.mountPath
71
+ } })).volumeCreate.id;
72
+ try {
73
+ await client.query(VOLUME_ATTACH_DEPLOY, {
74
+ serviceId: inputs.serviceId,
75
+ environmentId: inputs.environmentId
76
+ });
77
+ } catch (error) {
78
+ pulumi.log.warn(`Volume attached; redeploy skipped (service not deployable yet?): ${String(error)}`);
79
+ }
80
+ }
66
81
  return {
67
82
  id: volumeId,
68
83
  outs: {
@@ -1 +1 @@
1
- {"version":3,"file":"volume.mjs","names":[],"sources":["../../src/railway/volume.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport { RailwayClient } from \"./client\";\nimport type { RailwayEnvironment } from \"./environment\";\nimport type { RailwayProject } from \"./project\";\nimport type { RailwayProvider } from \"./provider\";\nimport type { RailwayService } from \"./service\";\n\n/** Resolved inputs for the Railway volume dynamic provider. */\nexport interface RailwayVolumeInputs {\n\t/** Railway API bearer token. */\n\ttoken: string;\n\n\t/** Railway project UUID. */\n\tprojectId: string;\n\n\t/** Railway service UUID to attach the volume to. */\n\tserviceId: string;\n\n\t/** Railway environment UUID (e.g. production). */\n\tenvironmentId: string;\n\n\t/** Absolute path inside the container where the volume is mounted (e.g. `\"/data\"`). */\n\tmountPath: string;\n}\n\n/** Persisted state for the Railway volume, extending inputs with the Railway-assigned ID. */\ninterface RailwayVolumeOutputs extends RailwayVolumeInputs {\n\t/** Railway-assigned volume UUID (set after create or adopt). */\n\tvolumeId: string;\n}\n\nconst VOLUME_CREATE = `\n mutation($input: VolumeCreateInput!) {\n volumeCreate(input: $input) {\n id\n name\n }\n }\n`;\n\nconst VOLUME_DELETE = `\n mutation($volumeId: String!) {\n volumeDelete(volumeId: $volumeId)\n }\n`;\n\nconst VOLUMES_QUERY = `\n query($projectId: String!) {\n project(id: $projectId) {\n volumes {\n edges {\n node {\n id\n name\n volumeInstances {\n edges {\n node {\n id\n mountPath\n serviceId\n }\n }\n }\n }\n }\n }\n }\n }\n`;\n\n/**\n * Finds an existing volume attached to a specific service within a project.\n */\nasync function findVolumeByService(\n\tclient: RailwayClient,\n\tprojectId: string,\n\tserviceId: string,\n): Promise<string | undefined> {\n\tconst result = await client.query<{\n\t\tproject: {\n\t\t\tvolumes: {\n\t\t\t\tedges: Array<{\n\t\t\t\t\tnode: {\n\t\t\t\t\t\tid: string;\n\t\t\t\t\t\tvolumeInstances: {\n\t\t\t\t\t\t\tedges: Array<{\n\t\t\t\t\t\t\t\tnode: { serviceId: string };\n\t\t\t\t\t\t\t}>;\n\t\t\t\t\t\t};\n\t\t\t\t\t};\n\t\t\t\t}>;\n\t\t\t};\n\t\t};\n\t}>(VOLUMES_QUERY, { projectId });\n\n\tconst match = result.project.volumes.edges.find((edge) =>\n\t\tedge.node.volumeInstances.edges.some(\n\t\t\t(vi) => vi.node.serviceId === serviceId,\n\t\t),\n\t);\n\n\treturn match?.node.id;\n}\n\n/**\n * Dynamic provider implementing CRUD for Railway persistent volumes.\n *\n * Uses adopt-or-create on `create()`: finds an existing volume by service\n * before creating a new one. Volumes are immutable after creation — changing\n * `serviceId`, `mountPath`, `environmentId`, or `projectId` triggers replacement.\n */\nexport class RailwayVolumeResourceProvider\n\timplements pulumi.dynamic.ResourceProvider\n{\n\tasync create(\n\t\tinputs: RailwayVolumeInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new RailwayClient(inputs.token);\n\n\t\tlet volumeId = await findVolumeByService(\n\t\t\tclient,\n\t\t\tinputs.projectId,\n\t\t\tinputs.serviceId,\n\t\t);\n\n\t\tif (volumeId) {\n\t\t\tpulumi.log.info(`Adopting existing volume for service (${volumeId})`);\n\t\t} else {\n\t\t\tconst result = await client.query<{\n\t\t\t\tvolumeCreate: { id: string };\n\t\t\t}>(VOLUME_CREATE, {\n\t\t\t\tinput: {\n\t\t\t\t\tprojectId: inputs.projectId,\n\t\t\t\t\tserviceId: inputs.serviceId,\n\t\t\t\t\tenvironmentId: inputs.environmentId,\n\t\t\t\t\tmountPath: inputs.mountPath,\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tvolumeId = result.volumeCreate.id;\n\t\t}\n\n\t\treturn {\n\t\t\tid: volumeId,\n\t\t\touts: { ...inputs, volumeId },\n\t\t};\n\t}\n\n\tasync read(\n\t\tid: string,\n\t\tprops: RailwayVolumeOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\tlet volumeId: string | undefined;\n\n\t\ttry {\n\t\t\tvolumeId = await findVolumeByService(\n\t\t\t\tclient,\n\t\t\t\tprops.projectId,\n\t\t\t\tprops.serviceId,\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tpulumi.log.warn(\n\t\t\t\t`Railway volume refresh lookup failed; keeping existing state: ${String(error)}`,\n\t\t\t);\n\t\t}\n\n\t\t// A project-level service's volume can be momentarily unresolvable at\n\t\t// refresh (eventual consistency / environment-scoped volume instances), so\n\t\t// fall back to the stored id rather than throwing — that turned a healthy\n\t\t// volume into a refresh error. A genuinely-deleted volume is re-adopted or\n\t\t// recreated on the next `up` (create is adopt-or-create). `read` never\n\t\t// fabricates drift and never hard-fails.\n\t\tconst resolvedId = volumeId ?? props.volumeId ?? id;\n\n\t\treturn { id: resolvedId, props: { ...props, volumeId: resolvedId } };\n\t}\n\n\tasync delete(id: string, props: RailwayVolumeOutputs): Promise<void> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\ttry {\n\t\t\tawait client.query(VOLUME_DELETE, { volumeId: id });\n\t\t} catch {\n\t\t\tpulumi.log.warn(\n\t\t\t\t\"Failed to delete Railway volume (may already be deleted)\",\n\t\t\t);\n\t\t}\n\t}\n\n\tasync diff(\n\t\t_id: string,\n\t\tolds: RailwayVolumeOutputs,\n\t\tnews: RailwayVolumeInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\n\t\tif (olds.serviceId !== news.serviceId) {\n\t\t\treplaces.push(\"serviceId\");\n\t\t}\n\n\t\tif (olds.mountPath !== news.mountPath) {\n\t\t\treplaces.push(\"mountPath\");\n\t\t}\n\n\t\tif (olds.environmentId !== news.environmentId) {\n\t\t\treplaces.push(\"environmentId\");\n\t\t}\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\treplaces.push(\"projectId\");\n\t\t}\n\n\t\treturn {\n\t\t\tchanges: replaces.length > 0,\n\t\t\treplaces,\n\t\t\tdeleteBeforeReplace: true,\n\t\t};\n\t}\n}\n\n/** Internal dynamic resource — not part of the public API. */\nclass RailwayVolumeResource extends pulumi.dynamic.Resource {\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\ttoken: pulumi.Input<string>;\n\t\t\tprojectId: pulumi.Input<string>;\n\t\t\tserviceId: pulumi.Input<string>;\n\t\t\tenvironmentId: pulumi.Input<string>;\n\t\t\tmountPath: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew RailwayVolumeResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, volumeId: undefined },\n\t\t\topts,\n\t\t);\n\t}\n}\n\n/** Options type for RailwayVolume — replaces Pulumi's native `provider` field. */\ntype RailwayVolumeOptions = Omit<\n\tpulumi.ComponentResourceOptions,\n\t\"provider\"\n> & {\n\t/** Railway authentication context. */\n\tprovider: RailwayProvider;\n\n\t/** Railway project context. */\n\tproject: RailwayProject;\n\n\t/** Railway environment context. */\n\tenvironment: RailwayEnvironment;\n\n\t/** Railway service context. */\n\tservice: RailwayService;\n};\n\n/** Args for RailwayVolume. */\nexport interface RailwayVolumeArgs {\n\t/** Absolute path inside the container where the volume is mounted. */\n\tmountPath: pulumi.Input<string>;\n}\n\n/**\n * Manages a Railway persistent volume with adopt-or-create semantics.\n *\n * @example\n * ```typescript\n * new RailwayVolume(\"api-data\", {\n * mountPath: \"/data\",\n * }, { provider, project, environment, service });\n * ```\n */\nexport class RailwayVolume extends pulumi.ComponentResource {\n\tconstructor(\n\t\tname: string,\n\t\targs: RailwayVolumeArgs,\n\t\topts: RailwayVolumeOptions,\n\t) {\n\t\tconst { provider, project, environment, service, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:railway:Volume\", name, {}, pulumiOpts);\n\n\t\tnew RailwayVolumeResource(\n\t\t\t`${name}-resource`,\n\t\t\t{\n\t\t\t\ttoken: provider.token,\n\t\t\t\tprojectId: project.id,\n\t\t\t\tserviceId: service.id,\n\t\t\t\tenvironmentId: environment.id,\n\t\t\t\tmountPath: args.mountPath,\n\t\t\t},\n\t\t\t// Forward the consumer's resource options to the underlying resource. Pulumi\n\t\t\t// auto-inherits `provider`/`protect` from the parent component, but NOT options\n\t\t\t// like `retainOnDelete` — without this pass-through, setting `retainOnDelete` on a\n\t\t\t// RailwayVolume would silently never reach the actual cloud volume.\n\t\t\tpulumi.mergeOptions(pulumiOpts, { parent: this }),\n\t\t);\n\n\t\tthis.registerOutputs({});\n\t}\n}\n"],"mappings":";;;;;AA+BA,MAAM,gBAAgB;;;;;;;;AAStB,MAAM,gBAAgB;;;;;AAMtB,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BtB,eAAe,oBACd,QACA,WACA,WAC8B;CAwB9B,QANc,MAjBO,OAAO,MAezB,eAAe,EAAE,UAAU,CAAC,GAEV,QAAQ,QAAQ,MAAM,MAAM,SAChD,KAAK,KAAK,gBAAgB,MAAM,MAC9B,OAAO,GAAG,KAAK,cAAc,SAC/B,CAGU,GAAG,KAAK;AACpB;;;;;;;;AASA,IAAa,gCAAb,MAEA;CACC,MAAM,OACL,QACuC;EACvC,MAAM,SAAS,IAAI,cAAc,OAAO,KAAK;EAE7C,IAAI,WAAW,MAAM,oBACpB,QACA,OAAO,WACP,OAAO,SACR;EAEA,IAAI,UACH,OAAO,IAAI,KAAK,yCAAyC,SAAS,EAAE;OAapE,YAAW,MAXU,OAAO,MAEzB,eAAe,EACjB,OAAO;GACN,WAAW,OAAO;GAClB,WAAW,OAAO;GAClB,eAAe,OAAO;GACtB,WAAW,OAAO;EACnB,EACD,CAAC,GAEiB,aAAa;EAGhC,OAAO;GACN,IAAI;GACJ,MAAM;IAAE,GAAG;IAAQ;GAAS;EAC7B;CACD;CAEA,MAAM,KACL,IACA,OACqC;EACrC,MAAM,SAAS,IAAI,cAAc,MAAM,KAAK;EAE5C,IAAI;EAEJ,IAAI;GACH,WAAW,MAAM,oBAChB,QACA,MAAM,WACN,MAAM,SACP;EACD,SAAS,OAAO;GACf,OAAO,IAAI,KACV,iEAAiE,OAAO,KAAK,GAC9E;EACD;EAQA,MAAM,aAAa,YAAY,MAAM,YAAY;EAEjD,OAAO;GAAE,IAAI;GAAY,OAAO;IAAE,GAAG;IAAO,UAAU;GAAW;EAAE;CACpE;CAEA,MAAM,OAAO,IAAY,OAA4C;EACpE,MAAM,SAAS,IAAI,cAAc,MAAM,KAAK;EAE5C,IAAI;GACH,MAAM,OAAO,MAAM,eAAe,EAAE,UAAU,GAAG,CAAC;EACnD,QAAQ;GACP,OAAO,IAAI,KACV,0DACD;EACD;CACD;CAEA,MAAM,KACL,KACA,MACA,MACqC;EACrC,MAAM,WAAqB,CAAC;EAE5B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,IAAI,KAAK,kBAAkB,KAAK,eAC/B,SAAS,KAAK,eAAe;EAG9B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,OAAO;GACN,SAAS,SAAS,SAAS;GAC3B;GACA,qBAAqB;EACtB;CACD;AACD;;AAGA,IAAM,wBAAN,cAAoC,OAAO,QAAQ,SAAS;CAC3D,YACC,MACA,MAOA,MACC;EACD,MACC,IAAI,8BAA8B,GAClC,MACA;GAAE,GAAG;GAAM,UAAU;EAAU,GAC/B,IACD;CACD;AACD;;;;;;;;;;;AAoCA,IAAa,gBAAb,cAAmC,OAAO,kBAAkB;CAC3D,YACC,MACA,MACA,MACC;EACD,MAAM,EAAE,UAAU,SAAS,aAAa,SAAS,GAAG,eAAe;EAEnE,MAAM,6BAA6B,MAAM,CAAC,GAAG,UAAU;EAEvD,IAAI,sBACH,GAAG,KAAK,YACR;GACC,OAAO,SAAS;GAChB,WAAW,QAAQ;GACnB,WAAW,QAAQ;GACnB,eAAe,YAAY;GAC3B,WAAW,KAAK;EACjB,GAKA,OAAO,aAAa,YAAY,EAAE,QAAQ,KAAK,CAAC,CACjD;EAEA,KAAK,gBAAgB,CAAC,CAAC;CACxB;AACD"}
1
+ {"version":3,"file":"volume.mjs","names":[],"sources":["../../src/railway/volume.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport { RailwayClient } from \"./client\";\nimport type { RailwayEnvironment } from \"./environment\";\nimport type { RailwayProject } from \"./project\";\nimport type { RailwayProvider } from \"./provider\";\nimport type { RailwayService } from \"./service\";\n\n/** Resolved inputs for the Railway volume dynamic provider. */\nexport interface RailwayVolumeInputs {\n\t/** Railway API bearer token. */\n\ttoken: string;\n\n\t/** Railway project UUID. */\n\tprojectId: string;\n\n\t/** Railway service UUID to attach the volume to. */\n\tserviceId: string;\n\n\t/** Railway environment UUID (e.g. production). */\n\tenvironmentId: string;\n\n\t/** Absolute path inside the container where the volume is mounted (e.g. `\"/data\"`). */\n\tmountPath: string;\n}\n\n/** Persisted state for the Railway volume, extending inputs with the Railway-assigned ID. */\ninterface RailwayVolumeOutputs extends RailwayVolumeInputs {\n\t/** Railway-assigned volume UUID (set after create or adopt). */\n\tvolumeId: string;\n}\n\nconst VOLUME_CREATE = `\n mutation($input: VolumeCreateInput!) {\n volumeCreate(input: $input) {\n id\n name\n }\n }\n`;\n\nconst VOLUME_ATTACH_DEPLOY = `\n mutation($serviceId: String!, $environmentId: String!) {\n serviceInstanceDeployV2(serviceId: $serviceId, environmentId: $environmentId)\n }\n`;\n\nconst VOLUME_DELETE = `\n mutation($volumeId: String!) {\n volumeDelete(volumeId: $volumeId)\n }\n`;\n\nconst VOLUMES_QUERY = `\n query($projectId: String!) {\n project(id: $projectId) {\n volumes {\n edges {\n node {\n id\n name\n volumeInstances {\n edges {\n node {\n id\n mountPath\n serviceId\n }\n }\n }\n }\n }\n }\n }\n }\n`;\n\n/**\n * Finds an existing volume attached to a specific service within a project.\n */\nasync function findVolumeByService(\n\tclient: RailwayClient,\n\tprojectId: string,\n\tserviceId: string,\n): Promise<string | undefined> {\n\tconst result = await client.query<{\n\t\tproject: {\n\t\t\tvolumes: {\n\t\t\t\tedges: Array<{\n\t\t\t\t\tnode: {\n\t\t\t\t\t\tid: string;\n\t\t\t\t\t\tvolumeInstances: {\n\t\t\t\t\t\t\tedges: Array<{\n\t\t\t\t\t\t\t\tnode: { serviceId: string };\n\t\t\t\t\t\t\t}>;\n\t\t\t\t\t\t};\n\t\t\t\t\t};\n\t\t\t\t}>;\n\t\t\t};\n\t\t};\n\t}>(VOLUMES_QUERY, { projectId });\n\n\tconst match = result.project.volumes.edges.find((edge) =>\n\t\tedge.node.volumeInstances.edges.some(\n\t\t\t(vi) => vi.node.serviceId === serviceId,\n\t\t),\n\t);\n\n\treturn match?.node.id;\n}\n\n/**\n * Dynamic provider implementing CRUD for Railway persistent volumes.\n *\n * Uses adopt-or-create on `create()`: finds an existing volume by service\n * before creating a new one. Volumes are immutable after creation — changing\n * `serviceId`, `mountPath`, `environmentId`, or `projectId` triggers replacement.\n */\nexport class RailwayVolumeResourceProvider\n\timplements pulumi.dynamic.ResourceProvider\n{\n\tasync create(\n\t\tinputs: RailwayVolumeInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new RailwayClient(inputs.token);\n\n\t\tlet volumeId = await findVolumeByService(\n\t\t\tclient,\n\t\t\tinputs.projectId,\n\t\t\tinputs.serviceId,\n\t\t);\n\n\t\tif (volumeId) {\n\t\t\tpulumi.log.info(`Adopting existing volume for service (${volumeId})`);\n\t\t} else {\n\t\t\tconst result = await client.query<{\n\t\t\t\tvolumeCreate: { id: string };\n\t\t\t}>(VOLUME_CREATE, {\n\t\t\t\tinput: {\n\t\t\t\t\tprojectId: inputs.projectId,\n\t\t\t\t\tserviceId: inputs.serviceId,\n\t\t\t\t\tenvironmentId: inputs.environmentId,\n\t\t\t\t\tmountPath: inputs.mountPath,\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tvolumeId = result.volumeCreate.id;\n\n\t\t\t// A volume mounts into the container only on the NEXT deployment —\n\t\t\t// attaching alone changes nothing for a running service (the dashboard\n\t\t\t// redeploys after attach for the same reason). Best-effort: a service\n\t\t\t// with no deployable source yet (code service before its first\n\t\t\t// `railway up`) has nothing to redeploy, and its mount lands with that\n\t\t\t// first deploy anyway.\n\t\t\ttry {\n\t\t\t\tawait client.query(VOLUME_ATTACH_DEPLOY, {\n\t\t\t\t\tserviceId: inputs.serviceId,\n\t\t\t\t\tenvironmentId: inputs.environmentId,\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\tpulumi.log.warn(\n\t\t\t\t\t`Volume attached; redeploy skipped (service not deployable yet?): ${String(error)}`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\tid: volumeId,\n\t\t\touts: { ...inputs, volumeId },\n\t\t};\n\t}\n\n\tasync read(\n\t\tid: string,\n\t\tprops: RailwayVolumeOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\tlet volumeId: string | undefined;\n\n\t\ttry {\n\t\t\tvolumeId = await findVolumeByService(\n\t\t\t\tclient,\n\t\t\t\tprops.projectId,\n\t\t\t\tprops.serviceId,\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tpulumi.log.warn(\n\t\t\t\t`Railway volume refresh lookup failed; keeping existing state: ${String(error)}`,\n\t\t\t);\n\t\t}\n\n\t\t// A project-level service's volume can be momentarily unresolvable at\n\t\t// refresh (eventual consistency / environment-scoped volume instances), so\n\t\t// fall back to the stored id rather than throwing — that turned a healthy\n\t\t// volume into a refresh error. A genuinely-deleted volume is re-adopted or\n\t\t// recreated on the next `up` (create is adopt-or-create). `read` never\n\t\t// fabricates drift and never hard-fails.\n\t\tconst resolvedId = volumeId ?? props.volumeId ?? id;\n\n\t\treturn { id: resolvedId, props: { ...props, volumeId: resolvedId } };\n\t}\n\n\tasync delete(id: string, props: RailwayVolumeOutputs): Promise<void> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\ttry {\n\t\t\tawait client.query(VOLUME_DELETE, { volumeId: id });\n\t\t} catch {\n\t\t\tpulumi.log.warn(\n\t\t\t\t\"Failed to delete Railway volume (may already be deleted)\",\n\t\t\t);\n\t\t}\n\t}\n\n\tasync diff(\n\t\t_id: string,\n\t\tolds: RailwayVolumeOutputs,\n\t\tnews: RailwayVolumeInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\n\t\tif (olds.serviceId !== news.serviceId) {\n\t\t\treplaces.push(\"serviceId\");\n\t\t}\n\n\t\tif (olds.mountPath !== news.mountPath) {\n\t\t\treplaces.push(\"mountPath\");\n\t\t}\n\n\t\tif (olds.environmentId !== news.environmentId) {\n\t\t\treplaces.push(\"environmentId\");\n\t\t}\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\treplaces.push(\"projectId\");\n\t\t}\n\n\t\treturn {\n\t\t\tchanges: replaces.length > 0,\n\t\t\treplaces,\n\t\t\tdeleteBeforeReplace: true,\n\t\t};\n\t}\n}\n\n/** Internal dynamic resource — not part of the public API. */\nclass RailwayVolumeResource extends pulumi.dynamic.Resource {\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\ttoken: pulumi.Input<string>;\n\t\t\tprojectId: pulumi.Input<string>;\n\t\t\tserviceId: pulumi.Input<string>;\n\t\t\tenvironmentId: pulumi.Input<string>;\n\t\t\tmountPath: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew RailwayVolumeResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, volumeId: undefined },\n\t\t\topts,\n\t\t);\n\t}\n}\n\n/** Options type for RailwayVolume — replaces Pulumi's native `provider` field. */\ntype RailwayVolumeOptions = Omit<\n\tpulumi.ComponentResourceOptions,\n\t\"provider\"\n> & {\n\t/** Railway authentication context. */\n\tprovider: RailwayProvider;\n\n\t/** Railway project context. */\n\tproject: RailwayProject;\n\n\t/** Railway environment context. */\n\tenvironment: RailwayEnvironment;\n\n\t/** Railway service context. */\n\tservice: RailwayService;\n};\n\n/** Args for RailwayVolume. */\nexport interface RailwayVolumeArgs {\n\t/** Absolute path inside the container where the volume is mounted. */\n\tmountPath: pulumi.Input<string>;\n}\n\n/**\n * Manages a Railway persistent volume with adopt-or-create semantics.\n *\n * @example\n * ```typescript\n * new RailwayVolume(\"api-data\", {\n * mountPath: \"/data\",\n * }, { provider, project, environment, service });\n * ```\n */\nexport class RailwayVolume extends pulumi.ComponentResource {\n\tconstructor(\n\t\tname: string,\n\t\targs: RailwayVolumeArgs,\n\t\topts: RailwayVolumeOptions,\n\t) {\n\t\tconst { provider, project, environment, service, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:railway:Volume\", name, {}, pulumiOpts);\n\n\t\tnew RailwayVolumeResource(\n\t\t\t`${name}-resource`,\n\t\t\t{\n\t\t\t\ttoken: provider.token,\n\t\t\t\tprojectId: project.id,\n\t\t\t\tserviceId: service.id,\n\t\t\t\tenvironmentId: environment.id,\n\t\t\t\tmountPath: args.mountPath,\n\t\t\t},\n\t\t\t// Forward the consumer's resource options to the underlying resource. Pulumi\n\t\t\t// auto-inherits `provider`/`protect` from the parent component, but NOT options\n\t\t\t// like `retainOnDelete` — without this pass-through, setting `retainOnDelete` on a\n\t\t\t// RailwayVolume would silently never reach the actual cloud volume.\n\t\t\tpulumi.mergeOptions(pulumiOpts, { parent: this }),\n\t\t);\n\n\t\tthis.registerOutputs({});\n\t}\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@infracraft/pulumi",
3
- "version": "1.22.1",
3
+ "version": "1.24.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Native Pulumi providers for Railway, Neon, Vercel, and Fly.io with adopt-or-create semantics and deploy orchestration. No Terraform bridge.",