@infracraft/pulumi 0.1.3 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of @infracraft/pulumi might be problematic. Click here for more details.

@@ -60,20 +60,32 @@ var NeonProjectProvider = class {
60
60
  };
61
61
  }
62
62
  /**
63
+ * Updates the Neon project name via PATCH.
64
+ */
65
+ async update(id, _olds, news) {
66
+ await new require_neon_client.NeonClient(news.apiKey).patch(`/projects/${id}`, { project: { name: news.name } });
67
+ return { outs: {
68
+ ...news,
69
+ projectId: id
70
+ } };
71
+ }
72
+ /**
63
73
  * Skips deletion to protect production databases.
64
74
  */
65
75
  async delete() {
66
76
  _pulumi_pulumi.log.warn("Neon project deletion skipped — projects are not deleted by Pulumi");
67
77
  }
68
78
  /**
69
- * Compares old and new inputs. `name` or `orgId` changes trigger replacement.
79
+ * Compares old and new inputs. `orgId` changes trigger replacement.
80
+ * `name` changes trigger in-place update via PATCH.
70
81
  */
71
82
  async diff(_id, olds, news) {
72
83
  const replaces = [];
73
- if (olds.name !== news.name) replaces.push("name");
84
+ const changes = [];
85
+ if (olds.name !== news.name) changes.push("name");
74
86
  if (olds.orgId !== news.orgId) replaces.push("orgId");
75
87
  return {
76
- changes: replaces.length > 0,
88
+ changes: replaces.length > 0 || changes.length > 0,
77
89
  replaces,
78
90
  deleteBeforeReplace: true
79
91
  };
@@ -1 +1 @@
1
- {"version":3,"file":"project.cjs","names":["NeonClient","pulumi"],"sources":["../../src/neon/project.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport { NeonClient } from \"./client.js\";\n\n/** Resolved inputs for the Neon project dynamic provider. */\nexport interface NeonProjectInputs {\n\t/** Neon API key. */\n\tapiKey: string;\n\n\t/** Exact project display name to adopt or create. */\n\tname: string;\n\n\t/** Optional Neon organization ID to scope the project search. */\n\torgId?: string;\n}\n\n/** Persisted state for the Neon project. */\ninterface NeonProjectOutputs extends NeonProjectInputs {\n\t/** Neon-assigned project ID (e.g. `\"quiet-forest-69719462\"`). */\n\tprojectId: string;\n}\n\n/** Neon API response for listing projects. */\ninterface ProjectListResponse {\n\tprojects: Array<{ id: string; name: string }>;\n}\n\n/** Neon API response for project creation. */\ninterface ProjectCreateResponse {\n\tproject: { id: string; name: string };\n}\n\n/** Neon API response for reading a single project. */\ninterface ProjectReadResponse {\n\tproject: { id: string; name: string };\n}\n\n/**\n * Dynamic provider implementing adopt-or-create for Neon projects.\n *\n * On `create()`, queries `GET /projects` and performs an exact name match.\n * If found, adopts the existing project. If not, creates a new one via\n * `POST /projects`. Deletion is a no-op to protect production databases.\n */\nclass NeonProjectProvider implements pulumi.dynamic.ResourceProvider {\n\t/**\n\t * Creates or adopts a Neon project by name.\n\t *\n\t * @param inputs Resolved project configuration\n\t * @returns The Neon project ID as the resource ID\n\t */\n\tasync create(\n\t\tinputs: NeonProjectInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new NeonClient(inputs.apiKey);\n\n\t\tconst query = inputs.orgId\n\t\t\t? `/projects?org_id=${inputs.orgId}&search=${encodeURIComponent(inputs.name)}`\n\t\t\t: \"/projects\";\n\n\t\tconst result = await client.get<ProjectListResponse>(query);\n\n\t\tconst existing = result.projects.find((p) => p.name === inputs.name);\n\n\t\tlet projectId: string;\n\n\t\tif (existing) {\n\t\t\tpulumi.log.info(\n\t\t\t\t`Adopting existing Neon project \"${inputs.name}\" (${existing.id})`,\n\t\t\t);\n\n\t\t\tprojectId = existing.id;\n\t\t} else {\n\t\t\tpulumi.log.info(`Neon project \"${inputs.name}\" not found — creating...`);\n\n\t\t\tconst created = await client.post<ProjectCreateResponse>(\"/projects\", {\n\t\t\t\tproject: { name: inputs.name },\n\t\t\t});\n\n\t\t\tprojectId = created.project.id;\n\t\t}\n\n\t\tconst outs: NeonProjectOutputs = { ...inputs, projectId };\n\n\t\treturn { id: projectId, outs };\n\t}\n\n\t/**\n\t * Reads current state for `pulumi refresh`.\n\t *\n\t * @param id Current Neon project ID\n\t * @param props Last known persisted state\n\t * @returns Refreshed resource ID and properties\n\t * @throws {Error} If the project no longer exists\n\t */\n\tasync read(\n\t\tid: string,\n\t\tprops: NeonProjectOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new NeonClient(props.apiKey);\n\n\t\tconst result = await client.get<ProjectReadResponse>(`/projects/${id}`);\n\n\t\treturn {\n\t\t\tid: result.project.id,\n\t\t\tprops: {\n\t\t\t\t...props,\n\t\t\t\tname: result.project.name,\n\t\t\t\tprojectId: result.project.id,\n\t\t\t},\n\t\t};\n\t}\n\n\t/**\n\t * Skips deletion to protect production databases.\n\t */\n\tasync delete(): Promise<void> {\n\t\tpulumi.log.warn(\n\t\t\t\"Neon project deletion skipped — projects are not deleted by Pulumi\",\n\t\t);\n\t}\n\n\t/**\n\t * Compares old and new inputs. `name` or `orgId` changes trigger replacement.\n\t */\n\tasync diff(\n\t\t_id: string,\n\t\tolds: NeonProjectOutputs,\n\t\tnews: NeonProjectInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\n\t\tif (olds.name !== news.name) {\n\t\t\treplaces.push(\"name\");\n\t\t}\n\n\t\tif (olds.orgId !== news.orgId) {\n\t\t\treplaces.push(\"orgId\");\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/**\n * Manages a Neon project with adopt-or-create semantics.\n *\n * Discovers or creates the project by exact name match. Deletion is a no-op\n * to prevent accidental removal of production databases.\n *\n * @example\n * ```typescript\n * const project = new NeonProject(\"neon-project\", {\n * apiKey: config.requireSecret(\"neonApiKey\"),\n * name: \"my-app\",\n * orgId: \"org-abc123\",\n * });\n *\n * // Use the resolved project ID downstream\n * const branch = new NeonBranch(\"neon-branch-production\", {\n * apiKey: config.requireSecret(\"neonApiKey\"),\n * projectId: project.projectId,\n * name: \"production\",\n * });\n * ```\n */\nexport class NeonProject extends pulumi.dynamic.Resource {\n\t/** Neon-assigned project ID. */\n\tpublic declare readonly projectId: pulumi.Output<string>;\n\n\t/**\n\t * @param name Pulumi resource name\n\t * @param args Project configuration inputs\n\t * @param opts Standard Pulumi resource options\n\t */\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\t/** Neon API key. */\n\t\t\tapiKey: pulumi.Input<string>;\n\n\t\t\t/** Exact project display name to adopt or create. */\n\t\t\tname: pulumi.Input<string>;\n\n\t\t\t/** Optional Neon organization ID to scope the project search. */\n\t\t\torgId?: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew NeonProjectProvider(),\n\t\t\tname,\n\t\t\t{ ...args, projectId: undefined },\n\t\t\topts,\n\t\t);\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;AA2CA,IAAM,sBAAN,MAAqE;;;;;;;CAOpE,MAAM,OACL,QACuC;EACvC,MAAM,SAAS,IAAIA,+BAAW,OAAO,MAAM;EAE3C,MAAM,QAAQ,OAAO,QAClB,oBAAoB,OAAO,MAAM,UAAU,mBAAmB,OAAO,IAAI,MACzE;EAIH,MAAM,YAAW,MAFI,OAAO,IAAyB,KAAK,GAElC,SAAS,MAAM,MAAM,EAAE,SAAS,OAAO,IAAI;EAEnE,IAAI;EAEJ,IAAI,UAAU;GACb,eAAO,IAAI,KACV,mCAAmC,OAAO,KAAK,KAAK,SAAS,GAAG,EACjE;GAEA,YAAY,SAAS;EACtB,OAAO;GACN,eAAO,IAAI,KAAK,iBAAiB,OAAO,KAAK,0BAA0B;GAMvE,aAAY,MAJU,OAAO,KAA4B,aAAa,EACrE,SAAS,EAAE,MAAM,OAAO,KAAK,EAC9B,CAAC,GAEmB,QAAQ;EAC7B;EAEA,MAAM,OAA2B;GAAE,GAAG;GAAQ;EAAU;EAExD,OAAO;GAAE,IAAI;GAAW;EAAK;CAC9B;;;;;;;;;CAUA,MAAM,KACL,IACA,OACqC;EAGrC,MAAM,SAAS,MAAM,IAFFA,+BAAW,MAAM,MAEV,EAAE,IAAyB,aAAa,IAAI;EAEtE,OAAO;GACN,IAAI,OAAO,QAAQ;GACnB,OAAO;IACN,GAAG;IACH,MAAM,OAAO,QAAQ;IACrB,WAAW,OAAO,QAAQ;GAC3B;EACD;CACD;;;;CAKA,MAAM,SAAwB;EAC7B,eAAO,IAAI,KACV,oEACD;CACD;;;;CAKA,MAAM,KACL,KACA,MACA,MACqC;EACrC,MAAM,WAAqB,CAAC;EAE5B,IAAI,KAAK,SAAS,KAAK,MACtB,SAAS,KAAK,MAAM;EAGrB,IAAI,KAAK,UAAU,KAAK,OACvB,SAAS,KAAK,OAAO;EAGtB,OAAO;GACN,SAAS,SAAS,SAAS;GAC3B;GACA,qBAAqB;EACtB;CACD;AACD;;;;;;;;;;;;;;;;;;;;;;;AAwBA,IAAa,cAAb,cAAiCC,eAAO,QAAQ,SAAS;;;;;;CASxD,YACC,MACA,MAUA,MACC;EACD,MACC,IAAI,oBAAoB,GACxB,MACA;GAAE,GAAG;GAAM,WAAW;EAAU,GAChC,IACD;CACD;AACD"}
1
+ {"version":3,"file":"project.cjs","names":["NeonClient","pulumi"],"sources":["../../src/neon/project.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport { NeonClient } from \"./client.js\";\n\n/** Resolved inputs for the Neon project dynamic provider. */\nexport interface NeonProjectInputs {\n\t/** Neon API key. */\n\tapiKey: string;\n\n\t/** Exact project display name to adopt or create. */\n\tname: string;\n\n\t/** Optional Neon organization ID to scope the project search. */\n\torgId?: string;\n}\n\n/** Persisted state for the Neon project. */\ninterface NeonProjectOutputs extends NeonProjectInputs {\n\t/** Neon-assigned project ID (e.g. `\"quiet-forest-69719462\"`). */\n\tprojectId: string;\n}\n\n/** Neon API response for listing projects. */\ninterface ProjectListResponse {\n\tprojects: Array<{ id: string; name: string }>;\n}\n\n/** Neon API response for project creation. */\ninterface ProjectCreateResponse {\n\tproject: { id: string; name: string };\n}\n\n/** Neon API response for reading a single project. */\ninterface ProjectReadResponse {\n\tproject: { id: string; name: string };\n}\n\n/**\n * Dynamic provider implementing adopt-or-create for Neon projects.\n *\n * On `create()`, queries `GET /projects` and performs an exact name match.\n * If found, adopts the existing project. If not, creates a new one via\n * `POST /projects`. Deletion is a no-op to protect production databases.\n */\nclass NeonProjectProvider implements pulumi.dynamic.ResourceProvider {\n\t/**\n\t * Creates or adopts a Neon project by name.\n\t *\n\t * @param inputs Resolved project configuration\n\t * @returns The Neon project ID as the resource ID\n\t */\n\tasync create(\n\t\tinputs: NeonProjectInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new NeonClient(inputs.apiKey);\n\n\t\tconst query = inputs.orgId\n\t\t\t? `/projects?org_id=${inputs.orgId}&search=${encodeURIComponent(inputs.name)}`\n\t\t\t: \"/projects\";\n\n\t\tconst result = await client.get<ProjectListResponse>(query);\n\n\t\tconst existing = result.projects.find((p) => p.name === inputs.name);\n\n\t\tlet projectId: string;\n\n\t\tif (existing) {\n\t\t\tpulumi.log.info(\n\t\t\t\t`Adopting existing Neon project \"${inputs.name}\" (${existing.id})`,\n\t\t\t);\n\n\t\t\tprojectId = existing.id;\n\t\t} else {\n\t\t\tpulumi.log.info(`Neon project \"${inputs.name}\" not found — creating...`);\n\n\t\t\tconst created = await client.post<ProjectCreateResponse>(\"/projects\", {\n\t\t\t\tproject: { name: inputs.name },\n\t\t\t});\n\n\t\t\tprojectId = created.project.id;\n\t\t}\n\n\t\tconst outs: NeonProjectOutputs = { ...inputs, projectId };\n\n\t\treturn { id: projectId, outs };\n\t}\n\n\t/**\n\t * Reads current state for `pulumi refresh`.\n\t *\n\t * @param id Current Neon project ID\n\t * @param props Last known persisted state\n\t * @returns Refreshed resource ID and properties\n\t * @throws {Error} If the project no longer exists\n\t */\n\tasync read(\n\t\tid: string,\n\t\tprops: NeonProjectOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new NeonClient(props.apiKey);\n\n\t\tconst result = await client.get<ProjectReadResponse>(`/projects/${id}`);\n\n\t\treturn {\n\t\t\tid: result.project.id,\n\t\t\tprops: {\n\t\t\t\t...props,\n\t\t\t\tname: result.project.name,\n\t\t\t\tprojectId: result.project.id,\n\t\t\t},\n\t\t};\n\t}\n\n\t/**\n\t * Updates the Neon project name via PATCH.\n\t */\n\tasync update(\n\t\tid: string,\n\t\t_olds: NeonProjectOutputs,\n\t\tnews: NeonProjectInputs,\n\t): Promise<pulumi.dynamic.UpdateResult> {\n\t\tconst client = new NeonClient(news.apiKey);\n\n\t\tawait client.patch(`/projects/${id}`, {\n\t\t\tproject: { name: news.name },\n\t\t});\n\n\t\treturn { outs: { ...news, projectId: id } };\n\t}\n\n\t/**\n\t * Skips deletion to protect production databases.\n\t */\n\tasync delete(): Promise<void> {\n\t\tpulumi.log.warn(\n\t\t\t\"Neon project deletion skipped — projects are not deleted by Pulumi\",\n\t\t);\n\t}\n\n\t/**\n\t * Compares old and new inputs. `orgId` changes trigger replacement.\n\t * `name` changes trigger in-place update via PATCH.\n\t */\n\tasync diff(\n\t\t_id: string,\n\t\tolds: NeonProjectOutputs,\n\t\tnews: NeonProjectInputs,\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.orgId !== news.orgId) {\n\t\t\treplaces.push(\"orgId\");\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/**\n * Manages a Neon project with adopt-or-create semantics.\n *\n * Discovers or creates the project by exact name match. Deletion is a no-op\n * to prevent accidental removal of production databases.\n *\n * @example\n * ```typescript\n * const project = new NeonProject(\"neon-project\", {\n * apiKey: config.requireSecret(\"neonApiKey\"),\n * name: \"my-app\",\n * orgId: \"org-abc123\",\n * });\n *\n * // Use the resolved project ID downstream\n * const branch = new NeonBranch(\"neon-branch-production\", {\n * apiKey: config.requireSecret(\"neonApiKey\"),\n * projectId: project.projectId,\n * name: \"production\",\n * });\n * ```\n */\nexport class NeonProject extends pulumi.dynamic.Resource {\n\t/** Neon-assigned project ID. */\n\tpublic declare readonly projectId: pulumi.Output<string>;\n\n\t/**\n\t * @param name Pulumi resource name\n\t * @param args Project configuration inputs\n\t * @param opts Standard Pulumi resource options\n\t */\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\t/** Neon API key. */\n\t\t\tapiKey: pulumi.Input<string>;\n\n\t\t\t/** Exact project display name to adopt or create. */\n\t\t\tname: pulumi.Input<string>;\n\n\t\t\t/** Optional Neon organization ID to scope the project search. */\n\t\t\torgId?: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew NeonProjectProvider(),\n\t\t\tname,\n\t\t\t{ ...args, projectId: undefined },\n\t\t\topts,\n\t\t);\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;AA2CA,IAAM,sBAAN,MAAqE;;;;;;;CAOpE,MAAM,OACL,QACuC;EACvC,MAAM,SAAS,IAAIA,+BAAW,OAAO,MAAM;EAE3C,MAAM,QAAQ,OAAO,QAClB,oBAAoB,OAAO,MAAM,UAAU,mBAAmB,OAAO,IAAI,MACzE;EAIH,MAAM,YAAW,MAFI,OAAO,IAAyB,KAAK,GAElC,SAAS,MAAM,MAAM,EAAE,SAAS,OAAO,IAAI;EAEnE,IAAI;EAEJ,IAAI,UAAU;GACb,eAAO,IAAI,KACV,mCAAmC,OAAO,KAAK,KAAK,SAAS,GAAG,EACjE;GAEA,YAAY,SAAS;EACtB,OAAO;GACN,eAAO,IAAI,KAAK,iBAAiB,OAAO,KAAK,0BAA0B;GAMvE,aAAY,MAJU,OAAO,KAA4B,aAAa,EACrE,SAAS,EAAE,MAAM,OAAO,KAAK,EAC9B,CAAC,GAEmB,QAAQ;EAC7B;EAEA,MAAM,OAA2B;GAAE,GAAG;GAAQ;EAAU;EAExD,OAAO;GAAE,IAAI;GAAW;EAAK;CAC9B;;;;;;;;;CAUA,MAAM,KACL,IACA,OACqC;EAGrC,MAAM,SAAS,MAAM,IAFFA,+BAAW,MAAM,MAEV,EAAE,IAAyB,aAAa,IAAI;EAEtE,OAAO;GACN,IAAI,OAAO,QAAQ;GACnB,OAAO;IACN,GAAG;IACH,MAAM,OAAO,QAAQ;IACrB,WAAW,OAAO,QAAQ;GAC3B;EACD;CACD;;;;CAKA,MAAM,OACL,IACA,OACA,MACuC;EAGvC,MAAM,IAFaA,+BAAW,KAAK,MAExB,EAAE,MAAM,aAAa,MAAM,EACrC,SAAS,EAAE,MAAM,KAAK,KAAK,EAC5B,CAAC;EAED,OAAO,EAAE,MAAM;GAAE,GAAG;GAAM,WAAW;EAAG,EAAE;CAC3C;;;;CAKA,MAAM,SAAwB;EAC7B,eAAO,IAAI,KACV,oEACD;CACD;;;;;CAMA,MAAM,KACL,KACA,MACA,MACqC;EACrC,MAAM,WAAqB,CAAC;EAC5B,MAAM,UAAoB,CAAC;EAE3B,IAAI,KAAK,SAAS,KAAK,MACtB,QAAQ,KAAK,MAAM;EAGpB,IAAI,KAAK,UAAU,KAAK,OACvB,SAAS,KAAK,OAAO;EAGtB,OAAO;GACN,SAAS,SAAS,SAAS,KAAK,QAAQ,SAAS;GACjD;GACA,qBAAqB;EACtB;CACD;AACD;;;;;;;;;;;;;;;;;;;;;;;AAwBA,IAAa,cAAb,cAAiCC,eAAO,QAAQ,SAAS;;;;;;CASxD,YACC,MACA,MAUA,MACC;EACD,MACC,IAAI,oBAAoB,GACxB,MACA;GAAE,GAAG;GAAM,WAAW;EAAU,GAChC,IACD;CACD;AACD"}
@@ -1 +1 @@
1
- {"version":3,"file":"project.d.cts","names":[],"sources":["../../src/neon/project.ts"],"mappings":";;;;;UAIiB,iBAAA;;EAEhB,MAAA;EAFiC;EAKjC,IAAA;EALiC;EAQjC,KAAA;AAAA;;;AAAK;AA6JN;;;;;;;;;;;;;;;;;;;cAAa,WAAA,SAAoB,MAAA,CAAO,OAAA,CAAQ,QAAA;EAarC;EAAA,SAXc,SAAA,EAAW,MAAA,CAAO,MAAA;EAcxC;;;;;cAND,IAAA,UACA,IAAA;IAAA,oBAEC,MAAA,EAAQ,MAAA,CAAO,KAAA,UAQF;IALb,IAAA,EAAM,MAAA,CAAO,KAAA,UAKqB;IAFlC,KAAA,GAAQ,MAAA,CAAO,KAAA;EAAA,GAEhB,IAAA,GAAO,MAAA,CAAO,qBAAA;AAAA"}
1
+ {"version":3,"file":"project.d.cts","names":[],"sources":["../../src/neon/project.ts"],"mappings":";;;;;UAIiB,iBAAA;;EAEhB,MAAA;EAFiC;EAKjC,IAAA;EALiC;EAQjC,KAAA;AAAA;;;AAAK;AAgLN;;;;;;;;;;;;;;;;;;;cAAa,WAAA,SAAoB,MAAA,CAAO,OAAA,CAAQ,QAAA;EAarC;EAAA,SAXc,SAAA,EAAW,MAAA,CAAO,MAAA;EAcxC;;;;;cAND,IAAA,UACA,IAAA;IAAA,oBAEC,MAAA,EAAQ,MAAA,CAAO,KAAA,UAQF;IALb,IAAA,EAAM,MAAA,CAAO,KAAA,UAKqB;IAFlC,KAAA,GAAQ,MAAA,CAAO,KAAA;EAAA,GAEhB,IAAA,GAAO,MAAA,CAAO,qBAAA;AAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"project.d.mts","names":[],"sources":["../../src/neon/project.ts"],"mappings":";;;;;UAIiB,iBAAA;;EAEhB,MAAA;EAFiC;EAKjC,IAAA;EALiC;EAQjC,KAAA;AAAA;;;AAAK;AA6JN;;;;;;;;;;;;;;;;;;;cAAa,WAAA,SAAoB,MAAA,CAAO,OAAA,CAAQ,QAAA;EAarC;EAAA,SAXc,SAAA,EAAW,MAAA,CAAO,MAAA;EAcxC;;;;;cAND,IAAA,UACA,IAAA;IAAA,oBAEC,MAAA,EAAQ,MAAA,CAAO,KAAA,UAQF;IALb,IAAA,EAAM,MAAA,CAAO,KAAA,UAKqB;IAFlC,KAAA,GAAQ,MAAA,CAAO,KAAA;EAAA,GAEhB,IAAA,GAAO,MAAA,CAAO,qBAAA;AAAA"}
1
+ {"version":3,"file":"project.d.mts","names":[],"sources":["../../src/neon/project.ts"],"mappings":";;;;;UAIiB,iBAAA;;EAEhB,MAAA;EAFiC;EAKjC,IAAA;EALiC;EAQjC,KAAA;AAAA;;;AAAK;AAgLN;;;;;;;;;;;;;;;;;;;cAAa,WAAA,SAAoB,MAAA,CAAO,OAAA,CAAQ,QAAA;EAarC;EAAA,SAXc,SAAA,EAAW,MAAA,CAAO,MAAA;EAcxC;;;;;cAND,IAAA,UACA,IAAA;IAAA,oBAEC,MAAA,EAAQ,MAAA,CAAO,KAAA,UAQF;IALb,IAAA,EAAM,MAAA,CAAO,KAAA,UAKqB;IAFlC,KAAA,GAAQ,MAAA,CAAO,KAAA;EAAA,GAEhB,IAAA,GAAO,MAAA,CAAO,qBAAA;AAAA"}
@@ -58,20 +58,32 @@ var NeonProjectProvider = class {
58
58
  };
59
59
  }
60
60
  /**
61
+ * Updates the Neon project name via PATCH.
62
+ */
63
+ async update(id, _olds, news) {
64
+ await new NeonClient(news.apiKey).patch(`/projects/${id}`, { project: { name: news.name } });
65
+ return { outs: {
66
+ ...news,
67
+ projectId: id
68
+ } };
69
+ }
70
+ /**
61
71
  * Skips deletion to protect production databases.
62
72
  */
63
73
  async delete() {
64
74
  pulumi.log.warn("Neon project deletion skipped — projects are not deleted by Pulumi");
65
75
  }
66
76
  /**
67
- * Compares old and new inputs. `name` or `orgId` changes trigger replacement.
77
+ * Compares old and new inputs. `orgId` changes trigger replacement.
78
+ * `name` changes trigger in-place update via PATCH.
68
79
  */
69
80
  async diff(_id, olds, news) {
70
81
  const replaces = [];
71
- if (olds.name !== news.name) replaces.push("name");
82
+ const changes = [];
83
+ if (olds.name !== news.name) changes.push("name");
72
84
  if (olds.orgId !== news.orgId) replaces.push("orgId");
73
85
  return {
74
- changes: replaces.length > 0,
86
+ changes: replaces.length > 0 || changes.length > 0,
75
87
  replaces,
76
88
  deleteBeforeReplace: true
77
89
  };
@@ -1 +1 @@
1
- {"version":3,"file":"project.mjs","names":[],"sources":["../../src/neon/project.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport { NeonClient } from \"./client.js\";\n\n/** Resolved inputs for the Neon project dynamic provider. */\nexport interface NeonProjectInputs {\n\t/** Neon API key. */\n\tapiKey: string;\n\n\t/** Exact project display name to adopt or create. */\n\tname: string;\n\n\t/** Optional Neon organization ID to scope the project search. */\n\torgId?: string;\n}\n\n/** Persisted state for the Neon project. */\ninterface NeonProjectOutputs extends NeonProjectInputs {\n\t/** Neon-assigned project ID (e.g. `\"quiet-forest-69719462\"`). */\n\tprojectId: string;\n}\n\n/** Neon API response for listing projects. */\ninterface ProjectListResponse {\n\tprojects: Array<{ id: string; name: string }>;\n}\n\n/** Neon API response for project creation. */\ninterface ProjectCreateResponse {\n\tproject: { id: string; name: string };\n}\n\n/** Neon API response for reading a single project. */\ninterface ProjectReadResponse {\n\tproject: { id: string; name: string };\n}\n\n/**\n * Dynamic provider implementing adopt-or-create for Neon projects.\n *\n * On `create()`, queries `GET /projects` and performs an exact name match.\n * If found, adopts the existing project. If not, creates a new one via\n * `POST /projects`. Deletion is a no-op to protect production databases.\n */\nclass NeonProjectProvider implements pulumi.dynamic.ResourceProvider {\n\t/**\n\t * Creates or adopts a Neon project by name.\n\t *\n\t * @param inputs Resolved project configuration\n\t * @returns The Neon project ID as the resource ID\n\t */\n\tasync create(\n\t\tinputs: NeonProjectInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new NeonClient(inputs.apiKey);\n\n\t\tconst query = inputs.orgId\n\t\t\t? `/projects?org_id=${inputs.orgId}&search=${encodeURIComponent(inputs.name)}`\n\t\t\t: \"/projects\";\n\n\t\tconst result = await client.get<ProjectListResponse>(query);\n\n\t\tconst existing = result.projects.find((p) => p.name === inputs.name);\n\n\t\tlet projectId: string;\n\n\t\tif (existing) {\n\t\t\tpulumi.log.info(\n\t\t\t\t`Adopting existing Neon project \"${inputs.name}\" (${existing.id})`,\n\t\t\t);\n\n\t\t\tprojectId = existing.id;\n\t\t} else {\n\t\t\tpulumi.log.info(`Neon project \"${inputs.name}\" not found — creating...`);\n\n\t\t\tconst created = await client.post<ProjectCreateResponse>(\"/projects\", {\n\t\t\t\tproject: { name: inputs.name },\n\t\t\t});\n\n\t\t\tprojectId = created.project.id;\n\t\t}\n\n\t\tconst outs: NeonProjectOutputs = { ...inputs, projectId };\n\n\t\treturn { id: projectId, outs };\n\t}\n\n\t/**\n\t * Reads current state for `pulumi refresh`.\n\t *\n\t * @param id Current Neon project ID\n\t * @param props Last known persisted state\n\t * @returns Refreshed resource ID and properties\n\t * @throws {Error} If the project no longer exists\n\t */\n\tasync read(\n\t\tid: string,\n\t\tprops: NeonProjectOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new NeonClient(props.apiKey);\n\n\t\tconst result = await client.get<ProjectReadResponse>(`/projects/${id}`);\n\n\t\treturn {\n\t\t\tid: result.project.id,\n\t\t\tprops: {\n\t\t\t\t...props,\n\t\t\t\tname: result.project.name,\n\t\t\t\tprojectId: result.project.id,\n\t\t\t},\n\t\t};\n\t}\n\n\t/**\n\t * Skips deletion to protect production databases.\n\t */\n\tasync delete(): Promise<void> {\n\t\tpulumi.log.warn(\n\t\t\t\"Neon project deletion skipped — projects are not deleted by Pulumi\",\n\t\t);\n\t}\n\n\t/**\n\t * Compares old and new inputs. `name` or `orgId` changes trigger replacement.\n\t */\n\tasync diff(\n\t\t_id: string,\n\t\tolds: NeonProjectOutputs,\n\t\tnews: NeonProjectInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\n\t\tif (olds.name !== news.name) {\n\t\t\treplaces.push(\"name\");\n\t\t}\n\n\t\tif (olds.orgId !== news.orgId) {\n\t\t\treplaces.push(\"orgId\");\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/**\n * Manages a Neon project with adopt-or-create semantics.\n *\n * Discovers or creates the project by exact name match. Deletion is a no-op\n * to prevent accidental removal of production databases.\n *\n * @example\n * ```typescript\n * const project = new NeonProject(\"neon-project\", {\n * apiKey: config.requireSecret(\"neonApiKey\"),\n * name: \"my-app\",\n * orgId: \"org-abc123\",\n * });\n *\n * // Use the resolved project ID downstream\n * const branch = new NeonBranch(\"neon-branch-production\", {\n * apiKey: config.requireSecret(\"neonApiKey\"),\n * projectId: project.projectId,\n * name: \"production\",\n * });\n * ```\n */\nexport class NeonProject extends pulumi.dynamic.Resource {\n\t/** Neon-assigned project ID. */\n\tpublic declare readonly projectId: pulumi.Output<string>;\n\n\t/**\n\t * @param name Pulumi resource name\n\t * @param args Project configuration inputs\n\t * @param opts Standard Pulumi resource options\n\t */\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\t/** Neon API key. */\n\t\t\tapiKey: pulumi.Input<string>;\n\n\t\t\t/** Exact project display name to adopt or create. */\n\t\t\tname: pulumi.Input<string>;\n\n\t\t\t/** Optional Neon organization ID to scope the project search. */\n\t\t\torgId?: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew NeonProjectProvider(),\n\t\t\tname,\n\t\t\t{ ...args, projectId: undefined },\n\t\t\topts,\n\t\t);\n\t}\n}\n"],"mappings":";;;;;;;;;;;;AA2CA,IAAM,sBAAN,MAAqE;;;;;;;CAOpE,MAAM,OACL,QACuC;EACvC,MAAM,SAAS,IAAI,WAAW,OAAO,MAAM;EAE3C,MAAM,QAAQ,OAAO,QAClB,oBAAoB,OAAO,MAAM,UAAU,mBAAmB,OAAO,IAAI,MACzE;EAIH,MAAM,YAAW,MAFI,OAAO,IAAyB,KAAK,GAElC,SAAS,MAAM,MAAM,EAAE,SAAS,OAAO,IAAI;EAEnE,IAAI;EAEJ,IAAI,UAAU;GACb,OAAO,IAAI,KACV,mCAAmC,OAAO,KAAK,KAAK,SAAS,GAAG,EACjE;GAEA,YAAY,SAAS;EACtB,OAAO;GACN,OAAO,IAAI,KAAK,iBAAiB,OAAO,KAAK,0BAA0B;GAMvE,aAAY,MAJU,OAAO,KAA4B,aAAa,EACrE,SAAS,EAAE,MAAM,OAAO,KAAK,EAC9B,CAAC,GAEmB,QAAQ;EAC7B;EAEA,MAAM,OAA2B;GAAE,GAAG;GAAQ;EAAU;EAExD,OAAO;GAAE,IAAI;GAAW;EAAK;CAC9B;;;;;;;;;CAUA,MAAM,KACL,IACA,OACqC;EAGrC,MAAM,SAAS,MAAM,IAFF,WAAW,MAAM,MAEV,EAAE,IAAyB,aAAa,IAAI;EAEtE,OAAO;GACN,IAAI,OAAO,QAAQ;GACnB,OAAO;IACN,GAAG;IACH,MAAM,OAAO,QAAQ;IACrB,WAAW,OAAO,QAAQ;GAC3B;EACD;CACD;;;;CAKA,MAAM,SAAwB;EAC7B,OAAO,IAAI,KACV,oEACD;CACD;;;;CAKA,MAAM,KACL,KACA,MACA,MACqC;EACrC,MAAM,WAAqB,CAAC;EAE5B,IAAI,KAAK,SAAS,KAAK,MACtB,SAAS,KAAK,MAAM;EAGrB,IAAI,KAAK,UAAU,KAAK,OACvB,SAAS,KAAK,OAAO;EAGtB,OAAO;GACN,SAAS,SAAS,SAAS;GAC3B;GACA,qBAAqB;EACtB;CACD;AACD;;;;;;;;;;;;;;;;;;;;;;;AAwBA,IAAa,cAAb,cAAiC,OAAO,QAAQ,SAAS;;;;;;CASxD,YACC,MACA,MAUA,MACC;EACD,MACC,IAAI,oBAAoB,GACxB,MACA;GAAE,GAAG;GAAM,WAAW;EAAU,GAChC,IACD;CACD;AACD"}
1
+ {"version":3,"file":"project.mjs","names":[],"sources":["../../src/neon/project.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport { NeonClient } from \"./client.js\";\n\n/** Resolved inputs for the Neon project dynamic provider. */\nexport interface NeonProjectInputs {\n\t/** Neon API key. */\n\tapiKey: string;\n\n\t/** Exact project display name to adopt or create. */\n\tname: string;\n\n\t/** Optional Neon organization ID to scope the project search. */\n\torgId?: string;\n}\n\n/** Persisted state for the Neon project. */\ninterface NeonProjectOutputs extends NeonProjectInputs {\n\t/** Neon-assigned project ID (e.g. `\"quiet-forest-69719462\"`). */\n\tprojectId: string;\n}\n\n/** Neon API response for listing projects. */\ninterface ProjectListResponse {\n\tprojects: Array<{ id: string; name: string }>;\n}\n\n/** Neon API response for project creation. */\ninterface ProjectCreateResponse {\n\tproject: { id: string; name: string };\n}\n\n/** Neon API response for reading a single project. */\ninterface ProjectReadResponse {\n\tproject: { id: string; name: string };\n}\n\n/**\n * Dynamic provider implementing adopt-or-create for Neon projects.\n *\n * On `create()`, queries `GET /projects` and performs an exact name match.\n * If found, adopts the existing project. If not, creates a new one via\n * `POST /projects`. Deletion is a no-op to protect production databases.\n */\nclass NeonProjectProvider implements pulumi.dynamic.ResourceProvider {\n\t/**\n\t * Creates or adopts a Neon project by name.\n\t *\n\t * @param inputs Resolved project configuration\n\t * @returns The Neon project ID as the resource ID\n\t */\n\tasync create(\n\t\tinputs: NeonProjectInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new NeonClient(inputs.apiKey);\n\n\t\tconst query = inputs.orgId\n\t\t\t? `/projects?org_id=${inputs.orgId}&search=${encodeURIComponent(inputs.name)}`\n\t\t\t: \"/projects\";\n\n\t\tconst result = await client.get<ProjectListResponse>(query);\n\n\t\tconst existing = result.projects.find((p) => p.name === inputs.name);\n\n\t\tlet projectId: string;\n\n\t\tif (existing) {\n\t\t\tpulumi.log.info(\n\t\t\t\t`Adopting existing Neon project \"${inputs.name}\" (${existing.id})`,\n\t\t\t);\n\n\t\t\tprojectId = existing.id;\n\t\t} else {\n\t\t\tpulumi.log.info(`Neon project \"${inputs.name}\" not found — creating...`);\n\n\t\t\tconst created = await client.post<ProjectCreateResponse>(\"/projects\", {\n\t\t\t\tproject: { name: inputs.name },\n\t\t\t});\n\n\t\t\tprojectId = created.project.id;\n\t\t}\n\n\t\tconst outs: NeonProjectOutputs = { ...inputs, projectId };\n\n\t\treturn { id: projectId, outs };\n\t}\n\n\t/**\n\t * Reads current state for `pulumi refresh`.\n\t *\n\t * @param id Current Neon project ID\n\t * @param props Last known persisted state\n\t * @returns Refreshed resource ID and properties\n\t * @throws {Error} If the project no longer exists\n\t */\n\tasync read(\n\t\tid: string,\n\t\tprops: NeonProjectOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new NeonClient(props.apiKey);\n\n\t\tconst result = await client.get<ProjectReadResponse>(`/projects/${id}`);\n\n\t\treturn {\n\t\t\tid: result.project.id,\n\t\t\tprops: {\n\t\t\t\t...props,\n\t\t\t\tname: result.project.name,\n\t\t\t\tprojectId: result.project.id,\n\t\t\t},\n\t\t};\n\t}\n\n\t/**\n\t * Updates the Neon project name via PATCH.\n\t */\n\tasync update(\n\t\tid: string,\n\t\t_olds: NeonProjectOutputs,\n\t\tnews: NeonProjectInputs,\n\t): Promise<pulumi.dynamic.UpdateResult> {\n\t\tconst client = new NeonClient(news.apiKey);\n\n\t\tawait client.patch(`/projects/${id}`, {\n\t\t\tproject: { name: news.name },\n\t\t});\n\n\t\treturn { outs: { ...news, projectId: id } };\n\t}\n\n\t/**\n\t * Skips deletion to protect production databases.\n\t */\n\tasync delete(): Promise<void> {\n\t\tpulumi.log.warn(\n\t\t\t\"Neon project deletion skipped — projects are not deleted by Pulumi\",\n\t\t);\n\t}\n\n\t/**\n\t * Compares old and new inputs. `orgId` changes trigger replacement.\n\t * `name` changes trigger in-place update via PATCH.\n\t */\n\tasync diff(\n\t\t_id: string,\n\t\tolds: NeonProjectOutputs,\n\t\tnews: NeonProjectInputs,\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.orgId !== news.orgId) {\n\t\t\treplaces.push(\"orgId\");\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/**\n * Manages a Neon project with adopt-or-create semantics.\n *\n * Discovers or creates the project by exact name match. Deletion is a no-op\n * to prevent accidental removal of production databases.\n *\n * @example\n * ```typescript\n * const project = new NeonProject(\"neon-project\", {\n * apiKey: config.requireSecret(\"neonApiKey\"),\n * name: \"my-app\",\n * orgId: \"org-abc123\",\n * });\n *\n * // Use the resolved project ID downstream\n * const branch = new NeonBranch(\"neon-branch-production\", {\n * apiKey: config.requireSecret(\"neonApiKey\"),\n * projectId: project.projectId,\n * name: \"production\",\n * });\n * ```\n */\nexport class NeonProject extends pulumi.dynamic.Resource {\n\t/** Neon-assigned project ID. */\n\tpublic declare readonly projectId: pulumi.Output<string>;\n\n\t/**\n\t * @param name Pulumi resource name\n\t * @param args Project configuration inputs\n\t * @param opts Standard Pulumi resource options\n\t */\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\t/** Neon API key. */\n\t\t\tapiKey: pulumi.Input<string>;\n\n\t\t\t/** Exact project display name to adopt or create. */\n\t\t\tname: pulumi.Input<string>;\n\n\t\t\t/** Optional Neon organization ID to scope the project search. */\n\t\t\torgId?: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew NeonProjectProvider(),\n\t\t\tname,\n\t\t\t{ ...args, projectId: undefined },\n\t\t\topts,\n\t\t);\n\t}\n}\n"],"mappings":";;;;;;;;;;;;AA2CA,IAAM,sBAAN,MAAqE;;;;;;;CAOpE,MAAM,OACL,QACuC;EACvC,MAAM,SAAS,IAAI,WAAW,OAAO,MAAM;EAE3C,MAAM,QAAQ,OAAO,QAClB,oBAAoB,OAAO,MAAM,UAAU,mBAAmB,OAAO,IAAI,MACzE;EAIH,MAAM,YAAW,MAFI,OAAO,IAAyB,KAAK,GAElC,SAAS,MAAM,MAAM,EAAE,SAAS,OAAO,IAAI;EAEnE,IAAI;EAEJ,IAAI,UAAU;GACb,OAAO,IAAI,KACV,mCAAmC,OAAO,KAAK,KAAK,SAAS,GAAG,EACjE;GAEA,YAAY,SAAS;EACtB,OAAO;GACN,OAAO,IAAI,KAAK,iBAAiB,OAAO,KAAK,0BAA0B;GAMvE,aAAY,MAJU,OAAO,KAA4B,aAAa,EACrE,SAAS,EAAE,MAAM,OAAO,KAAK,EAC9B,CAAC,GAEmB,QAAQ;EAC7B;EAEA,MAAM,OAA2B;GAAE,GAAG;GAAQ;EAAU;EAExD,OAAO;GAAE,IAAI;GAAW;EAAK;CAC9B;;;;;;;;;CAUA,MAAM,KACL,IACA,OACqC;EAGrC,MAAM,SAAS,MAAM,IAFF,WAAW,MAAM,MAEV,EAAE,IAAyB,aAAa,IAAI;EAEtE,OAAO;GACN,IAAI,OAAO,QAAQ;GACnB,OAAO;IACN,GAAG;IACH,MAAM,OAAO,QAAQ;IACrB,WAAW,OAAO,QAAQ;GAC3B;EACD;CACD;;;;CAKA,MAAM,OACL,IACA,OACA,MACuC;EAGvC,MAAM,IAFa,WAAW,KAAK,MAExB,EAAE,MAAM,aAAa,MAAM,EACrC,SAAS,EAAE,MAAM,KAAK,KAAK,EAC5B,CAAC;EAED,OAAO,EAAE,MAAM;GAAE,GAAG;GAAM,WAAW;EAAG,EAAE;CAC3C;;;;CAKA,MAAM,SAAwB;EAC7B,OAAO,IAAI,KACV,oEACD;CACD;;;;;CAMA,MAAM,KACL,KACA,MACA,MACqC;EACrC,MAAM,WAAqB,CAAC;EAC5B,MAAM,UAAoB,CAAC;EAE3B,IAAI,KAAK,SAAS,KAAK,MACtB,QAAQ,KAAK,MAAM;EAGpB,IAAI,KAAK,UAAU,KAAK,OACvB,SAAS,KAAK,OAAO;EAGtB,OAAO;GACN,SAAS,SAAS,SAAS,KAAK,QAAQ,SAAS;GACjD;GACA,qBAAqB;EACtB;CACD;AACD;;;;;;;;;;;;;;;;;;;;;;;AAwBA,IAAa,cAAb,cAAiC,OAAO,QAAQ,SAAS;;;;;;CASxD,YACC,MACA,MAUA,MACC;EACD,MACC,IAAI,oBAAoB,GACxB,MACA;GAAE,GAAG;GAAM,WAAW;EAAU,GAChC,IACD;CACD;AACD"}
@@ -183,7 +183,7 @@ var RailwayProjectProvider = class {
183
183
  async diff(_id, olds, news) {
184
184
  const replaces = [];
185
185
  const changes = [];
186
- if (olds.name !== news.name) replaces.push("name");
186
+ if (olds.name !== news.name) changes.push("name");
187
187
  if (olds.description !== news.description) changes.push("description");
188
188
  return {
189
189
  changes: replaces.length > 0 || changes.length > 0,
@@ -1 +1 @@
1
- {"version":3,"file":"project.cjs","names":["RailwayClient","pulumi"],"sources":["../../src/railway/project.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport { RailwayClient } from \"./client.js\";\n\n/** Resolved inputs for the Railway project dynamic provider. */\nexport interface RailwayProjectInputs {\n\t/** Railway API bearer token. */\n\ttoken: string;\n\n\t/** Desired display name for the project in Railway's dashboard. */\n\tname: string;\n\n\t/** Optional description shown in Railway's dashboard. */\n\tdescription?: string;\n}\n\n/** Persisted state for the Railway project. */\ninterface RailwayProjectOutputs extends RailwayProjectInputs {\n\t/** Railway-assigned project UUID. */\n\tprojectId: string;\n\n\t/** Railway-assigned production environment UUID. */\n\tproductionEnvironmentId: string;\n\n\t/** Railway project-scoped token (auto-provisioned, exposed as secret output). */\n\tprojectToken: string;\n}\n\nconst WORKSPACE_QUERY = `\n query {\n me {\n workspaces {\n id\n name\n projects {\n edges {\n node {\n id\n name\n }\n }\n }\n }\n }\n }\n`;\n\nconst PROJECT_CREATE = `\n mutation($input: ProjectCreateInput!) {\n projectCreate(input: $input) {\n id\n name\n }\n }\n`;\n\nconst PROJECT_UPDATE = `\n mutation($id: String!, $input: ProjectUpdateInput!) {\n projectUpdate(id: $id, input: $input) {\n id\n name\n }\n }\n`;\n\nconst PROJECT_ENVIRONMENTS_QUERY = `\n query($projectId: String!) {\n project(id: $projectId) {\n environments {\n edges {\n node {\n id\n name\n }\n }\n }\n }\n }\n`;\n\nconst PROJECT_TOKENS_QUERY = `\n query($projectId: String!) {\n projectTokens(projectId: $projectId) {\n edges { node { id name } }\n }\n }\n`;\n\nconst PROJECT_TOKEN_CREATE = `\n mutation($input: ProjectTokenCreateInput!) {\n projectTokenCreate(input: $input)\n }\n`;\n\nconst PROJECT_TOKEN_DELETE = `\n mutation($id: String!) { projectTokenDelete(id: $id) }\n`;\n\nconst PERMANENT_TOKEN_NAME = \"pulumi\";\n\n/**\n * Fetches all environments for a project and returns a name → UUID map.\n */\nasync function fetchProjectEnvironments(\n\tclient: RailwayClient,\n\tprojectId: string,\n): Promise<Record<string, string>> {\n\tconst result = await client.query<{\n\t\tproject: {\n\t\t\tenvironments: {\n\t\t\t\tedges: Array<{ node: { id: string; name: string } }>;\n\t\t\t};\n\t\t};\n\t}>(PROJECT_ENVIRONMENTS_QUERY, { projectId });\n\n\tconst environments: Record<string, string> = {};\n\n\tfor (const edge of result.project.environments.edges) {\n\t\tenvironments[edge.node.name] = edge.node.id;\n\t}\n\n\treturn environments;\n}\n\n/**\n * Gets or creates a permanent project-scoped token named \"pulumi\".\n *\n * Deletes any stale tokens with the same name before creating a new one,\n * ensuring a single canonical token exists. Does NOT write to Pulumi config —\n * the token is returned as a secret output for the consumer to manage.\n */\nasync function getOrCreateProjectToken(\n\tclient: RailwayClient,\n\tprojectId: string,\n\tenvironmentId?: string,\n): Promise<string> {\n\tconst tokensResult = await client.query<{\n\t\tprojectTokens: {\n\t\t\tedges: Array<{ node: { id: string; name: string } }>;\n\t\t};\n\t}>(PROJECT_TOKENS_QUERY, { projectId });\n\n\tconst stale = tokensResult.projectTokens.edges.filter(\n\t\t(edge) => edge.node.name === PERMANENT_TOKEN_NAME,\n\t);\n\n\tfor (const entry of stale) {\n\t\tawait client.query(PROJECT_TOKEN_DELETE, { id: entry.node.id });\n\t}\n\n\tconst result = await client.query<{ projectTokenCreate: string }>(\n\t\tPROJECT_TOKEN_CREATE,\n\t\t{\n\t\t\tinput: {\n\t\t\t\tprojectId,\n\t\t\t\tname: PERMANENT_TOKEN_NAME,\n\t\t\t\t...(environmentId ? { environmentId } : {}),\n\t\t\t},\n\t\t},\n\t);\n\n\treturn result.projectTokenCreate;\n}\n\n/**\n * Dynamic provider that adopts an existing Railway project by name, or creates one.\n *\n * On create:\n * 1. Queries workspaces to find the project by name.\n * 2. If found → adopts. If not → creates via `projectCreate`.\n * 3. Fetches all environments and resolves the production environment ID.\n * 4. Creates/reuses a project-scoped token named \"pulumi\".\n *\n * Deletion is a no-op (with a warning) to prevent accidental project removal.\n * Name changes trigger replacement.\n */\nclass RailwayProjectProvider implements pulumi.dynamic.ResourceProvider {\n\tasync create(\n\t\tinputs: RailwayProjectInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new RailwayClient(inputs.token);\n\n\t\tconst workspaceResult = await client.query<{\n\t\t\tme: {\n\t\t\t\tworkspaces: Array<{\n\t\t\t\t\tid: string;\n\t\t\t\t\tname: string;\n\t\t\t\t\tprojects: {\n\t\t\t\t\t\tedges: Array<{ node: { id: string; name: string } }>;\n\t\t\t\t\t};\n\t\t\t\t}>;\n\t\t\t};\n\t\t}>(WORKSPACE_QUERY);\n\n\t\tconst workspaces = workspaceResult.me.workspaces;\n\n\t\tif (workspaces.length === 0) {\n\t\t\tthrow new Error(\"No Railway workspace found — cannot create project\");\n\t\t}\n\n\t\tlet projectId: string | undefined;\n\n\t\tfor (const workspace of workspaces) {\n\t\t\tconst match = workspace.projects.edges.find(\n\t\t\t\t(edge) => edge.node.name === inputs.name,\n\t\t\t);\n\n\t\t\tif (match) {\n\t\t\t\tprojectId = match.node.id;\n\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tif (projectId) {\n\t\t\tpulumi.log.info(\n\t\t\t\t`Adopted existing Railway project \"${inputs.name}\" (${projectId})`,\n\t\t\t);\n\n\t\t\tif (inputs.description) {\n\t\t\t\tawait client.query(PROJECT_UPDATE, {\n\t\t\t\t\tid: projectId,\n\t\t\t\t\tinput: { description: inputs.description },\n\t\t\t\t});\n\t\t\t}\n\t\t} else {\n\t\t\tconst workspace = workspaces[0];\n\n\t\t\tconst created = await client.query<{\n\t\t\t\tprojectCreate: { id: string; name: string };\n\t\t\t}>(PROJECT_CREATE, {\n\t\t\t\tinput: {\n\t\t\t\t\tname: inputs.name,\n\t\t\t\t\tdescription: inputs.description,\n\t\t\t\t\tworkspaceId: workspace.id,\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tprojectId = created.projectCreate.id;\n\n\t\t\tpulumi.log.info(\n\t\t\t\t`Created Railway project \"${inputs.name}\" (${projectId})`,\n\t\t\t);\n\t\t}\n\n\t\tif (!projectId) {\n\t\t\tthrow new Error(\n\t\t\t\t`Failed to find or create Railway project \"${inputs.name}\"`,\n\t\t\t);\n\t\t}\n\n\t\tconst environments = await fetchProjectEnvironments(client, projectId);\n\n\t\tconst productionEnvironmentId = environments.production ?? \"\";\n\n\t\tconst projectToken = await getOrCreateProjectToken(\n\t\t\tclient,\n\t\t\tprojectId,\n\t\t\tproductionEnvironmentId || undefined,\n\t\t);\n\n\t\tconst outs: RailwayProjectOutputs = {\n\t\t\t...inputs,\n\t\t\tprojectId,\n\t\t\tproductionEnvironmentId,\n\t\t\tprojectToken,\n\t\t};\n\n\t\treturn { id: projectId, outs };\n\t}\n\n\tasync update(\n\t\tid: string,\n\t\t_olds: RailwayProjectOutputs,\n\t\tnews: RailwayProjectInputs,\n\t): Promise<pulumi.dynamic.UpdateResult> {\n\t\tconst client = new RailwayClient(news.token);\n\n\t\tawait client.query(PROJECT_UPDATE, {\n\t\t\tid,\n\t\t\tinput: { name: news.name, description: news.description },\n\t\t});\n\n\t\tconst environments = await fetchProjectEnvironments(client, id);\n\n\t\tconst productionEnvironmentId = environments.production ?? \"\";\n\n\t\tconst projectToken = await getOrCreateProjectToken(\n\t\t\tclient,\n\t\t\tid,\n\t\t\tproductionEnvironmentId || undefined,\n\t\t);\n\n\t\tconst outs: RailwayProjectOutputs = {\n\t\t\t...news,\n\t\t\tprojectId: id,\n\t\t\tproductionEnvironmentId,\n\t\t\tprojectToken,\n\t\t};\n\n\t\treturn { outs };\n\t}\n\n\tasync read(\n\t\tid: string,\n\t\tprops: RailwayProjectOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\tconst environments = await fetchProjectEnvironments(client, id);\n\n\t\tconst productionEnvironmentId =\n\t\t\tenvironments.production ?? props.productionEnvironmentId;\n\n\t\treturn {\n\t\t\tid,\n\t\t\tprops: { ...props, projectId: id, productionEnvironmentId },\n\t\t};\n\t}\n\n\tasync delete(): Promise<void> {\n\t\tpulumi.log.warn(\n\t\t\t\"Railway project deletion skipped — projects are not deleted by Pulumi\",\n\t\t);\n\t}\n\n\tasync diff(\n\t\t_id: string,\n\t\tolds: RailwayProjectOutputs,\n\t\tnews: RailwayProjectInputs,\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\treplaces.push(\"name\");\n\t\t}\n\n\t\tif (olds.description !== news.description) {\n\t\t\tchanges.push(\"description\");\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/**\n * Manages a Railway project with adopt-or-create semantics.\n *\n * Discovers or creates the project, resolves the production environment ID,\n * and provisions a project-scoped token named \"pulumi\" for CLI deploys.\n * The token is exposed as a secret output — the consumer decides how to store it.\n *\n * @example\n * ```typescript\n * const project = new RailwayProject(\"railway-project\", {\n * token: railwayConfig.token,\n * name: \"my-app\",\n * description: \"Railway services for my-app\",\n * });\n *\n * // Use outputs downstream\n * const serviceVar = new RailwayVariable(\"...\", {\n * projectId: project.projectId,\n * environmentId: project.productionEnvironmentId,\n * ...\n * });\n * ```\n */\nexport class RailwayProject extends pulumi.dynamic.Resource {\n\t/** Railway project UUID. */\n\tpublic declare readonly projectId: pulumi.Output<string>;\n\n\t/** Railway production environment UUID. */\n\tpublic declare readonly productionEnvironmentId: pulumi.Output<string>;\n\n\t/** Railway project-scoped token (secret). */\n\tpublic declare readonly projectToken: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\t/** Railway API bearer token. */\n\t\t\ttoken: pulumi.Input<string>;\n\n\t\t\t/** Project display name to find and adopt or create. */\n\t\t\tname: pulumi.Input<string>;\n\n\t\t\t/** Optional description shown in Railway's dashboard. */\n\t\t\tdescription?: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew RailwayProjectProvider(),\n\t\t\tname,\n\t\t\t{\n\t\t\t\t...args,\n\t\t\t\tprojectId: undefined,\n\t\t\t\tproductionEnvironmentId: undefined,\n\t\t\t\tprojectToken: pulumi.secret(undefined as unknown as string),\n\t\t\t},\n\t\t\topts,\n\t\t);\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"project.cjs","names":["RailwayClient","pulumi"],"sources":["../../src/railway/project.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport { RailwayClient } from \"./client.js\";\n\n/** Resolved inputs for the Railway project dynamic provider. */\nexport interface RailwayProjectInputs {\n\t/** Railway API bearer token. */\n\ttoken: string;\n\n\t/** Desired display name for the project in Railway's dashboard. */\n\tname: string;\n\n\t/** Optional description shown in Railway's dashboard. */\n\tdescription?: string;\n}\n\n/** Persisted state for the Railway project. */\ninterface RailwayProjectOutputs extends RailwayProjectInputs {\n\t/** Railway-assigned project UUID. */\n\tprojectId: string;\n\n\t/** Railway-assigned production environment UUID. */\n\tproductionEnvironmentId: string;\n\n\t/** Railway project-scoped token (auto-provisioned, exposed as secret output). */\n\tprojectToken: string;\n}\n\nconst WORKSPACE_QUERY = `\n query {\n me {\n workspaces {\n id\n name\n projects {\n edges {\n node {\n id\n name\n }\n }\n }\n }\n }\n }\n`;\n\nconst PROJECT_CREATE = `\n mutation($input: ProjectCreateInput!) {\n projectCreate(input: $input) {\n id\n name\n }\n }\n`;\n\nconst PROJECT_UPDATE = `\n mutation($id: String!, $input: ProjectUpdateInput!) {\n projectUpdate(id: $id, input: $input) {\n id\n name\n }\n }\n`;\n\nconst PROJECT_ENVIRONMENTS_QUERY = `\n query($projectId: String!) {\n project(id: $projectId) {\n environments {\n edges {\n node {\n id\n name\n }\n }\n }\n }\n }\n`;\n\nconst PROJECT_TOKENS_QUERY = `\n query($projectId: String!) {\n projectTokens(projectId: $projectId) {\n edges { node { id name } }\n }\n }\n`;\n\nconst PROJECT_TOKEN_CREATE = `\n mutation($input: ProjectTokenCreateInput!) {\n projectTokenCreate(input: $input)\n }\n`;\n\nconst PROJECT_TOKEN_DELETE = `\n mutation($id: String!) { projectTokenDelete(id: $id) }\n`;\n\nconst PERMANENT_TOKEN_NAME = \"pulumi\";\n\n/**\n * Fetches all environments for a project and returns a name → UUID map.\n */\nasync function fetchProjectEnvironments(\n\tclient: RailwayClient,\n\tprojectId: string,\n): Promise<Record<string, string>> {\n\tconst result = await client.query<{\n\t\tproject: {\n\t\t\tenvironments: {\n\t\t\t\tedges: Array<{ node: { id: string; name: string } }>;\n\t\t\t};\n\t\t};\n\t}>(PROJECT_ENVIRONMENTS_QUERY, { projectId });\n\n\tconst environments: Record<string, string> = {};\n\n\tfor (const edge of result.project.environments.edges) {\n\t\tenvironments[edge.node.name] = edge.node.id;\n\t}\n\n\treturn environments;\n}\n\n/**\n * Gets or creates a permanent project-scoped token named \"pulumi\".\n *\n * Deletes any stale tokens with the same name before creating a new one,\n * ensuring a single canonical token exists. Does NOT write to Pulumi config —\n * the token is returned as a secret output for the consumer to manage.\n */\nasync function getOrCreateProjectToken(\n\tclient: RailwayClient,\n\tprojectId: string,\n\tenvironmentId?: string,\n): Promise<string> {\n\tconst tokensResult = await client.query<{\n\t\tprojectTokens: {\n\t\t\tedges: Array<{ node: { id: string; name: string } }>;\n\t\t};\n\t}>(PROJECT_TOKENS_QUERY, { projectId });\n\n\tconst stale = tokensResult.projectTokens.edges.filter(\n\t\t(edge) => edge.node.name === PERMANENT_TOKEN_NAME,\n\t);\n\n\tfor (const entry of stale) {\n\t\tawait client.query(PROJECT_TOKEN_DELETE, { id: entry.node.id });\n\t}\n\n\tconst result = await client.query<{ projectTokenCreate: string }>(\n\t\tPROJECT_TOKEN_CREATE,\n\t\t{\n\t\t\tinput: {\n\t\t\t\tprojectId,\n\t\t\t\tname: PERMANENT_TOKEN_NAME,\n\t\t\t\t...(environmentId ? { environmentId } : {}),\n\t\t\t},\n\t\t},\n\t);\n\n\treturn result.projectTokenCreate;\n}\n\n/**\n * Dynamic provider that adopts an existing Railway project by name, or creates one.\n *\n * On create:\n * 1. Queries workspaces to find the project by name.\n * 2. If found → adopts. If not → creates via `projectCreate`.\n * 3. Fetches all environments and resolves the production environment ID.\n * 4. Creates/reuses a project-scoped token named \"pulumi\".\n *\n * Deletion is a no-op (with a warning) to prevent accidental project removal.\n * Name changes trigger replacement.\n */\nclass RailwayProjectProvider implements pulumi.dynamic.ResourceProvider {\n\tasync create(\n\t\tinputs: RailwayProjectInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new RailwayClient(inputs.token);\n\n\t\tconst workspaceResult = await client.query<{\n\t\t\tme: {\n\t\t\t\tworkspaces: Array<{\n\t\t\t\t\tid: string;\n\t\t\t\t\tname: string;\n\t\t\t\t\tprojects: {\n\t\t\t\t\t\tedges: Array<{ node: { id: string; name: string } }>;\n\t\t\t\t\t};\n\t\t\t\t}>;\n\t\t\t};\n\t\t}>(WORKSPACE_QUERY);\n\n\t\tconst workspaces = workspaceResult.me.workspaces;\n\n\t\tif (workspaces.length === 0) {\n\t\t\tthrow new Error(\"No Railway workspace found — cannot create project\");\n\t\t}\n\n\t\tlet projectId: string | undefined;\n\n\t\tfor (const workspace of workspaces) {\n\t\t\tconst match = workspace.projects.edges.find(\n\t\t\t\t(edge) => edge.node.name === inputs.name,\n\t\t\t);\n\n\t\t\tif (match) {\n\t\t\t\tprojectId = match.node.id;\n\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tif (projectId) {\n\t\t\tpulumi.log.info(\n\t\t\t\t`Adopted existing Railway project \"${inputs.name}\" (${projectId})`,\n\t\t\t);\n\n\t\t\tif (inputs.description) {\n\t\t\t\tawait client.query(PROJECT_UPDATE, {\n\t\t\t\t\tid: projectId,\n\t\t\t\t\tinput: { description: inputs.description },\n\t\t\t\t});\n\t\t\t}\n\t\t} else {\n\t\t\tconst workspace = workspaces[0];\n\n\t\t\tconst created = await client.query<{\n\t\t\t\tprojectCreate: { id: string; name: string };\n\t\t\t}>(PROJECT_CREATE, {\n\t\t\t\tinput: {\n\t\t\t\t\tname: inputs.name,\n\t\t\t\t\tdescription: inputs.description,\n\t\t\t\t\tworkspaceId: workspace.id,\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tprojectId = created.projectCreate.id;\n\n\t\t\tpulumi.log.info(\n\t\t\t\t`Created Railway project \"${inputs.name}\" (${projectId})`,\n\t\t\t);\n\t\t}\n\n\t\tif (!projectId) {\n\t\t\tthrow new Error(\n\t\t\t\t`Failed to find or create Railway project \"${inputs.name}\"`,\n\t\t\t);\n\t\t}\n\n\t\tconst environments = await fetchProjectEnvironments(client, projectId);\n\n\t\tconst productionEnvironmentId = environments.production ?? \"\";\n\n\t\tconst projectToken = await getOrCreateProjectToken(\n\t\t\tclient,\n\t\t\tprojectId,\n\t\t\tproductionEnvironmentId || undefined,\n\t\t);\n\n\t\tconst outs: RailwayProjectOutputs = {\n\t\t\t...inputs,\n\t\t\tprojectId,\n\t\t\tproductionEnvironmentId,\n\t\t\tprojectToken,\n\t\t};\n\n\t\treturn { id: projectId, outs };\n\t}\n\n\tasync update(\n\t\tid: string,\n\t\t_olds: RailwayProjectOutputs,\n\t\tnews: RailwayProjectInputs,\n\t): Promise<pulumi.dynamic.UpdateResult> {\n\t\tconst client = new RailwayClient(news.token);\n\n\t\tawait client.query(PROJECT_UPDATE, {\n\t\t\tid,\n\t\t\tinput: { name: news.name, description: news.description },\n\t\t});\n\n\t\tconst environments = await fetchProjectEnvironments(client, id);\n\n\t\tconst productionEnvironmentId = environments.production ?? \"\";\n\n\t\tconst projectToken = await getOrCreateProjectToken(\n\t\t\tclient,\n\t\t\tid,\n\t\t\tproductionEnvironmentId || undefined,\n\t\t);\n\n\t\tconst outs: RailwayProjectOutputs = {\n\t\t\t...news,\n\t\t\tprojectId: id,\n\t\t\tproductionEnvironmentId,\n\t\t\tprojectToken,\n\t\t};\n\n\t\treturn { outs };\n\t}\n\n\tasync read(\n\t\tid: string,\n\t\tprops: RailwayProjectOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\tconst environments = await fetchProjectEnvironments(client, id);\n\n\t\tconst productionEnvironmentId =\n\t\t\tenvironments.production ?? props.productionEnvironmentId;\n\n\t\treturn {\n\t\t\tid,\n\t\t\tprops: { ...props, projectId: id, productionEnvironmentId },\n\t\t};\n\t}\n\n\tasync delete(): Promise<void> {\n\t\tpulumi.log.warn(\n\t\t\t\"Railway project deletion skipped — projects are not deleted by Pulumi\",\n\t\t);\n\t}\n\n\tasync diff(\n\t\t_id: string,\n\t\tolds: RailwayProjectOutputs,\n\t\tnews: RailwayProjectInputs,\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.description !== news.description) {\n\t\t\tchanges.push(\"description\");\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/**\n * Manages a Railway project with adopt-or-create semantics.\n *\n * Discovers or creates the project, resolves the production environment ID,\n * and provisions a project-scoped token named \"pulumi\" for CLI deploys.\n * The token is exposed as a secret output — the consumer decides how to store it.\n *\n * @example\n * ```typescript\n * const project = new RailwayProject(\"railway-project\", {\n * token: railwayConfig.token,\n * name: \"my-app\",\n * description: \"Railway services for my-app\",\n * });\n *\n * // Use outputs downstream\n * const serviceVar = new RailwayVariable(\"...\", {\n * projectId: project.projectId,\n * environmentId: project.productionEnvironmentId,\n * ...\n * });\n * ```\n */\nexport class RailwayProject extends pulumi.dynamic.Resource {\n\t/** Railway project UUID. */\n\tpublic declare readonly projectId: pulumi.Output<string>;\n\n\t/** Railway production environment UUID. */\n\tpublic declare readonly productionEnvironmentId: pulumi.Output<string>;\n\n\t/** Railway project-scoped token (secret). */\n\tpublic declare readonly projectToken: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\t/** Railway API bearer token. */\n\t\t\ttoken: pulumi.Input<string>;\n\n\t\t\t/** Project display name to find and adopt or create. */\n\t\t\tname: pulumi.Input<string>;\n\n\t\t\t/** Optional description shown in Railway's dashboard. */\n\t\t\tdescription?: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew RailwayProjectProvider(),\n\t\t\tname,\n\t\t\t{\n\t\t\t\t...args,\n\t\t\t\tprojectId: undefined,\n\t\t\t\tproductionEnvironmentId: undefined,\n\t\t\t\tprojectToken: pulumi.secret(undefined as unknown as string),\n\t\t\t},\n\t\t\topts,\n\t\t);\n\t}\n}\n"],"mappings":"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"}
@@ -181,7 +181,7 @@ var RailwayProjectProvider = class {
181
181
  async diff(_id, olds, news) {
182
182
  const replaces = [];
183
183
  const changes = [];
184
- if (olds.name !== news.name) replaces.push("name");
184
+ if (olds.name !== news.name) changes.push("name");
185
185
  if (olds.description !== news.description) changes.push("description");
186
186
  return {
187
187
  changes: replaces.length > 0 || changes.length > 0,
@@ -1 +1 @@
1
- {"version":3,"file":"project.mjs","names":[],"sources":["../../src/railway/project.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport { RailwayClient } from \"./client.js\";\n\n/** Resolved inputs for the Railway project dynamic provider. */\nexport interface RailwayProjectInputs {\n\t/** Railway API bearer token. */\n\ttoken: string;\n\n\t/** Desired display name for the project in Railway's dashboard. */\n\tname: string;\n\n\t/** Optional description shown in Railway's dashboard. */\n\tdescription?: string;\n}\n\n/** Persisted state for the Railway project. */\ninterface RailwayProjectOutputs extends RailwayProjectInputs {\n\t/** Railway-assigned project UUID. */\n\tprojectId: string;\n\n\t/** Railway-assigned production environment UUID. */\n\tproductionEnvironmentId: string;\n\n\t/** Railway project-scoped token (auto-provisioned, exposed as secret output). */\n\tprojectToken: string;\n}\n\nconst WORKSPACE_QUERY = `\n query {\n me {\n workspaces {\n id\n name\n projects {\n edges {\n node {\n id\n name\n }\n }\n }\n }\n }\n }\n`;\n\nconst PROJECT_CREATE = `\n mutation($input: ProjectCreateInput!) {\n projectCreate(input: $input) {\n id\n name\n }\n }\n`;\n\nconst PROJECT_UPDATE = `\n mutation($id: String!, $input: ProjectUpdateInput!) {\n projectUpdate(id: $id, input: $input) {\n id\n name\n }\n }\n`;\n\nconst PROJECT_ENVIRONMENTS_QUERY = `\n query($projectId: String!) {\n project(id: $projectId) {\n environments {\n edges {\n node {\n id\n name\n }\n }\n }\n }\n }\n`;\n\nconst PROJECT_TOKENS_QUERY = `\n query($projectId: String!) {\n projectTokens(projectId: $projectId) {\n edges { node { id name } }\n }\n }\n`;\n\nconst PROJECT_TOKEN_CREATE = `\n mutation($input: ProjectTokenCreateInput!) {\n projectTokenCreate(input: $input)\n }\n`;\n\nconst PROJECT_TOKEN_DELETE = `\n mutation($id: String!) { projectTokenDelete(id: $id) }\n`;\n\nconst PERMANENT_TOKEN_NAME = \"pulumi\";\n\n/**\n * Fetches all environments for a project and returns a name → UUID map.\n */\nasync function fetchProjectEnvironments(\n\tclient: RailwayClient,\n\tprojectId: string,\n): Promise<Record<string, string>> {\n\tconst result = await client.query<{\n\t\tproject: {\n\t\t\tenvironments: {\n\t\t\t\tedges: Array<{ node: { id: string; name: string } }>;\n\t\t\t};\n\t\t};\n\t}>(PROJECT_ENVIRONMENTS_QUERY, { projectId });\n\n\tconst environments: Record<string, string> = {};\n\n\tfor (const edge of result.project.environments.edges) {\n\t\tenvironments[edge.node.name] = edge.node.id;\n\t}\n\n\treturn environments;\n}\n\n/**\n * Gets or creates a permanent project-scoped token named \"pulumi\".\n *\n * Deletes any stale tokens with the same name before creating a new one,\n * ensuring a single canonical token exists. Does NOT write to Pulumi config —\n * the token is returned as a secret output for the consumer to manage.\n */\nasync function getOrCreateProjectToken(\n\tclient: RailwayClient,\n\tprojectId: string,\n\tenvironmentId?: string,\n): Promise<string> {\n\tconst tokensResult = await client.query<{\n\t\tprojectTokens: {\n\t\t\tedges: Array<{ node: { id: string; name: string } }>;\n\t\t};\n\t}>(PROJECT_TOKENS_QUERY, { projectId });\n\n\tconst stale = tokensResult.projectTokens.edges.filter(\n\t\t(edge) => edge.node.name === PERMANENT_TOKEN_NAME,\n\t);\n\n\tfor (const entry of stale) {\n\t\tawait client.query(PROJECT_TOKEN_DELETE, { id: entry.node.id });\n\t}\n\n\tconst result = await client.query<{ projectTokenCreate: string }>(\n\t\tPROJECT_TOKEN_CREATE,\n\t\t{\n\t\t\tinput: {\n\t\t\t\tprojectId,\n\t\t\t\tname: PERMANENT_TOKEN_NAME,\n\t\t\t\t...(environmentId ? { environmentId } : {}),\n\t\t\t},\n\t\t},\n\t);\n\n\treturn result.projectTokenCreate;\n}\n\n/**\n * Dynamic provider that adopts an existing Railway project by name, or creates one.\n *\n * On create:\n * 1. Queries workspaces to find the project by name.\n * 2. If found → adopts. If not → creates via `projectCreate`.\n * 3. Fetches all environments and resolves the production environment ID.\n * 4. Creates/reuses a project-scoped token named \"pulumi\".\n *\n * Deletion is a no-op (with a warning) to prevent accidental project removal.\n * Name changes trigger replacement.\n */\nclass RailwayProjectProvider implements pulumi.dynamic.ResourceProvider {\n\tasync create(\n\t\tinputs: RailwayProjectInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new RailwayClient(inputs.token);\n\n\t\tconst workspaceResult = await client.query<{\n\t\t\tme: {\n\t\t\t\tworkspaces: Array<{\n\t\t\t\t\tid: string;\n\t\t\t\t\tname: string;\n\t\t\t\t\tprojects: {\n\t\t\t\t\t\tedges: Array<{ node: { id: string; name: string } }>;\n\t\t\t\t\t};\n\t\t\t\t}>;\n\t\t\t};\n\t\t}>(WORKSPACE_QUERY);\n\n\t\tconst workspaces = workspaceResult.me.workspaces;\n\n\t\tif (workspaces.length === 0) {\n\t\t\tthrow new Error(\"No Railway workspace found — cannot create project\");\n\t\t}\n\n\t\tlet projectId: string | undefined;\n\n\t\tfor (const workspace of workspaces) {\n\t\t\tconst match = workspace.projects.edges.find(\n\t\t\t\t(edge) => edge.node.name === inputs.name,\n\t\t\t);\n\n\t\t\tif (match) {\n\t\t\t\tprojectId = match.node.id;\n\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tif (projectId) {\n\t\t\tpulumi.log.info(\n\t\t\t\t`Adopted existing Railway project \"${inputs.name}\" (${projectId})`,\n\t\t\t);\n\n\t\t\tif (inputs.description) {\n\t\t\t\tawait client.query(PROJECT_UPDATE, {\n\t\t\t\t\tid: projectId,\n\t\t\t\t\tinput: { description: inputs.description },\n\t\t\t\t});\n\t\t\t}\n\t\t} else {\n\t\t\tconst workspace = workspaces[0];\n\n\t\t\tconst created = await client.query<{\n\t\t\t\tprojectCreate: { id: string; name: string };\n\t\t\t}>(PROJECT_CREATE, {\n\t\t\t\tinput: {\n\t\t\t\t\tname: inputs.name,\n\t\t\t\t\tdescription: inputs.description,\n\t\t\t\t\tworkspaceId: workspace.id,\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tprojectId = created.projectCreate.id;\n\n\t\t\tpulumi.log.info(\n\t\t\t\t`Created Railway project \"${inputs.name}\" (${projectId})`,\n\t\t\t);\n\t\t}\n\n\t\tif (!projectId) {\n\t\t\tthrow new Error(\n\t\t\t\t`Failed to find or create Railway project \"${inputs.name}\"`,\n\t\t\t);\n\t\t}\n\n\t\tconst environments = await fetchProjectEnvironments(client, projectId);\n\n\t\tconst productionEnvironmentId = environments.production ?? \"\";\n\n\t\tconst projectToken = await getOrCreateProjectToken(\n\t\t\tclient,\n\t\t\tprojectId,\n\t\t\tproductionEnvironmentId || undefined,\n\t\t);\n\n\t\tconst outs: RailwayProjectOutputs = {\n\t\t\t...inputs,\n\t\t\tprojectId,\n\t\t\tproductionEnvironmentId,\n\t\t\tprojectToken,\n\t\t};\n\n\t\treturn { id: projectId, outs };\n\t}\n\n\tasync update(\n\t\tid: string,\n\t\t_olds: RailwayProjectOutputs,\n\t\tnews: RailwayProjectInputs,\n\t): Promise<pulumi.dynamic.UpdateResult> {\n\t\tconst client = new RailwayClient(news.token);\n\n\t\tawait client.query(PROJECT_UPDATE, {\n\t\t\tid,\n\t\t\tinput: { name: news.name, description: news.description },\n\t\t});\n\n\t\tconst environments = await fetchProjectEnvironments(client, id);\n\n\t\tconst productionEnvironmentId = environments.production ?? \"\";\n\n\t\tconst projectToken = await getOrCreateProjectToken(\n\t\t\tclient,\n\t\t\tid,\n\t\t\tproductionEnvironmentId || undefined,\n\t\t);\n\n\t\tconst outs: RailwayProjectOutputs = {\n\t\t\t...news,\n\t\t\tprojectId: id,\n\t\t\tproductionEnvironmentId,\n\t\t\tprojectToken,\n\t\t};\n\n\t\treturn { outs };\n\t}\n\n\tasync read(\n\t\tid: string,\n\t\tprops: RailwayProjectOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\tconst environments = await fetchProjectEnvironments(client, id);\n\n\t\tconst productionEnvironmentId =\n\t\t\tenvironments.production ?? props.productionEnvironmentId;\n\n\t\treturn {\n\t\t\tid,\n\t\t\tprops: { ...props, projectId: id, productionEnvironmentId },\n\t\t};\n\t}\n\n\tasync delete(): Promise<void> {\n\t\tpulumi.log.warn(\n\t\t\t\"Railway project deletion skipped — projects are not deleted by Pulumi\",\n\t\t);\n\t}\n\n\tasync diff(\n\t\t_id: string,\n\t\tolds: RailwayProjectOutputs,\n\t\tnews: RailwayProjectInputs,\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\treplaces.push(\"name\");\n\t\t}\n\n\t\tif (olds.description !== news.description) {\n\t\t\tchanges.push(\"description\");\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/**\n * Manages a Railway project with adopt-or-create semantics.\n *\n * Discovers or creates the project, resolves the production environment ID,\n * and provisions a project-scoped token named \"pulumi\" for CLI deploys.\n * The token is exposed as a secret output — the consumer decides how to store it.\n *\n * @example\n * ```typescript\n * const project = new RailwayProject(\"railway-project\", {\n * token: railwayConfig.token,\n * name: \"my-app\",\n * description: \"Railway services for my-app\",\n * });\n *\n * // Use outputs downstream\n * const serviceVar = new RailwayVariable(\"...\", {\n * projectId: project.projectId,\n * environmentId: project.productionEnvironmentId,\n * ...\n * });\n * ```\n */\nexport class RailwayProject extends pulumi.dynamic.Resource {\n\t/** Railway project UUID. */\n\tpublic declare readonly projectId: pulumi.Output<string>;\n\n\t/** Railway production environment UUID. */\n\tpublic declare readonly productionEnvironmentId: pulumi.Output<string>;\n\n\t/** Railway project-scoped token (secret). */\n\tpublic declare readonly projectToken: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\t/** Railway API bearer token. */\n\t\t\ttoken: pulumi.Input<string>;\n\n\t\t\t/** Project display name to find and adopt or create. */\n\t\t\tname: pulumi.Input<string>;\n\n\t\t\t/** Optional description shown in Railway's dashboard. */\n\t\t\tdescription?: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew RailwayProjectProvider(),\n\t\t\tname,\n\t\t\t{\n\t\t\t\t...args,\n\t\t\t\tprojectId: undefined,\n\t\t\t\tproductionEnvironmentId: undefined,\n\t\t\t\tprojectToken: pulumi.secret(undefined as unknown as string),\n\t\t\t},\n\t\t\topts,\n\t\t);\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"project.mjs","names":[],"sources":["../../src/railway/project.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport { RailwayClient } from \"./client.js\";\n\n/** Resolved inputs for the Railway project dynamic provider. */\nexport interface RailwayProjectInputs {\n\t/** Railway API bearer token. */\n\ttoken: string;\n\n\t/** Desired display name for the project in Railway's dashboard. */\n\tname: string;\n\n\t/** Optional description shown in Railway's dashboard. */\n\tdescription?: string;\n}\n\n/** Persisted state for the Railway project. */\ninterface RailwayProjectOutputs extends RailwayProjectInputs {\n\t/** Railway-assigned project UUID. */\n\tprojectId: string;\n\n\t/** Railway-assigned production environment UUID. */\n\tproductionEnvironmentId: string;\n\n\t/** Railway project-scoped token (auto-provisioned, exposed as secret output). */\n\tprojectToken: string;\n}\n\nconst WORKSPACE_QUERY = `\n query {\n me {\n workspaces {\n id\n name\n projects {\n edges {\n node {\n id\n name\n }\n }\n }\n }\n }\n }\n`;\n\nconst PROJECT_CREATE = `\n mutation($input: ProjectCreateInput!) {\n projectCreate(input: $input) {\n id\n name\n }\n }\n`;\n\nconst PROJECT_UPDATE = `\n mutation($id: String!, $input: ProjectUpdateInput!) {\n projectUpdate(id: $id, input: $input) {\n id\n name\n }\n }\n`;\n\nconst PROJECT_ENVIRONMENTS_QUERY = `\n query($projectId: String!) {\n project(id: $projectId) {\n environments {\n edges {\n node {\n id\n name\n }\n }\n }\n }\n }\n`;\n\nconst PROJECT_TOKENS_QUERY = `\n query($projectId: String!) {\n projectTokens(projectId: $projectId) {\n edges { node { id name } }\n }\n }\n`;\n\nconst PROJECT_TOKEN_CREATE = `\n mutation($input: ProjectTokenCreateInput!) {\n projectTokenCreate(input: $input)\n }\n`;\n\nconst PROJECT_TOKEN_DELETE = `\n mutation($id: String!) { projectTokenDelete(id: $id) }\n`;\n\nconst PERMANENT_TOKEN_NAME = \"pulumi\";\n\n/**\n * Fetches all environments for a project and returns a name → UUID map.\n */\nasync function fetchProjectEnvironments(\n\tclient: RailwayClient,\n\tprojectId: string,\n): Promise<Record<string, string>> {\n\tconst result = await client.query<{\n\t\tproject: {\n\t\t\tenvironments: {\n\t\t\t\tedges: Array<{ node: { id: string; name: string } }>;\n\t\t\t};\n\t\t};\n\t}>(PROJECT_ENVIRONMENTS_QUERY, { projectId });\n\n\tconst environments: Record<string, string> = {};\n\n\tfor (const edge of result.project.environments.edges) {\n\t\tenvironments[edge.node.name] = edge.node.id;\n\t}\n\n\treturn environments;\n}\n\n/**\n * Gets or creates a permanent project-scoped token named \"pulumi\".\n *\n * Deletes any stale tokens with the same name before creating a new one,\n * ensuring a single canonical token exists. Does NOT write to Pulumi config —\n * the token is returned as a secret output for the consumer to manage.\n */\nasync function getOrCreateProjectToken(\n\tclient: RailwayClient,\n\tprojectId: string,\n\tenvironmentId?: string,\n): Promise<string> {\n\tconst tokensResult = await client.query<{\n\t\tprojectTokens: {\n\t\t\tedges: Array<{ node: { id: string; name: string } }>;\n\t\t};\n\t}>(PROJECT_TOKENS_QUERY, { projectId });\n\n\tconst stale = tokensResult.projectTokens.edges.filter(\n\t\t(edge) => edge.node.name === PERMANENT_TOKEN_NAME,\n\t);\n\n\tfor (const entry of stale) {\n\t\tawait client.query(PROJECT_TOKEN_DELETE, { id: entry.node.id });\n\t}\n\n\tconst result = await client.query<{ projectTokenCreate: string }>(\n\t\tPROJECT_TOKEN_CREATE,\n\t\t{\n\t\t\tinput: {\n\t\t\t\tprojectId,\n\t\t\t\tname: PERMANENT_TOKEN_NAME,\n\t\t\t\t...(environmentId ? { environmentId } : {}),\n\t\t\t},\n\t\t},\n\t);\n\n\treturn result.projectTokenCreate;\n}\n\n/**\n * Dynamic provider that adopts an existing Railway project by name, or creates one.\n *\n * On create:\n * 1. Queries workspaces to find the project by name.\n * 2. If found → adopts. If not → creates via `projectCreate`.\n * 3. Fetches all environments and resolves the production environment ID.\n * 4. Creates/reuses a project-scoped token named \"pulumi\".\n *\n * Deletion is a no-op (with a warning) to prevent accidental project removal.\n * Name changes trigger replacement.\n */\nclass RailwayProjectProvider implements pulumi.dynamic.ResourceProvider {\n\tasync create(\n\t\tinputs: RailwayProjectInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new RailwayClient(inputs.token);\n\n\t\tconst workspaceResult = await client.query<{\n\t\t\tme: {\n\t\t\t\tworkspaces: Array<{\n\t\t\t\t\tid: string;\n\t\t\t\t\tname: string;\n\t\t\t\t\tprojects: {\n\t\t\t\t\t\tedges: Array<{ node: { id: string; name: string } }>;\n\t\t\t\t\t};\n\t\t\t\t}>;\n\t\t\t};\n\t\t}>(WORKSPACE_QUERY);\n\n\t\tconst workspaces = workspaceResult.me.workspaces;\n\n\t\tif (workspaces.length === 0) {\n\t\t\tthrow new Error(\"No Railway workspace found — cannot create project\");\n\t\t}\n\n\t\tlet projectId: string | undefined;\n\n\t\tfor (const workspace of workspaces) {\n\t\t\tconst match = workspace.projects.edges.find(\n\t\t\t\t(edge) => edge.node.name === inputs.name,\n\t\t\t);\n\n\t\t\tif (match) {\n\t\t\t\tprojectId = match.node.id;\n\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tif (projectId) {\n\t\t\tpulumi.log.info(\n\t\t\t\t`Adopted existing Railway project \"${inputs.name}\" (${projectId})`,\n\t\t\t);\n\n\t\t\tif (inputs.description) {\n\t\t\t\tawait client.query(PROJECT_UPDATE, {\n\t\t\t\t\tid: projectId,\n\t\t\t\t\tinput: { description: inputs.description },\n\t\t\t\t});\n\t\t\t}\n\t\t} else {\n\t\t\tconst workspace = workspaces[0];\n\n\t\t\tconst created = await client.query<{\n\t\t\t\tprojectCreate: { id: string; name: string };\n\t\t\t}>(PROJECT_CREATE, {\n\t\t\t\tinput: {\n\t\t\t\t\tname: inputs.name,\n\t\t\t\t\tdescription: inputs.description,\n\t\t\t\t\tworkspaceId: workspace.id,\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tprojectId = created.projectCreate.id;\n\n\t\t\tpulumi.log.info(\n\t\t\t\t`Created Railway project \"${inputs.name}\" (${projectId})`,\n\t\t\t);\n\t\t}\n\n\t\tif (!projectId) {\n\t\t\tthrow new Error(\n\t\t\t\t`Failed to find or create Railway project \"${inputs.name}\"`,\n\t\t\t);\n\t\t}\n\n\t\tconst environments = await fetchProjectEnvironments(client, projectId);\n\n\t\tconst productionEnvironmentId = environments.production ?? \"\";\n\n\t\tconst projectToken = await getOrCreateProjectToken(\n\t\t\tclient,\n\t\t\tprojectId,\n\t\t\tproductionEnvironmentId || undefined,\n\t\t);\n\n\t\tconst outs: RailwayProjectOutputs = {\n\t\t\t...inputs,\n\t\t\tprojectId,\n\t\t\tproductionEnvironmentId,\n\t\t\tprojectToken,\n\t\t};\n\n\t\treturn { id: projectId, outs };\n\t}\n\n\tasync update(\n\t\tid: string,\n\t\t_olds: RailwayProjectOutputs,\n\t\tnews: RailwayProjectInputs,\n\t): Promise<pulumi.dynamic.UpdateResult> {\n\t\tconst client = new RailwayClient(news.token);\n\n\t\tawait client.query(PROJECT_UPDATE, {\n\t\t\tid,\n\t\t\tinput: { name: news.name, description: news.description },\n\t\t});\n\n\t\tconst environments = await fetchProjectEnvironments(client, id);\n\n\t\tconst productionEnvironmentId = environments.production ?? \"\";\n\n\t\tconst projectToken = await getOrCreateProjectToken(\n\t\t\tclient,\n\t\t\tid,\n\t\t\tproductionEnvironmentId || undefined,\n\t\t);\n\n\t\tconst outs: RailwayProjectOutputs = {\n\t\t\t...news,\n\t\t\tprojectId: id,\n\t\t\tproductionEnvironmentId,\n\t\t\tprojectToken,\n\t\t};\n\n\t\treturn { outs };\n\t}\n\n\tasync read(\n\t\tid: string,\n\t\tprops: RailwayProjectOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\tconst environments = await fetchProjectEnvironments(client, id);\n\n\t\tconst productionEnvironmentId =\n\t\t\tenvironments.production ?? props.productionEnvironmentId;\n\n\t\treturn {\n\t\t\tid,\n\t\t\tprops: { ...props, projectId: id, productionEnvironmentId },\n\t\t};\n\t}\n\n\tasync delete(): Promise<void> {\n\t\tpulumi.log.warn(\n\t\t\t\"Railway project deletion skipped — projects are not deleted by Pulumi\",\n\t\t);\n\t}\n\n\tasync diff(\n\t\t_id: string,\n\t\tolds: RailwayProjectOutputs,\n\t\tnews: RailwayProjectInputs,\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.description !== news.description) {\n\t\t\tchanges.push(\"description\");\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/**\n * Manages a Railway project with adopt-or-create semantics.\n *\n * Discovers or creates the project, resolves the production environment ID,\n * and provisions a project-scoped token named \"pulumi\" for CLI deploys.\n * The token is exposed as a secret output — the consumer decides how to store it.\n *\n * @example\n * ```typescript\n * const project = new RailwayProject(\"railway-project\", {\n * token: railwayConfig.token,\n * name: \"my-app\",\n * description: \"Railway services for my-app\",\n * });\n *\n * // Use outputs downstream\n * const serviceVar = new RailwayVariable(\"...\", {\n * projectId: project.projectId,\n * environmentId: project.productionEnvironmentId,\n * ...\n * });\n * ```\n */\nexport class RailwayProject extends pulumi.dynamic.Resource {\n\t/** Railway project UUID. */\n\tpublic declare readonly projectId: pulumi.Output<string>;\n\n\t/** Railway production environment UUID. */\n\tpublic declare readonly productionEnvironmentId: pulumi.Output<string>;\n\n\t/** Railway project-scoped token (secret). */\n\tpublic declare readonly projectToken: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\t/** Railway API bearer token. */\n\t\t\ttoken: pulumi.Input<string>;\n\n\t\t\t/** Project display name to find and adopt or create. */\n\t\t\tname: pulumi.Input<string>;\n\n\t\t\t/** Optional description shown in Railway's dashboard. */\n\t\t\tdescription?: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew RailwayProjectProvider(),\n\t\t\tname,\n\t\t\t{\n\t\t\t\t...args,\n\t\t\t\tprojectId: undefined,\n\t\t\t\tproductionEnvironmentId: undefined,\n\t\t\t\tprojectToken: pulumi.secret(undefined as unknown as string),\n\t\t\t},\n\t\t\topts,\n\t\t);\n\t}\n}\n"],"mappings":"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"}
@@ -149,11 +149,14 @@ var RailwayServiceProvider = class {
149
149
  * @param _olds Previous persisted state
150
150
  * @param news New desired configuration
151
151
  */
152
- async update(id, _olds, news) {
152
+ async update(id, olds, news) {
153
153
  const client = new require_railway_client.RailwayClient(news.token);
154
- if (news.icon) await client.query(SERVICE_UPDATE, {
154
+ const updateInput = {};
155
+ if (olds.name !== news.name) updateInput.name = news.name;
156
+ if (news.icon && olds.icon !== news.icon) updateInput.icon = news.icon;
157
+ if (Object.keys(updateInput).length > 0) await client.query(SERVICE_UPDATE, {
155
158
  id,
156
- input: { icon: news.icon }
159
+ input: updateInput
157
160
  });
158
161
  await applyInstanceConfig(client, id, news.environmentId, news);
159
162
  return { outs: {
@@ -200,8 +203,8 @@ var RailwayServiceProvider = class {
200
203
  /**
201
204
  * Compares old and new inputs to determine what changed.
202
205
  *
203
- * Name, projectId, and environmentId changes trigger replacement.
204
- * Builder, commands, and healthcheck changes trigger in-place update.
206
+ * ProjectId and environmentId changes trigger replacement.
207
+ * Name, builder, commands, and healthcheck changes trigger in-place update.
205
208
  *
206
209
  * @param _id Current resource ID (unused)
207
210
  * @param olds Previous persisted state
@@ -210,7 +213,7 @@ var RailwayServiceProvider = class {
210
213
  async diff(_id, olds, news) {
211
214
  const replaces = [];
212
215
  const changes = [];
213
- if (olds.name !== news.name) replaces.push("name");
216
+ if (olds.name !== news.name) changes.push("name");
214
217
  if (olds.projectId !== news.projectId) replaces.push("projectId");
215
218
  if (olds.environmentId !== news.environmentId) replaces.push("environmentId");
216
219
  if (olds.builder !== news.builder) changes.push("builder");
@@ -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.js\";\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. */\nexport interface 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: `\"RAILPACK\"`, `\"NIXPACKS\"`, or `\"DOCKERFILE\"`. */\n\tbuilder?: string;\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: `\"ON_FAILURE\"`, `\"ALWAYS\"`, or `\"NEVER\"`. */\n\trestartPolicyType?: string;\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_DELETE = `\n mutation($id: String!) { serviceDelete(id: $id) }\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 */\nclass RailwayServiceProvider implements pulumi.dynamic.ResourceProvider {\n\t/**\n\t * Creates or adopts a Railway service by name, then applies instance config.\n\t *\n\t * @param inputs Resolved service configuration\n\t * @returns The Railway service UUID as the resource ID\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\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\t * @param id Railway service UUID\n\t * @param _olds Previous persisted state\n\t * @param news New desired configuration\n\t */\n\tasync update(\n\t\tid: string,\n\t\t_olds: RailwayServiceOutputs,\n\t\tnews: RailwayServiceInputs,\n\t): Promise<pulumi.dynamic.UpdateResult> {\n\t\tconst client = new RailwayClient(news.token);\n\n\t\tif (news.icon) {\n\t\t\tawait client.query(SERVICE_UPDATE, {\n\t\t\t\tid,\n\t\t\t\tinput: { icon: news.icon },\n\t\t\t});\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\t * @param id Railway service UUID\n\t * @param props Last known persisted state\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 * Deletes the Railway service. Silently succeeds if already deleted.\n\t *\n\t * @param id Railway service UUID to delete\n\t * @param props Last known persisted state (used for token and logging)\n\t */\n\tasync delete(id: string, props: RailwayServiceOutputs): Promise<void> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\ttry {\n\t\t\tawait client.query(SERVICE_DELETE, { id });\n\n\t\t\tpulumi.log.info(`Deleted Railway service \"${props.name}\" (${id})`);\n\t\t} catch {\n\t\t\tpulumi.log.warn(\n\t\t\t\t`Failed to delete Railway service \"${props.name}\" (${id}) — may already be deleted`,\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Compares old and new inputs to determine what changed.\n\t *\n\t * Name, projectId, and environmentId changes trigger replacement.\n\t * Builder, commands, and healthcheck changes trigger in-place update.\n\t *\n\t * @param _id Current resource ID (unused)\n\t * @param olds Previous persisted state\n\t * @param news New desired configuration\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\treplaces.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/**\n * Manages a Railway service with adopt-or-create semantics.\n *\n * Queries existing services by project ID and name before creating new ones.\n * Service instance configuration (builder, start command, healthcheck) is\n * applied after creation via `serviceInstanceUpdate`.\n *\n * @example\n * ```typescript\n * const service = new RailwayService(\"railway-service-api\", {\n * token: project.projectToken,\n * projectId: project.projectId,\n * environmentId: project.productionEnvironmentId,\n * name: \"@my-app/api\",\n * builder: \"RAILPACK\",\n * startCommand: \"node dist/index.js\",\n * healthcheckPath: \"/health\",\n * });\n *\n * // Use serviceId downstream\n * new RailwayVariable(\"railway-variable-api\", {\n * serviceId: service.serviceId,\n * ...\n * });\n * ```\n */\nexport class RailwayService extends pulumi.dynamic.Resource {\n\t/** Railway service UUID. */\n\tpublic declare readonly serviceId: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\t/** Railway API bearer token. */\n\t\t\ttoken: pulumi.Input<string>;\n\n\t\t\t/** Railway project UUID. */\n\t\t\tprojectId: pulumi.Input<string>;\n\n\t\t\t/** Railway environment UUID (e.g. production). */\n\t\t\tenvironmentId: pulumi.Input<string>;\n\n\t\t\t/** Human-readable service name used for adopt-or-create matching. */\n\t\t\tname: pulumi.Input<string>;\n\n\t\t\t/** SVG icon URL displayed in the Railway dashboard. */\n\t\t\ticon?: pulumi.Input<string>;\n\n\t\t\t/** Docker image source for image-based services. */\n\t\t\tsource?: pulumi.Input<{ image: pulumi.Input<string> }>;\n\n\t\t\t/** Build system: `\"RAILPACK\"`, `\"NIXPACKS\"`, or `\"DOCKERFILE\"`. */\n\t\t\tbuilder?: pulumi.Input<string>;\n\n\t\t\t/** Shell command executed during the build phase. */\n\t\t\tbuildCommand?: pulumi.Input<string>;\n\n\t\t\t/** Shell command executed to start the service at runtime. */\n\t\t\tstartCommand?: pulumi.Input<string>;\n\n\t\t\t/** Restart behavior: `\"ON_FAILURE\"`, `\"ALWAYS\"`, or `\"NEVER\"`. */\n\t\t\trestartPolicyType?: pulumi.Input<string>;\n\n\t\t\t/** HTTP path polled for health checks (e.g. `\"/health-check\"`). */\n\t\t\thealthcheckPath?: pulumi.Input<string>;\n\n\t\t\t/** Seconds to wait for a healthy response before marking unhealthy. */\n\t\t\thealthcheckTimeout?: pulumi.Input<number>;\n\n\t\t\t/** Shell command executed before the main deploy (e.g. migrations). */\n\t\t\tpreDeployCommand?: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew RailwayServiceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, serviceId: undefined },\n\t\t\topts,\n\t\t);\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.js\";\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. */\nexport interface 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: `\"RAILPACK\"`, `\"NIXPACKS\"`, or `\"DOCKERFILE\"`. */\n\tbuilder?: string;\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: `\"ON_FAILURE\"`, `\"ALWAYS\"`, or `\"NEVER\"`. */\n\trestartPolicyType?: string;\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_DELETE = `\n mutation($id: String!) { serviceDelete(id: $id) }\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 */\nclass RailwayServiceProvider implements pulumi.dynamic.ResourceProvider {\n\t/**\n\t * Creates or adopts a Railway service by name, then applies instance config.\n\t *\n\t * @param inputs Resolved service configuration\n\t * @returns The Railway service UUID as the resource ID\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\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\t * @param id Railway service UUID\n\t * @param _olds Previous persisted state\n\t * @param news New desired 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\t * @param id Railway service UUID\n\t * @param props Last known persisted state\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 * Deletes the Railway service. Silently succeeds if already deleted.\n\t *\n\t * @param id Railway service UUID to delete\n\t * @param props Last known persisted state (used for token and logging)\n\t */\n\tasync delete(id: string, props: RailwayServiceOutputs): Promise<void> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\ttry {\n\t\t\tawait client.query(SERVICE_DELETE, { id });\n\n\t\t\tpulumi.log.info(`Deleted Railway service \"${props.name}\" (${id})`);\n\t\t} catch {\n\t\t\tpulumi.log.warn(\n\t\t\t\t`Failed to delete Railway service \"${props.name}\" (${id}) — may already be deleted`,\n\t\t\t);\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 * Name, builder, commands, and healthcheck changes trigger in-place update.\n\t *\n\t * @param _id Current resource ID (unused)\n\t * @param olds Previous persisted state\n\t * @param news New desired configuration\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/**\n * Manages a Railway service with adopt-or-create semantics.\n *\n * Queries existing services by project ID and name before creating new ones.\n * Service instance configuration (builder, start command, healthcheck) is\n * applied after creation via `serviceInstanceUpdate`.\n *\n * @example\n * ```typescript\n * const service = new RailwayService(\"railway-service-api\", {\n * token: project.projectToken,\n * projectId: project.projectId,\n * environmentId: project.productionEnvironmentId,\n * name: \"@my-app/api\",\n * builder: \"RAILPACK\",\n * startCommand: \"node dist/index.js\",\n * healthcheckPath: \"/health\",\n * });\n *\n * // Use serviceId downstream\n * new RailwayVariable(\"railway-variable-api\", {\n * serviceId: service.serviceId,\n * ...\n * });\n * ```\n */\nexport class RailwayService extends pulumi.dynamic.Resource {\n\t/** Railway service UUID. */\n\tpublic declare readonly serviceId: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\t/** Railway API bearer token. */\n\t\t\ttoken: pulumi.Input<string>;\n\n\t\t\t/** Railway project UUID. */\n\t\t\tprojectId: pulumi.Input<string>;\n\n\t\t\t/** Railway environment UUID (e.g. production). */\n\t\t\tenvironmentId: pulumi.Input<string>;\n\n\t\t\t/** Human-readable service name used for adopt-or-create matching. */\n\t\t\tname: pulumi.Input<string>;\n\n\t\t\t/** SVG icon URL displayed in the Railway dashboard. */\n\t\t\ticon?: pulumi.Input<string>;\n\n\t\t\t/** Docker image source for image-based services. */\n\t\t\tsource?: pulumi.Input<{ image: pulumi.Input<string> }>;\n\n\t\t\t/** Build system: `\"RAILPACK\"`, `\"NIXPACKS\"`, or `\"DOCKERFILE\"`. */\n\t\t\tbuilder?: pulumi.Input<string>;\n\n\t\t\t/** Shell command executed during the build phase. */\n\t\t\tbuildCommand?: pulumi.Input<string>;\n\n\t\t\t/** Shell command executed to start the service at runtime. */\n\t\t\tstartCommand?: pulumi.Input<string>;\n\n\t\t\t/** Restart behavior: `\"ON_FAILURE\"`, `\"ALWAYS\"`, or `\"NEVER\"`. */\n\t\t\trestartPolicyType?: pulumi.Input<string>;\n\n\t\t\t/** HTTP path polled for health checks (e.g. `\"/health-check\"`). */\n\t\t\thealthcheckPath?: pulumi.Input<string>;\n\n\t\t\t/** Seconds to wait for a healthy response before marking unhealthy. */\n\t\t\thealthcheckTimeout?: pulumi.Input<number>;\n\n\t\t\t/** Shell command executed before the main deploy (e.g. migrations). */\n\t\t\tpreDeployCommand?: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew RailwayServiceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, serviceId: undefined },\n\t\t\topts,\n\t\t);\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":";;;;;UAIU,oBAAA;;EAET,KAAK;AAAA;;UAIW,oBAAA;EAJX;EAML,KAAA;EAFoC;EAKpC,SAAA;EAY6B;EAT7B,aAAA;EAHA;EAMA,IAAA;EAAA;EAGA,IAAA;EAGA;EAAA,MAAA,GAAS,oBAAoB;EAG7B;EAAA,OAAA;EAMA;EAHA,YAAA;EASA;EANA,YAAA;EAYA;EATA,iBAAA;EASgB;EANhB,eAAA;EA0Y2B;EAvY3B,kBAAA;EAyYmC;EAtYnC,gBAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;cAoYY,cAAA,SAAuB,MAAA,CAAO,OAAA,CAAQ,QAAA;EAWhD;EAAA,SATsB,SAAA,EAAW,MAAA,CAAO,MAAA;cAGzC,IAAA,UACA,IAAA;IAQC,gCANA,KAAA,EAAO,MAAA,CAAO,KAAA,UAMQ;IAHtB,SAAA,EAAW,MAAA,CAAO,KAAA,UAMZ;IAHN,aAAA,EAAe,MAAA,CAAO,KAAA,UAMtB;IAHA,IAAA,EAAM,MAAA,CAAO,KAAA,UAGC;IAAd,IAAA,GAAO,MAAA,CAAO,KAAA,UAGL;IAAT,MAAA,GAAS,MAAA,CAAO,KAAA;MAAQ,KAAA,EAAO,MAAA,CAAO,KAAA;IAAA,IAAA;IAGtC,OAAA,GAAU,MAAA,CAAO,KAAA,UAAP;IAGV,YAAA,GAAe,MAAA,CAAO,KAAA,UAAtB;IAGA,YAAA,GAAe,MAAA,CAAO,KAAA,UAHA;IAMtB,iBAAA,GAAoB,MAAA,CAAO,KAAA,UAHZ;IAMf,eAAA,GAAkB,MAAA,CAAO,KAAA,UAHzB;IAMA,kBAAA,GAAqB,MAAA,CAAO,KAAA,UAND;IAS3B,gBAAA,GAAmB,MAAA,CAAO,KAAA;EAAA,GAE3B,IAAA,GAAO,MAAA,CAAO,qBAAA;AAAA"}
1
+ {"version":3,"file":"service.d.cts","names":[],"sources":["../../src/railway/service.ts"],"mappings":";;;;;UAIU,oBAAA;;EAET,KAAK;AAAA;;UAIW,oBAAA;EAJX;EAML,KAAA;EAFoC;EAKpC,SAAA;EAY6B;EAT7B,aAAA;EAHA;EAMA,IAAA;EAAA;EAGA,IAAA;EAGA;EAAA,MAAA,GAAS,oBAAoB;EAG7B;EAAA,OAAA;EAMA;EAHA,YAAA;EASA;EANA,YAAA;EAYA;EATA,iBAAA;EASgB;EANhB,eAAA;EAiZ2B;EA9Y3B,kBAAA;EAgZmC;EA7YnC,gBAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;cA2YY,cAAA,SAAuB,MAAA,CAAO,OAAA,CAAQ,QAAA;EAWhD;EAAA,SATsB,SAAA,EAAW,MAAA,CAAO,MAAA;cAGzC,IAAA,UACA,IAAA;IAQC,gCANA,KAAA,EAAO,MAAA,CAAO,KAAA,UAMQ;IAHtB,SAAA,EAAW,MAAA,CAAO,KAAA,UAMZ;IAHN,aAAA,EAAe,MAAA,CAAO,KAAA,UAMtB;IAHA,IAAA,EAAM,MAAA,CAAO,KAAA,UAGC;IAAd,IAAA,GAAO,MAAA,CAAO,KAAA,UAGL;IAAT,MAAA,GAAS,MAAA,CAAO,KAAA;MAAQ,KAAA,EAAO,MAAA,CAAO,KAAA;IAAA,IAAA;IAGtC,OAAA,GAAU,MAAA,CAAO,KAAA,UAAP;IAGV,YAAA,GAAe,MAAA,CAAO,KAAA,UAAtB;IAGA,YAAA,GAAe,MAAA,CAAO,KAAA,UAHA;IAMtB,iBAAA,GAAoB,MAAA,CAAO,KAAA,UAHZ;IAMf,eAAA,GAAkB,MAAA,CAAO,KAAA,UAHzB;IAMA,kBAAA,GAAqB,MAAA,CAAO,KAAA,UAND;IAS3B,gBAAA,GAAmB,MAAA,CAAO,KAAA;EAAA,GAE3B,IAAA,GAAO,MAAA,CAAO,qBAAA;AAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.mts","names":[],"sources":["../../src/railway/service.ts"],"mappings":";;;;;UAIU,oBAAA;;EAET,KAAK;AAAA;;UAIW,oBAAA;EAJX;EAML,KAAA;EAFoC;EAKpC,SAAA;EAY6B;EAT7B,aAAA;EAHA;EAMA,IAAA;EAAA;EAGA,IAAA;EAGA;EAAA,MAAA,GAAS,oBAAoB;EAG7B;EAAA,OAAA;EAMA;EAHA,YAAA;EASA;EANA,YAAA;EAYA;EATA,iBAAA;EASgB;EANhB,eAAA;EA0Y2B;EAvY3B,kBAAA;EAyYmC;EAtYnC,gBAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;cAoYY,cAAA,SAAuB,MAAA,CAAO,OAAA,CAAQ,QAAA;EAWhD;EAAA,SATsB,SAAA,EAAW,MAAA,CAAO,MAAA;cAGzC,IAAA,UACA,IAAA;IAQC,gCANA,KAAA,EAAO,MAAA,CAAO,KAAA,UAMQ;IAHtB,SAAA,EAAW,MAAA,CAAO,KAAA,UAMZ;IAHN,aAAA,EAAe,MAAA,CAAO,KAAA,UAMtB;IAHA,IAAA,EAAM,MAAA,CAAO,KAAA,UAGC;IAAd,IAAA,GAAO,MAAA,CAAO,KAAA,UAGL;IAAT,MAAA,GAAS,MAAA,CAAO,KAAA;MAAQ,KAAA,EAAO,MAAA,CAAO,KAAA;IAAA,IAAA;IAGtC,OAAA,GAAU,MAAA,CAAO,KAAA,UAAP;IAGV,YAAA,GAAe,MAAA,CAAO,KAAA,UAAtB;IAGA,YAAA,GAAe,MAAA,CAAO,KAAA,UAHA;IAMtB,iBAAA,GAAoB,MAAA,CAAO,KAAA,UAHZ;IAMf,eAAA,GAAkB,MAAA,CAAO,KAAA,UAHzB;IAMA,kBAAA,GAAqB,MAAA,CAAO,KAAA,UAND;IAS3B,gBAAA,GAAmB,MAAA,CAAO,KAAA;EAAA,GAE3B,IAAA,GAAO,MAAA,CAAO,qBAAA;AAAA"}
1
+ {"version":3,"file":"service.d.mts","names":[],"sources":["../../src/railway/service.ts"],"mappings":";;;;;UAIU,oBAAA;;EAET,KAAK;AAAA;;UAIW,oBAAA;EAJX;EAML,KAAA;EAFoC;EAKpC,SAAA;EAY6B;EAT7B,aAAA;EAHA;EAMA,IAAA;EAAA;EAGA,IAAA;EAGA;EAAA,MAAA,GAAS,oBAAoB;EAG7B;EAAA,OAAA;EAMA;EAHA,YAAA;EASA;EANA,YAAA;EAYA;EATA,iBAAA;EASgB;EANhB,eAAA;EAiZ2B;EA9Y3B,kBAAA;EAgZmC;EA7YnC,gBAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;cA2YY,cAAA,SAAuB,MAAA,CAAO,OAAA,CAAQ,QAAA;EAWhD;EAAA,SATsB,SAAA,EAAW,MAAA,CAAO,MAAA;cAGzC,IAAA,UACA,IAAA;IAQC,gCANA,KAAA,EAAO,MAAA,CAAO,KAAA,UAMQ;IAHtB,SAAA,EAAW,MAAA,CAAO,KAAA,UAMZ;IAHN,aAAA,EAAe,MAAA,CAAO,KAAA,UAMtB;IAHA,IAAA,EAAM,MAAA,CAAO,KAAA,UAGC;IAAd,IAAA,GAAO,MAAA,CAAO,KAAA,UAGL;IAAT,MAAA,GAAS,MAAA,CAAO,KAAA;MAAQ,KAAA,EAAO,MAAA,CAAO,KAAA;IAAA,IAAA;IAGtC,OAAA,GAAU,MAAA,CAAO,KAAA,UAAP;IAGV,YAAA,GAAe,MAAA,CAAO,KAAA,UAAtB;IAGA,YAAA,GAAe,MAAA,CAAO,KAAA,UAHA;IAMtB,iBAAA,GAAoB,MAAA,CAAO,KAAA,UAHZ;IAMf,eAAA,GAAkB,MAAA,CAAO,KAAA,UAHzB;IAMA,kBAAA,GAAqB,MAAA,CAAO,KAAA,UAND;IAS3B,gBAAA,GAAmB,MAAA,CAAO,KAAA;EAAA,GAE3B,IAAA,GAAO,MAAA,CAAO,qBAAA;AAAA"}
@@ -147,11 +147,14 @@ var RailwayServiceProvider = class {
147
147
  * @param _olds Previous persisted state
148
148
  * @param news New desired configuration
149
149
  */
150
- async update(id, _olds, news) {
150
+ async update(id, olds, news) {
151
151
  const client = new RailwayClient(news.token);
152
- if (news.icon) await client.query(SERVICE_UPDATE, {
152
+ const updateInput = {};
153
+ if (olds.name !== news.name) updateInput.name = news.name;
154
+ if (news.icon && olds.icon !== news.icon) updateInput.icon = news.icon;
155
+ if (Object.keys(updateInput).length > 0) await client.query(SERVICE_UPDATE, {
153
156
  id,
154
- input: { icon: news.icon }
157
+ input: updateInput
155
158
  });
156
159
  await applyInstanceConfig(client, id, news.environmentId, news);
157
160
  return { outs: {
@@ -198,8 +201,8 @@ var RailwayServiceProvider = class {
198
201
  /**
199
202
  * Compares old and new inputs to determine what changed.
200
203
  *
201
- * Name, projectId, and environmentId changes trigger replacement.
202
- * Builder, commands, and healthcheck changes trigger in-place update.
204
+ * ProjectId and environmentId changes trigger replacement.
205
+ * Name, builder, commands, and healthcheck changes trigger in-place update.
203
206
  *
204
207
  * @param _id Current resource ID (unused)
205
208
  * @param olds Previous persisted state
@@ -208,7 +211,7 @@ var RailwayServiceProvider = class {
208
211
  async diff(_id, olds, news) {
209
212
  const replaces = [];
210
213
  const changes = [];
211
- if (olds.name !== news.name) replaces.push("name");
214
+ if (olds.name !== news.name) changes.push("name");
212
215
  if (olds.projectId !== news.projectId) replaces.push("projectId");
213
216
  if (olds.environmentId !== news.environmentId) replaces.push("environmentId");
214
217
  if (olds.builder !== news.builder) changes.push("builder");
@@ -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.js\";\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. */\nexport interface 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: `\"RAILPACK\"`, `\"NIXPACKS\"`, or `\"DOCKERFILE\"`. */\n\tbuilder?: string;\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: `\"ON_FAILURE\"`, `\"ALWAYS\"`, or `\"NEVER\"`. */\n\trestartPolicyType?: string;\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_DELETE = `\n mutation($id: String!) { serviceDelete(id: $id) }\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 */\nclass RailwayServiceProvider implements pulumi.dynamic.ResourceProvider {\n\t/**\n\t * Creates or adopts a Railway service by name, then applies instance config.\n\t *\n\t * @param inputs Resolved service configuration\n\t * @returns The Railway service UUID as the resource ID\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\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\t * @param id Railway service UUID\n\t * @param _olds Previous persisted state\n\t * @param news New desired configuration\n\t */\n\tasync update(\n\t\tid: string,\n\t\t_olds: RailwayServiceOutputs,\n\t\tnews: RailwayServiceInputs,\n\t): Promise<pulumi.dynamic.UpdateResult> {\n\t\tconst client = new RailwayClient(news.token);\n\n\t\tif (news.icon) {\n\t\t\tawait client.query(SERVICE_UPDATE, {\n\t\t\t\tid,\n\t\t\t\tinput: { icon: news.icon },\n\t\t\t});\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\t * @param id Railway service UUID\n\t * @param props Last known persisted state\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 * Deletes the Railway service. Silently succeeds if already deleted.\n\t *\n\t * @param id Railway service UUID to delete\n\t * @param props Last known persisted state (used for token and logging)\n\t */\n\tasync delete(id: string, props: RailwayServiceOutputs): Promise<void> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\ttry {\n\t\t\tawait client.query(SERVICE_DELETE, { id });\n\n\t\t\tpulumi.log.info(`Deleted Railway service \"${props.name}\" (${id})`);\n\t\t} catch {\n\t\t\tpulumi.log.warn(\n\t\t\t\t`Failed to delete Railway service \"${props.name}\" (${id}) — may already be deleted`,\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Compares old and new inputs to determine what changed.\n\t *\n\t * Name, projectId, and environmentId changes trigger replacement.\n\t * Builder, commands, and healthcheck changes trigger in-place update.\n\t *\n\t * @param _id Current resource ID (unused)\n\t * @param olds Previous persisted state\n\t * @param news New desired configuration\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\treplaces.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/**\n * Manages a Railway service with adopt-or-create semantics.\n *\n * Queries existing services by project ID and name before creating new ones.\n * Service instance configuration (builder, start command, healthcheck) is\n * applied after creation via `serviceInstanceUpdate`.\n *\n * @example\n * ```typescript\n * const service = new RailwayService(\"railway-service-api\", {\n * token: project.projectToken,\n * projectId: project.projectId,\n * environmentId: project.productionEnvironmentId,\n * name: \"@my-app/api\",\n * builder: \"RAILPACK\",\n * startCommand: \"node dist/index.js\",\n * healthcheckPath: \"/health\",\n * });\n *\n * // Use serviceId downstream\n * new RailwayVariable(\"railway-variable-api\", {\n * serviceId: service.serviceId,\n * ...\n * });\n * ```\n */\nexport class RailwayService extends pulumi.dynamic.Resource {\n\t/** Railway service UUID. */\n\tpublic declare readonly serviceId: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\t/** Railway API bearer token. */\n\t\t\ttoken: pulumi.Input<string>;\n\n\t\t\t/** Railway project UUID. */\n\t\t\tprojectId: pulumi.Input<string>;\n\n\t\t\t/** Railway environment UUID (e.g. production). */\n\t\t\tenvironmentId: pulumi.Input<string>;\n\n\t\t\t/** Human-readable service name used for adopt-or-create matching. */\n\t\t\tname: pulumi.Input<string>;\n\n\t\t\t/** SVG icon URL displayed in the Railway dashboard. */\n\t\t\ticon?: pulumi.Input<string>;\n\n\t\t\t/** Docker image source for image-based services. */\n\t\t\tsource?: pulumi.Input<{ image: pulumi.Input<string> }>;\n\n\t\t\t/** Build system: `\"RAILPACK\"`, `\"NIXPACKS\"`, or `\"DOCKERFILE\"`. */\n\t\t\tbuilder?: pulumi.Input<string>;\n\n\t\t\t/** Shell command executed during the build phase. */\n\t\t\tbuildCommand?: pulumi.Input<string>;\n\n\t\t\t/** Shell command executed to start the service at runtime. */\n\t\t\tstartCommand?: pulumi.Input<string>;\n\n\t\t\t/** Restart behavior: `\"ON_FAILURE\"`, `\"ALWAYS\"`, or `\"NEVER\"`. */\n\t\t\trestartPolicyType?: pulumi.Input<string>;\n\n\t\t\t/** HTTP path polled for health checks (e.g. `\"/health-check\"`). */\n\t\t\thealthcheckPath?: pulumi.Input<string>;\n\n\t\t\t/** Seconds to wait for a healthy response before marking unhealthy. */\n\t\t\thealthcheckTimeout?: pulumi.Input<number>;\n\n\t\t\t/** Shell command executed before the main deploy (e.g. migrations). */\n\t\t\tpreDeployCommand?: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew RailwayServiceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, serviceId: undefined },\n\t\t\topts,\n\t\t);\n\t}\n}\n"],"mappings":";;;;;AAyDA,MAAM,iBAAiB;;;;;;;;;;;;;;AAevB,MAAM,gBAAgB;;;;;;;;AAStB,MAAM,iBAAiB;;;;;;;;AASvB,MAAM,iBAAiB;;;;;;;;AASvB,MAAM,0BAA0B;;;;;;;;;;;;;AAchC,MAAM,kBAAkB;;;;;;;AAQxB,MAAM,iBAAiB;;;;;;;AAQvB,eAAe,oBACd,QACA,WACA,eACA,QACgB;CAChB,MAAM,gBAAyC,CAAC;CAEhD,IAAI,OAAO,SACV,cAAc,UAAU,OAAO;CAGhC,IAAI,OAAO,cACV,cAAc,eAAe,OAAO;CAGrC,IAAI,OAAO,cACV,cAAc,eAAe,OAAO;CAGrC,IAAI,OAAO,mBACV,cAAc,oBAAoB,OAAO;CAG1C,IAAI,OAAO,iBACV,cAAc,kBAAkB,OAAO;CAGxC,IAAI,OAAO,oBACV,cAAc,qBAAqB,OAAO;CAG3C,IAAI,OAAO,kBACV,cAAc,mBAAmB,OAAO;CAGzC,IAAI,OAAO,KAAK,aAAa,EAAE,WAAW,GACzC;CAGD,IAAI;EACH,MAAM,OAAO,MAAM,yBAAyB;GAC3C;GACA;GACA,OAAO;EACR,CAAC;CACF,SAAS,OAAO;EACf,OAAO,IAAI,KACV,sEAAsE,OACvE;EAEA,OAAO,cAAc;EACrB,OAAO,cAAc;EAErB,IAAI,OAAO,KAAK,aAAa,EAAE,SAAS,GACvC,MAAM,OAAO,MAAM,yBAAyB;GAC3C;GACA;GACA,OAAO;EACR,CAAC;CAEH;AACD;;;;;;;;AASA,IAAM,yBAAN,MAAwE;;;;;;;CAOvE,MAAM,OACL,QACuC;EACvC,MAAM,SAAS,IAAI,cAAc,OAAO,KAAK;EAQ7C,IAAI,aAAY,MANK,OAAO,MAIzB,gBAAgB,EAAE,WAAW,OAAO,UAAU,CAAC,GAE3B,QAAQ,SAAS,MAAM,MAC5C,SAAS,KAAK,KAAK,SAAS,OAAO,IACrC,GAAG,KAAK;EAER,IAAI,WACH,OAAO,IAAI,KACV,qCAAqC,OAAO,KAAK,KAAK,UAAU,EACjE;OACM;GACN,MAAM,cAAuC;IAC5C,WAAW,OAAO;IAClB,MAAM,OAAO;GACd;GAEA,IAAI,OAAO,QACV,YAAY,SAAS,EAAE,OAAO,OAAO,OAAO,MAAM;GAOnD,aAAY,MAJU,OAAO,MAE1B,gBAAgB,EAAE,OAAO,YAAY,CAAC,GAErB,cAAc;GAElC,OAAO,IAAI,KACV,4BAA4B,OAAO,KAAK,KAAK,UAAU,EACxD;GAEA,IAAI,OAAO,QACV,MAAM,OAAO,MAAM,iBAAiB;IACnC,IAAI;IACJ,OAAO,EAAE,OAAO,OAAO,OAAO,MAAM;GACrC,CAAC;GAGF,IAAI,OAAO,MACV,MAAM,OAAO,MAAM,gBAAgB;IAClC,IAAI;IACJ,OAAO,EAAE,MAAM,OAAO,KAAK;GAC5B,CAAC;EAEH;EAEA,MAAM,oBAAoB,QAAQ,WAAW,OAAO,eAAe,MAAM;EAEzE,MAAM,OAA8B;GAAE,GAAG;GAAQ;EAAU;EAE3D,OAAO;GAAE,IAAI;GAAW;EAAK;CAC9B;;;;;;;;CASA,MAAM,OACL,IACA,OACA,MACuC;EACvC,MAAM,SAAS,IAAI,cAAc,KAAK,KAAK;EAE3C,IAAI,KAAK,MACR,MAAM,OAAO,MAAM,gBAAgB;GAClC;GACA,OAAO,EAAE,MAAM,KAAK,KAAK;EAC1B,CAAC;EAGF,MAAM,oBAAoB,QAAQ,IAAI,KAAK,eAAe,IAAI;EAI9D,OAAO,EAAE;GAF6B,GAAG;GAAM,WAAW;EAE9C,EAAE;CACf;;;;;;;CAQA,MAAM,KACL,IACA,OACqC;EACrC,MAAM,SAAS,IAAI,cAAc,MAAM,KAAK;EAE5C,IAAI;GACH,MAAM,OAAO,MACZ,eACA,EAAE,WAAW,GAAG,CACjB;EACD,QAAQ;GACP,MAAM,IAAI,MAAM,oBAAoB,MAAM,KAAK,KAAK,GAAG,YAAY;EACpE;EAEA,OAAO;GAAE;GAAI,OAAO;IAAE,GAAG;IAAO,WAAW;GAAG;EAAE;CACjD;;;;;;;CAQA,MAAM,OAAO,IAAY,OAA6C;EACrE,MAAM,SAAS,IAAI,cAAc,MAAM,KAAK;EAE5C,IAAI;GACH,MAAM,OAAO,MAAM,gBAAgB,EAAE,GAAG,CAAC;GAEzC,OAAO,IAAI,KAAK,4BAA4B,MAAM,KAAK,KAAK,GAAG,EAAE;EAClE,QAAQ;GACP,OAAO,IAAI,KACV,qCAAqC,MAAM,KAAK,KAAK,GAAG,2BACzD;EACD;CACD;;;;;;;;;;;CAYA,MAAM,KACL,KACA,MACA,MACqC;EACrC,MAAM,WAAqB,CAAC;EAC5B,MAAM,UAAoB,CAAC;EAE3B,IAAI,KAAK,SAAS,KAAK,MACtB,SAAS,KAAK,MAAM;EAGrB,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,IAAI,KAAK,kBAAkB,KAAK,eAC/B,SAAS,KAAK,eAAe;EAG9B,IAAI,KAAK,YAAY,KAAK,SACzB,QAAQ,KAAK,SAAS;EAGvB,IAAI,KAAK,iBAAiB,KAAK,cAC9B,QAAQ,KAAK,cAAc;EAG5B,IAAI,KAAK,iBAAiB,KAAK,cAC9B,QAAQ,KAAK,cAAc;EAG5B,IAAI,KAAK,sBAAsB,KAAK,mBACnC,QAAQ,KAAK,mBAAmB;EAGjC,IAAI,KAAK,oBAAoB,KAAK,iBACjC,QAAQ,KAAK,iBAAiB;EAG/B,IAAI,KAAK,uBAAuB,KAAK,oBACpC,QAAQ,KAAK,oBAAoB;EAGlC,IAAI,KAAK,qBAAqB,KAAK,kBAClC,QAAQ,KAAK,kBAAkB;EAGhC,IAAI,KAAK,SAAS,KAAK,MACtB,QAAQ,KAAK,MAAM;EAGpB,OAAO;GACN,SAAS,SAAS,SAAS,KAAK,QAAQ,SAAS;GACjD;GACA,qBAAqB;EACtB;CACD;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,IAAa,iBAAb,cAAoC,OAAO,QAAQ,SAAS;CAI3D,YACC,MACA,MAwCA,MACC;EACD,MACC,IAAI,uBAAuB,GAC3B,MACA;GAAE,GAAG;GAAM,WAAW;EAAU,GAChC,IACD;CACD;AACD"}
1
+ {"version":3,"file":"service.mjs","names":[],"sources":["../../src/railway/service.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport { RailwayClient } from \"./client.js\";\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. */\nexport interface 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: `\"RAILPACK\"`, `\"NIXPACKS\"`, or `\"DOCKERFILE\"`. */\n\tbuilder?: string;\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: `\"ON_FAILURE\"`, `\"ALWAYS\"`, or `\"NEVER\"`. */\n\trestartPolicyType?: string;\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_DELETE = `\n mutation($id: String!) { serviceDelete(id: $id) }\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 */\nclass RailwayServiceProvider implements pulumi.dynamic.ResourceProvider {\n\t/**\n\t * Creates or adopts a Railway service by name, then applies instance config.\n\t *\n\t * @param inputs Resolved service configuration\n\t * @returns The Railway service UUID as the resource ID\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\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\t * @param id Railway service UUID\n\t * @param _olds Previous persisted state\n\t * @param news New desired 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\t * @param id Railway service UUID\n\t * @param props Last known persisted state\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 * Deletes the Railway service. Silently succeeds if already deleted.\n\t *\n\t * @param id Railway service UUID to delete\n\t * @param props Last known persisted state (used for token and logging)\n\t */\n\tasync delete(id: string, props: RailwayServiceOutputs): Promise<void> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\ttry {\n\t\t\tawait client.query(SERVICE_DELETE, { id });\n\n\t\t\tpulumi.log.info(`Deleted Railway service \"${props.name}\" (${id})`);\n\t\t} catch {\n\t\t\tpulumi.log.warn(\n\t\t\t\t`Failed to delete Railway service \"${props.name}\" (${id}) — may already be deleted`,\n\t\t\t);\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 * Name, builder, commands, and healthcheck changes trigger in-place update.\n\t *\n\t * @param _id Current resource ID (unused)\n\t * @param olds Previous persisted state\n\t * @param news New desired configuration\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/**\n * Manages a Railway service with adopt-or-create semantics.\n *\n * Queries existing services by project ID and name before creating new ones.\n * Service instance configuration (builder, start command, healthcheck) is\n * applied after creation via `serviceInstanceUpdate`.\n *\n * @example\n * ```typescript\n * const service = new RailwayService(\"railway-service-api\", {\n * token: project.projectToken,\n * projectId: project.projectId,\n * environmentId: project.productionEnvironmentId,\n * name: \"@my-app/api\",\n * builder: \"RAILPACK\",\n * startCommand: \"node dist/index.js\",\n * healthcheckPath: \"/health\",\n * });\n *\n * // Use serviceId downstream\n * new RailwayVariable(\"railway-variable-api\", {\n * serviceId: service.serviceId,\n * ...\n * });\n * ```\n */\nexport class RailwayService extends pulumi.dynamic.Resource {\n\t/** Railway service UUID. */\n\tpublic declare readonly serviceId: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\t/** Railway API bearer token. */\n\t\t\ttoken: pulumi.Input<string>;\n\n\t\t\t/** Railway project UUID. */\n\t\t\tprojectId: pulumi.Input<string>;\n\n\t\t\t/** Railway environment UUID (e.g. production). */\n\t\t\tenvironmentId: pulumi.Input<string>;\n\n\t\t\t/** Human-readable service name used for adopt-or-create matching. */\n\t\t\tname: pulumi.Input<string>;\n\n\t\t\t/** SVG icon URL displayed in the Railway dashboard. */\n\t\t\ticon?: pulumi.Input<string>;\n\n\t\t\t/** Docker image source for image-based services. */\n\t\t\tsource?: pulumi.Input<{ image: pulumi.Input<string> }>;\n\n\t\t\t/** Build system: `\"RAILPACK\"`, `\"NIXPACKS\"`, or `\"DOCKERFILE\"`. */\n\t\t\tbuilder?: pulumi.Input<string>;\n\n\t\t\t/** Shell command executed during the build phase. */\n\t\t\tbuildCommand?: pulumi.Input<string>;\n\n\t\t\t/** Shell command executed to start the service at runtime. */\n\t\t\tstartCommand?: pulumi.Input<string>;\n\n\t\t\t/** Restart behavior: `\"ON_FAILURE\"`, `\"ALWAYS\"`, or `\"NEVER\"`. */\n\t\t\trestartPolicyType?: pulumi.Input<string>;\n\n\t\t\t/** HTTP path polled for health checks (e.g. `\"/health-check\"`). */\n\t\t\thealthcheckPath?: pulumi.Input<string>;\n\n\t\t\t/** Seconds to wait for a healthy response before marking unhealthy. */\n\t\t\thealthcheckTimeout?: pulumi.Input<number>;\n\n\t\t\t/** Shell command executed before the main deploy (e.g. migrations). */\n\t\t\tpreDeployCommand?: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew RailwayServiceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, serviceId: undefined },\n\t\t\topts,\n\t\t);\n\t}\n}\n"],"mappings":";;;;;AAyDA,MAAM,iBAAiB;;;;;;;;;;;;;;AAevB,MAAM,gBAAgB;;;;;;;;AAStB,MAAM,iBAAiB;;;;;;;;AASvB,MAAM,iBAAiB;;;;;;;;AASvB,MAAM,0BAA0B;;;;;;;;;;;;;AAchC,MAAM,kBAAkB;;;;;;;AAQxB,MAAM,iBAAiB;;;;;;;AAQvB,eAAe,oBACd,QACA,WACA,eACA,QACgB;CAChB,MAAM,gBAAyC,CAAC;CAEhD,IAAI,OAAO,SACV,cAAc,UAAU,OAAO;CAGhC,IAAI,OAAO,cACV,cAAc,eAAe,OAAO;CAGrC,IAAI,OAAO,cACV,cAAc,eAAe,OAAO;CAGrC,IAAI,OAAO,mBACV,cAAc,oBAAoB,OAAO;CAG1C,IAAI,OAAO,iBACV,cAAc,kBAAkB,OAAO;CAGxC,IAAI,OAAO,oBACV,cAAc,qBAAqB,OAAO;CAG3C,IAAI,OAAO,kBACV,cAAc,mBAAmB,OAAO;CAGzC,IAAI,OAAO,KAAK,aAAa,EAAE,WAAW,GACzC;CAGD,IAAI;EACH,MAAM,OAAO,MAAM,yBAAyB;GAC3C;GACA;GACA,OAAO;EACR,CAAC;CACF,SAAS,OAAO;EACf,OAAO,IAAI,KACV,sEAAsE,OACvE;EAEA,OAAO,cAAc;EACrB,OAAO,cAAc;EAErB,IAAI,OAAO,KAAK,aAAa,EAAE,SAAS,GACvC,MAAM,OAAO,MAAM,yBAAyB;GAC3C;GACA;GACA,OAAO;EACR,CAAC;CAEH;AACD;;;;;;;;AASA,IAAM,yBAAN,MAAwE;;;;;;;CAOvE,MAAM,OACL,QACuC;EACvC,MAAM,SAAS,IAAI,cAAc,OAAO,KAAK;EAQ7C,IAAI,aAAY,MANK,OAAO,MAIzB,gBAAgB,EAAE,WAAW,OAAO,UAAU,CAAC,GAE3B,QAAQ,SAAS,MAAM,MAC5C,SAAS,KAAK,KAAK,SAAS,OAAO,IACrC,GAAG,KAAK;EAER,IAAI,WACH,OAAO,IAAI,KACV,qCAAqC,OAAO,KAAK,KAAK,UAAU,EACjE;OACM;GACN,MAAM,cAAuC;IAC5C,WAAW,OAAO;IAClB,MAAM,OAAO;GACd;GAEA,IAAI,OAAO,QACV,YAAY,SAAS,EAAE,OAAO,OAAO,OAAO,MAAM;GAOnD,aAAY,MAJU,OAAO,MAE1B,gBAAgB,EAAE,OAAO,YAAY,CAAC,GAErB,cAAc;GAElC,OAAO,IAAI,KACV,4BAA4B,OAAO,KAAK,KAAK,UAAU,EACxD;GAEA,IAAI,OAAO,QACV,MAAM,OAAO,MAAM,iBAAiB;IACnC,IAAI;IACJ,OAAO,EAAE,OAAO,OAAO,OAAO,MAAM;GACrC,CAAC;GAGF,IAAI,OAAO,MACV,MAAM,OAAO,MAAM,gBAAgB;IAClC,IAAI;IACJ,OAAO,EAAE,MAAM,OAAO,KAAK;GAC5B,CAAC;EAEH;EAEA,MAAM,oBAAoB,QAAQ,WAAW,OAAO,eAAe,MAAM;EAEzE,MAAM,OAA8B;GAAE,GAAG;GAAQ;EAAU;EAE3D,OAAO;GAAE,IAAI;GAAW;EAAK;CAC9B;;;;;;;;CASA,MAAM,OACL,IACA,MACA,MACuC;EACvC,MAAM,SAAS,IAAI,cAAc,KAAK,KAAK;EAE3C,MAAM,cAAuC,CAAC;EAE9C,IAAI,KAAK,SAAS,KAAK,MACtB,YAAY,OAAO,KAAK;EAGzB,IAAI,KAAK,QAAQ,KAAK,SAAS,KAAK,MACnC,YAAY,OAAO,KAAK;EAGzB,IAAI,OAAO,KAAK,WAAW,EAAE,SAAS,GACrC,MAAM,OAAO,MAAM,gBAAgB;GAAE;GAAI,OAAO;EAAY,CAAC;EAG9D,MAAM,oBAAoB,QAAQ,IAAI,KAAK,eAAe,IAAI;EAI9D,OAAO,EAAE;GAF6B,GAAG;GAAM,WAAW;EAE9C,EAAE;CACf;;;;;;;CAQA,MAAM,KACL,IACA,OACqC;EACrC,MAAM,SAAS,IAAI,cAAc,MAAM,KAAK;EAE5C,IAAI;GACH,MAAM,OAAO,MACZ,eACA,EAAE,WAAW,GAAG,CACjB;EACD,QAAQ;GACP,MAAM,IAAI,MAAM,oBAAoB,MAAM,KAAK,KAAK,GAAG,YAAY;EACpE;EAEA,OAAO;GAAE;GAAI,OAAO;IAAE,GAAG;IAAO,WAAW;GAAG;EAAE;CACjD;;;;;;;CAQA,MAAM,OAAO,IAAY,OAA6C;EACrE,MAAM,SAAS,IAAI,cAAc,MAAM,KAAK;EAE5C,IAAI;GACH,MAAM,OAAO,MAAM,gBAAgB,EAAE,GAAG,CAAC;GAEzC,OAAO,IAAI,KAAK,4BAA4B,MAAM,KAAK,KAAK,GAAG,EAAE;EAClE,QAAQ;GACP,OAAO,IAAI,KACV,qCAAqC,MAAM,KAAK,KAAK,GAAG,2BACzD;EACD;CACD;;;;;;;;;;;CAYA,MAAM,KACL,KACA,MACA,MACqC;EACrC,MAAM,WAAqB,CAAC;EAC5B,MAAM,UAAoB,CAAC;EAE3B,IAAI,KAAK,SAAS,KAAK,MACtB,QAAQ,KAAK,MAAM;EAGpB,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,IAAI,KAAK,kBAAkB,KAAK,eAC/B,SAAS,KAAK,eAAe;EAG9B,IAAI,KAAK,YAAY,KAAK,SACzB,QAAQ,KAAK,SAAS;EAGvB,IAAI,KAAK,iBAAiB,KAAK,cAC9B,QAAQ,KAAK,cAAc;EAG5B,IAAI,KAAK,iBAAiB,KAAK,cAC9B,QAAQ,KAAK,cAAc;EAG5B,IAAI,KAAK,sBAAsB,KAAK,mBACnC,QAAQ,KAAK,mBAAmB;EAGjC,IAAI,KAAK,oBAAoB,KAAK,iBACjC,QAAQ,KAAK,iBAAiB;EAG/B,IAAI,KAAK,uBAAuB,KAAK,oBACpC,QAAQ,KAAK,oBAAoB;EAGlC,IAAI,KAAK,qBAAqB,KAAK,kBAClC,QAAQ,KAAK,kBAAkB;EAGhC,IAAI,KAAK,SAAS,KAAK,MACtB,QAAQ,KAAK,MAAM;EAGpB,OAAO;GACN,SAAS,SAAS,SAAS,KAAK,QAAQ,SAAS;GACjD;GACA,qBAAqB;EACtB;CACD;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,IAAa,iBAAb,cAAoC,OAAO,QAAQ,SAAS;CAI3D,YACC,MACA,MAwCA,MACC;EACD,MACC,IAAI,uBAAuB,GAC3B,MACA;GAAE,GAAG;GAAM,WAAW;EAAU,GAChC,IACD;CACD;AACD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@infracraft/pulumi",
3
- "version": "0.1.3",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {