@infracraft/pulumi 0.2.0 → 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/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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"}
|