@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.
- package/dist/neon/project.cjs +15 -3
- package/dist/neon/project.cjs.map +1 -1
- package/dist/neon/project.d.cts.map +1 -1
- package/dist/neon/project.d.mts.map +1 -1
- package/dist/neon/project.mjs +15 -3
- package/dist/neon/project.mjs.map +1 -1
- package/dist/railway/project.cjs +1 -1
- package/dist/railway/project.cjs.map +1 -1
- package/dist/railway/project.mjs +1 -1
- package/dist/railway/project.mjs.map +1 -1
- package/dist/railway/service.cjs +9 -6
- package/dist/railway/service.cjs.map +1 -1
- package/dist/railway/service.d.cts.map +1 -1
- package/dist/railway/service.d.mts.map +1 -1
- package/dist/railway/service.mjs +9 -6
- package/dist/railway/service.mjs.map +1 -1
- package/package.json +1 -1
package/dist/neon/project.cjs
CHANGED
|
@@ -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. `
|
|
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
|
-
|
|
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. `
|
|
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;
|
|
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;
|
|
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"}
|
package/dist/neon/project.mjs
CHANGED
|
@@ -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. `
|
|
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
|
-
|
|
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. `
|
|
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"}
|
package/dist/railway/project.cjs
CHANGED
|
@@ -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)
|
|
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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"}
|
package/dist/railway/project.mjs
CHANGED
|
@@ -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)
|
|
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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"}
|
package/dist/railway/service.cjs
CHANGED
|
@@ -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,
|
|
152
|
+
async update(id, olds, news) {
|
|
153
153
|
const client = new require_railway_client.RailwayClient(news.token);
|
|
154
|
-
|
|
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:
|
|
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
|
-
*
|
|
204
|
-
*
|
|
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)
|
|
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;
|
|
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;
|
|
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"}
|
package/dist/railway/service.mjs
CHANGED
|
@@ -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,
|
|
150
|
+
async update(id, olds, news) {
|
|
151
151
|
const client = new RailwayClient(news.token);
|
|
152
|
-
|
|
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:
|
|
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
|
-
*
|
|
202
|
-
*
|
|
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)
|
|
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":"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"}
|
|
1
|
+
{"version":3,"file":"service.mjs","names":[],"sources":["../../src/railway/service.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport { RailwayClient } from \"./client.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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"}
|