@infracraft/pulumi 1.16.6 → 1.16.8

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.
package/dist/hash.cjs CHANGED
@@ -29,22 +29,95 @@ function hash(input, options) {
29
29
  }
30
30
  const ignore = options?.ignore ?? DEFAULT_IGNORE;
31
31
  const digest = node_crypto.createHash("sha256");
32
- function walk(currentPath) {
33
- const entries = node_fs.readdirSync(currentPath, { withFileTypes: true });
34
- for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
35
- if (ignore.has(entry.name)) continue;
36
- const fullPath = node_path.join(currentPath, entry.name);
37
- if (entry.isDirectory()) walk(fullPath);
38
- else if (entry.isFile()) {
39
- digest.update(entry.name);
40
- digest.update(node_fs.readFileSync(fullPath));
41
- }
32
+ hashDirInto(digest, input, ignore);
33
+ return digest.digest("hex");
34
+ }
35
+ /** Recursively folds a directory's file names + contents into `digest`. */
36
+ function hashDirInto(digest, currentPath, ignore) {
37
+ const entries = node_fs.readdirSync(currentPath, { withFileTypes: true });
38
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
39
+ if (ignore.has(entry.name)) continue;
40
+ const fullPath = node_path.join(currentPath, entry.name);
41
+ if (entry.isDirectory()) hashDirInto(digest, fullPath, ignore);
42
+ else if (entry.isFile()) {
43
+ digest.update(entry.name);
44
+ digest.update(node_fs.readFileSync(fullPath));
45
+ }
46
+ }
47
+ }
48
+ function readWorkspacePackage(directory) {
49
+ try {
50
+ return JSON.parse(node_fs.readFileSync(node_path.join(directory, "package.json"), "utf8"));
51
+ } catch {
52
+ return;
53
+ }
54
+ }
55
+ /**
56
+ * Indexes every workspace package by its `package.json` `name` -> directory, by
57
+ * scanning `apps/*` and `packages/*`. Robust to a package whose directory name
58
+ * differs from its published name.
59
+ */
60
+ function buildWorkspaceIndex(monorepoRoot) {
61
+ const index = /* @__PURE__ */ new Map();
62
+ for (const group of ["apps", "packages"]) {
63
+ const base = node_path.join(monorepoRoot, group);
64
+ let entries;
65
+ try {
66
+ entries = node_fs.readdirSync(base, { withFileTypes: true });
67
+ } catch {
68
+ continue;
42
69
  }
70
+ for (const entry of entries) {
71
+ if (!entry.isDirectory()) continue;
72
+ const directory = node_path.join(base, entry.name);
73
+ const pkg = readWorkspacePackage(directory);
74
+ if (pkg?.name) index.set(pkg.name, directory);
75
+ }
76
+ }
77
+ return index;
78
+ }
79
+ /**
80
+ * Hashes an app's source AND every workspace package it depends on
81
+ * (transitively), for use as a redeploy trigger. Resolving the dependency
82
+ * closure from each `package.json` means a change to a shared `packages/*` an
83
+ * app depends on correctly retriggers that app's deploy — and adding a new app
84
+ * needs no hand-maintained list of which packages to hash.
85
+ *
86
+ * @param monorepoRoot Absolute repo root (holds `apps/` and `packages/`).
87
+ * @param appDirectory The app to hash, relative to the root (e.g. `apps/mesh`).
88
+ * @example
89
+ * triggers: [hashApp(monorepoRoot, "apps/mesh"), hash(env)]
90
+ */
91
+ function hashApp(monorepoRoot, appDirectory, options) {
92
+ const ignore = options?.ignore ?? DEFAULT_IGNORE;
93
+ const index = buildWorkspaceIndex(monorepoRoot);
94
+ const start = node_path.join(monorepoRoot, appDirectory);
95
+ const visited = /* @__PURE__ */ new Set();
96
+ const queue = [start];
97
+ while (queue.length > 0) {
98
+ const directory = queue.pop();
99
+ if (!directory || visited.has(directory)) continue;
100
+ visited.add(directory);
101
+ const pkg = readWorkspacePackage(directory);
102
+ if (!pkg) continue;
103
+ const deps = {
104
+ ...pkg.dependencies,
105
+ ...pkg.devDependencies
106
+ };
107
+ for (const depName of Object.keys(deps)) {
108
+ const depDirectory = index.get(depName);
109
+ if (depDirectory && !visited.has(depDirectory)) queue.push(depDirectory);
110
+ }
111
+ }
112
+ const digest = node_crypto.createHash("sha256");
113
+ for (const directory of [...visited].sort()) {
114
+ digest.update(`\0${node_path.relative(monorepoRoot, directory)}\0`);
115
+ hashDirInto(digest, directory, ignore);
43
116
  }
44
- walk(input);
45
117
  return digest.digest("hex");
46
118
  }
47
119
 
48
120
  //#endregion
49
121
  exports.hash = hash;
122
+ exports.hashApp = hashApp;
50
123
  //# sourceMappingURL=hash.cjs.map
package/dist/hash.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"hash.cjs","names":["pulumi","crypto","fs","path"],"sources":["../src/hash.ts"],"sourcesContent":["import * as crypto from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as pulumi from \"@pulumi/pulumi\";\n\nconst DEFAULT_IGNORE = new Set([\n\t\"node_modules\",\n\t\"dist\",\n\t\".turbo\",\n\t\".next\",\n\t\".git\",\n\t\".vercel\",\n]);\n\ninterface HashOptions {\n\tignore?: Set<string>;\n}\n\n/**\n * Produces a stable SHA-256 hex digest for use as a resource/deploy trigger,\n * from either a source directory or an environment map.\n *\n * - **Directory** (`string`): recursively hashes file names + contents (build\n * and VCS directories skipped). Synchronous — returns a plain `string`.\n * - **Env map** (`Record<string, Input<string>>`): resolves the values, sorts\n * by key, and hashes them into a single non-secret `Output<string>`. Passing\n * secret `Output`s straight into a dynamic resource's inputs intermittently\n * races Pulumi's gRPC secret serialization (`Unexpected struct type`, issue\n * #16041) — the reason such deploys otherwise need `--parallel 1`. Collapsing\n * the env to one `unsecret` digest keeps the trigger moving on any change\n * while carrying no secret, so deploys are safe to (re)create at full\n * parallelism. Exposing the digest is safe: it is a one-way hash of\n * high-entropy secrets.\n *\n * @param input A source directory path, or a map of env var name to value.\n * @param options Directory mode only: `ignore` overrides the default skip set.\n * @returns `string` for a directory, `Output<string>` for an env map.\n * @example\n * triggers: [hash(appDir), hash(env)]\n */\nexport function hash(directory: string, options?: HashOptions): string;\nexport function hash(\n\tenv: Record<string, pulumi.Input<string>>,\n): pulumi.Output<string>;\nexport function hash(\n\tinput: string | Record<string, pulumi.Input<string>>,\n\toptions?: HashOptions,\n): string | pulumi.Output<string> {\n\tif (typeof input !== \"string\") {\n\t\tconst keys = Object.keys(input).sort();\n\n\t\treturn pulumi.unsecret(\n\t\t\tpulumi.all(keys.map((key) => input[key])).apply((values) => {\n\t\t\t\tconst digest = crypto.createHash(\"sha256\");\n\n\t\t\t\tfor (const [index, key] of keys.entries()) {\n\t\t\t\t\tdigest.update(`${key}=${values[index]}\\0`);\n\t\t\t\t}\n\n\t\t\t\treturn digest.digest(\"hex\");\n\t\t\t}),\n\t\t);\n\t}\n\n\tconst ignore = options?.ignore ?? DEFAULT_IGNORE;\n\tconst digest = crypto.createHash(\"sha256\");\n\n\tfunction walk(currentPath: string) {\n\t\tconst entries = fs.readdirSync(currentPath, { withFileTypes: true });\n\n\t\tfor (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {\n\t\t\tif (ignore.has(entry.name)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst fullPath = path.join(currentPath, entry.name);\n\n\t\t\tif (entry.isDirectory()) {\n\t\t\t\twalk(fullPath);\n\t\t\t} else if (entry.isFile()) {\n\t\t\t\tdigest.update(entry.name);\n\t\t\t\tdigest.update(fs.readFileSync(fullPath));\n\t\t\t}\n\t\t}\n\t}\n\n\twalk(input);\n\n\treturn digest.digest(\"hex\");\n}\n"],"mappings":";;;;;;;;;;;;AAKA,MAAM,iBAAiB,IAAI,IAAI;CAC9B;CACA;CACA;CACA;CACA;CACA;AACD,CAAC;AAgCD,SAAgB,KACf,OACA,SACiC;CACjC,IAAI,OAAO,UAAU,UAAU;EAC9B,MAAM,OAAO,OAAO,KAAK,KAAK,EAAE,KAAK;EAErC,OAAOA,eAAO,SACbA,eAAO,IAAI,KAAK,KAAK,QAAQ,MAAM,IAAI,CAAC,EAAE,OAAO,WAAW;GAC3D,MAAM,SAASC,YAAO,WAAW,QAAQ;GAEzC,KAAK,MAAM,CAAC,OAAO,QAAQ,KAAK,QAAQ,GACvC,OAAO,OAAO,GAAG,IAAI,GAAG,OAAO,OAAO,GAAG;GAG1C,OAAO,OAAO,OAAO,KAAK;EAC3B,CAAC,CACF;CACD;CAEA,MAAM,SAAS,SAAS,UAAU;CAClC,MAAM,SAASA,YAAO,WAAW,QAAQ;CAEzC,SAAS,KAAK,aAAqB;EAClC,MAAM,UAAUC,QAAG,YAAY,aAAa,EAAE,eAAe,KAAK,CAAC;EAEnE,KAAK,MAAM,SAAS,QAAQ,MAAM,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC,GAAG;GACzE,IAAI,OAAO,IAAI,MAAM,IAAI,GACxB;GAGD,MAAM,WAAWC,UAAK,KAAK,aAAa,MAAM,IAAI;GAElD,IAAI,MAAM,YAAY,GACrB,KAAK,QAAQ;QACP,IAAI,MAAM,OAAO,GAAG;IAC1B,OAAO,OAAO,MAAM,IAAI;IACxB,OAAO,OAAOD,QAAG,aAAa,QAAQ,CAAC;GACxC;EACD;CACD;CAEA,KAAK,KAAK;CAEV,OAAO,OAAO,OAAO,KAAK;AAC3B"}
1
+ {"version":3,"file":"hash.cjs","names":["pulumi","crypto","fs","path"],"sources":["../src/hash.ts"],"sourcesContent":["import * as crypto from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as pulumi from \"@pulumi/pulumi\";\n\nconst DEFAULT_IGNORE = new Set([\n\t\"node_modules\",\n\t\"dist\",\n\t\".turbo\",\n\t\".next\",\n\t\".git\",\n\t\".vercel\",\n]);\n\ninterface HashOptions {\n\tignore?: Set<string>;\n}\n\n/**\n * Produces a stable SHA-256 hex digest for use as a resource/deploy trigger,\n * from either a source directory or an environment map.\n *\n * - **Directory** (`string`): recursively hashes file names + contents (build\n * and VCS directories skipped). Synchronous — returns a plain `string`.\n * - **Env map** (`Record<string, Input<string>>`): resolves the values, sorts\n * by key, and hashes them into a single non-secret `Output<string>`. Passing\n * secret `Output`s straight into a dynamic resource's inputs intermittently\n * races Pulumi's gRPC secret serialization (`Unexpected struct type`, issue\n * #16041) — the reason such deploys otherwise need `--parallel 1`. Collapsing\n * the env to one `unsecret` digest keeps the trigger moving on any change\n * while carrying no secret, so deploys are safe to (re)create at full\n * parallelism. Exposing the digest is safe: it is a one-way hash of\n * high-entropy secrets.\n *\n * @param input A source directory path, or a map of env var name to value.\n * @param options Directory mode only: `ignore` overrides the default skip set.\n * @returns `string` for a directory, `Output<string>` for an env map.\n * @example\n * triggers: [hash(appDir), hash(env)]\n */\nexport function hash(directory: string, options?: HashOptions): string;\nexport function hash(\n\tenv: Record<string, pulumi.Input<string>>,\n): pulumi.Output<string>;\nexport function hash(\n\tinput: string | Record<string, pulumi.Input<string>>,\n\toptions?: HashOptions,\n): string | pulumi.Output<string> {\n\tif (typeof input !== \"string\") {\n\t\tconst keys = Object.keys(input).sort();\n\n\t\treturn pulumi.unsecret(\n\t\t\tpulumi.all(keys.map((key) => input[key])).apply((values) => {\n\t\t\t\tconst digest = crypto.createHash(\"sha256\");\n\n\t\t\t\tfor (const [index, key] of keys.entries()) {\n\t\t\t\t\tdigest.update(`${key}=${values[index]}\\0`);\n\t\t\t\t}\n\n\t\t\t\treturn digest.digest(\"hex\");\n\t\t\t}),\n\t\t);\n\t}\n\n\tconst ignore = options?.ignore ?? DEFAULT_IGNORE;\n\tconst digest = crypto.createHash(\"sha256\");\n\n\thashDirInto(digest, input, ignore);\n\n\treturn digest.digest(\"hex\");\n}\n\n/** Recursively folds a directory's file names + contents into `digest`. */\nfunction hashDirInto(\n\tdigest: crypto.Hash,\n\tcurrentPath: string,\n\tignore: Set<string>,\n): void {\n\tconst entries = fs.readdirSync(currentPath, { withFileTypes: true });\n\n\tfor (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {\n\t\tif (ignore.has(entry.name)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst fullPath = path.join(currentPath, entry.name);\n\n\t\tif (entry.isDirectory()) {\n\t\t\thashDirInto(digest, fullPath, ignore);\n\t\t} else if (entry.isFile()) {\n\t\t\tdigest.update(entry.name);\n\t\t\tdigest.update(fs.readFileSync(fullPath));\n\t\t}\n\t}\n}\n\ninterface WorkspacePackage {\n\tname?: string;\n\tdependencies?: Record<string, string>;\n\tdevDependencies?: Record<string, string>;\n}\n\nfunction readWorkspacePackage(directory: string): WorkspacePackage | undefined {\n\ttry {\n\t\treturn JSON.parse(\n\t\t\tfs.readFileSync(path.join(directory, \"package.json\"), \"utf8\"),\n\t\t) as WorkspacePackage;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Indexes every workspace package by its `package.json` `name` -> directory, by\n * scanning `apps/*` and `packages/*`. Robust to a package whose directory name\n * differs from its published name.\n */\nfunction buildWorkspaceIndex(monorepoRoot: string): Map<string, string> {\n\tconst index = new Map<string, string>();\n\n\tfor (const group of [\"apps\", \"packages\"]) {\n\t\tconst base = path.join(monorepoRoot, group);\n\n\t\tlet entries: fs.Dirent[];\n\n\t\ttry {\n\t\t\tentries = fs.readdirSync(base, { withFileTypes: true });\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\n\t\tfor (const entry of entries) {\n\t\t\tif (!entry.isDirectory()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst directory = path.join(base, entry.name);\n\t\t\tconst pkg = readWorkspacePackage(directory);\n\n\t\t\tif (pkg?.name) {\n\t\t\t\tindex.set(pkg.name, directory);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn index;\n}\n\n/**\n * Hashes an app's source AND every workspace package it depends on\n * (transitively), for use as a redeploy trigger. Resolving the dependency\n * closure from each `package.json` means a change to a shared `packages/*` an\n * app depends on correctly retriggers that app's deploy — and adding a new app\n * needs no hand-maintained list of which packages to hash.\n *\n * @param monorepoRoot Absolute repo root (holds `apps/` and `packages/`).\n * @param appDirectory The app to hash, relative to the root (e.g. `apps/mesh`).\n * @example\n * triggers: [hashApp(monorepoRoot, \"apps/mesh\"), hash(env)]\n */\nexport function hashApp(\n\tmonorepoRoot: string,\n\tappDirectory: string,\n\toptions?: HashOptions,\n): string {\n\tconst ignore = options?.ignore ?? DEFAULT_IGNORE;\n\tconst index = buildWorkspaceIndex(monorepoRoot);\n\n\tconst start = path.join(monorepoRoot, appDirectory);\n\tconst visited = new Set<string>();\n\tconst queue = [start];\n\n\twhile (queue.length > 0) {\n\t\tconst directory = queue.pop();\n\n\t\tif (!directory || visited.has(directory)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tvisited.add(directory);\n\n\t\tconst pkg = readWorkspacePackage(directory);\n\n\t\tif (!pkg) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst deps = { ...pkg.dependencies, ...pkg.devDependencies };\n\n\t\tfor (const depName of Object.keys(deps)) {\n\t\t\tconst depDirectory = index.get(depName);\n\n\t\t\t// Only workspace packages are in the index — external deps are ignored.\n\t\t\tif (depDirectory && !visited.has(depDirectory)) {\n\t\t\t\tqueue.push(depDirectory);\n\t\t\t}\n\t\t}\n\t}\n\n\tconst digest = crypto.createHash(\"sha256\");\n\n\t// Sort by directory so the digest is independent of traversal order; include\n\t// the relative path so moving content between packages changes the hash.\n\tfor (const directory of [...visited].sort()) {\n\t\tdigest.update(`\\0${path.relative(monorepoRoot, directory)}\\0`);\n\t\thashDirInto(digest, directory, ignore);\n\t}\n\n\treturn digest.digest(\"hex\");\n}\n"],"mappings":"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"}
package/dist/hash.d.cts CHANGED
@@ -29,6 +29,19 @@ interface HashOptions {
29
29
  */
30
30
  declare function hash(directory: string, options?: HashOptions): string;
31
31
  declare function hash(env: Record<string, pulumi.Input<string>>): pulumi.Output<string>;
32
+ /**
33
+ * Hashes an app's source AND every workspace package it depends on
34
+ * (transitively), for use as a redeploy trigger. Resolving the dependency
35
+ * closure from each `package.json` means a change to a shared `packages/*` an
36
+ * app depends on correctly retriggers that app's deploy — and adding a new app
37
+ * needs no hand-maintained list of which packages to hash.
38
+ *
39
+ * @param monorepoRoot Absolute repo root (holds `apps/` and `packages/`).
40
+ * @param appDirectory The app to hash, relative to the root (e.g. `apps/mesh`).
41
+ * @example
42
+ * triggers: [hashApp(monorepoRoot, "apps/mesh"), hash(env)]
43
+ */
44
+ declare function hashApp(monorepoRoot: string, appDirectory: string, options?: HashOptions): string;
32
45
  //#endregion
33
- export { hash };
46
+ export { hash, hashApp };
34
47
  //# sourceMappingURL=hash.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"hash.d.cts","names":[],"sources":["../src/hash.ts"],"mappings":";;;;UAcU,WAAA;EACT,MAAA,GAAS,GAAG;AAAA;AAZ4B;;;;AAY5B;AAyBb;;;;;;;;AAA6D;AAC7D;;;;;;;;AAtCyC,iBAqCzB,IAAA,CAAK,SAAA,UAAmB,OAAA,GAAU,WAAW;AAAA,iBAC7C,IAAA,CACf,GAAA,EAAK,MAAA,SAAe,MAAA,CAAO,KAAA,YACzB,MAAA,CAAO,MAAA"}
1
+ {"version":3,"file":"hash.d.cts","names":[],"sources":["../src/hash.ts"],"mappings":";;;;UAcU,WAAA;EACT,MAAA,GAAS,GAAG;AAAA;AAZ4B;;;;AAY5B;AAyBb;;;;;;;;AAA6D;AAC7D;;;;;;;;AAtCyC,iBAqCzB,IAAA,CAAK,SAAA,UAAmB,OAAA,GAAU,WAAW;AAAA,iBAC7C,IAAA,CACf,GAAA,EAAK,MAAA,SAAe,MAAA,CAAO,KAAA,YACzB,MAAA,CAAO,MAAA;;;;;;AAAM;AAqHhB;;;;;;iBAAgB,OAAA,CACf,YAAA,UACA,YAAA,UACA,OAAA,GAAU,WAAW"}
package/dist/hash.d.mts CHANGED
@@ -29,6 +29,19 @@ interface HashOptions {
29
29
  */
30
30
  declare function hash(directory: string, options?: HashOptions): string;
31
31
  declare function hash(env: Record<string, pulumi.Input<string>>): pulumi.Output<string>;
32
+ /**
33
+ * Hashes an app's source AND every workspace package it depends on
34
+ * (transitively), for use as a redeploy trigger. Resolving the dependency
35
+ * closure from each `package.json` means a change to a shared `packages/*` an
36
+ * app depends on correctly retriggers that app's deploy — and adding a new app
37
+ * needs no hand-maintained list of which packages to hash.
38
+ *
39
+ * @param monorepoRoot Absolute repo root (holds `apps/` and `packages/`).
40
+ * @param appDirectory The app to hash, relative to the root (e.g. `apps/mesh`).
41
+ * @example
42
+ * triggers: [hashApp(monorepoRoot, "apps/mesh"), hash(env)]
43
+ */
44
+ declare function hashApp(monorepoRoot: string, appDirectory: string, options?: HashOptions): string;
32
45
  //#endregion
33
- export { hash };
46
+ export { hash, hashApp };
34
47
  //# sourceMappingURL=hash.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"hash.d.mts","names":[],"sources":["../src/hash.ts"],"mappings":";;;;UAcU,WAAA;EACT,MAAA,GAAS,GAAG;AAAA;AAZ4B;;;;AAY5B;AAyBb;;;;;;;;AAA6D;AAC7D;;;;;;;;AAtCyC,iBAqCzB,IAAA,CAAK,SAAA,UAAmB,OAAA,GAAU,WAAW;AAAA,iBAC7C,IAAA,CACf,GAAA,EAAK,MAAA,SAAe,MAAA,CAAO,KAAA,YACzB,MAAA,CAAO,MAAA"}
1
+ {"version":3,"file":"hash.d.mts","names":[],"sources":["../src/hash.ts"],"mappings":";;;;UAcU,WAAA;EACT,MAAA,GAAS,GAAG;AAAA;AAZ4B;;;;AAY5B;AAyBb;;;;;;;;AAA6D;AAC7D;;;;;;;;AAtCyC,iBAqCzB,IAAA,CAAK,SAAA,UAAmB,OAAA,GAAU,WAAW;AAAA,iBAC7C,IAAA,CACf,GAAA,EAAK,MAAA,SAAe,MAAA,CAAO,KAAA,YACzB,MAAA,CAAO,MAAA;;;;;;AAAM;AAqHhB;;;;;;iBAAgB,OAAA,CACf,YAAA,UACA,YAAA,UACA,OAAA,GAAU,WAAW"}
package/dist/hash.mjs CHANGED
@@ -24,22 +24,94 @@ function hash(input, options) {
24
24
  }
25
25
  const ignore = options?.ignore ?? DEFAULT_IGNORE;
26
26
  const digest = crypto.createHash("sha256");
27
- function walk(currentPath) {
28
- const entries = fs.readdirSync(currentPath, { withFileTypes: true });
29
- for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
30
- if (ignore.has(entry.name)) continue;
31
- const fullPath = path.join(currentPath, entry.name);
32
- if (entry.isDirectory()) walk(fullPath);
33
- else if (entry.isFile()) {
34
- digest.update(entry.name);
35
- digest.update(fs.readFileSync(fullPath));
36
- }
27
+ hashDirInto(digest, input, ignore);
28
+ return digest.digest("hex");
29
+ }
30
+ /** Recursively folds a directory's file names + contents into `digest`. */
31
+ function hashDirInto(digest, currentPath, ignore) {
32
+ const entries = fs.readdirSync(currentPath, { withFileTypes: true });
33
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
34
+ if (ignore.has(entry.name)) continue;
35
+ const fullPath = path.join(currentPath, entry.name);
36
+ if (entry.isDirectory()) hashDirInto(digest, fullPath, ignore);
37
+ else if (entry.isFile()) {
38
+ digest.update(entry.name);
39
+ digest.update(fs.readFileSync(fullPath));
40
+ }
41
+ }
42
+ }
43
+ function readWorkspacePackage(directory) {
44
+ try {
45
+ return JSON.parse(fs.readFileSync(path.join(directory, "package.json"), "utf8"));
46
+ } catch {
47
+ return;
48
+ }
49
+ }
50
+ /**
51
+ * Indexes every workspace package by its `package.json` `name` -> directory, by
52
+ * scanning `apps/*` and `packages/*`. Robust to a package whose directory name
53
+ * differs from its published name.
54
+ */
55
+ function buildWorkspaceIndex(monorepoRoot) {
56
+ const index = /* @__PURE__ */ new Map();
57
+ for (const group of ["apps", "packages"]) {
58
+ const base = path.join(monorepoRoot, group);
59
+ let entries;
60
+ try {
61
+ entries = fs.readdirSync(base, { withFileTypes: true });
62
+ } catch {
63
+ continue;
37
64
  }
65
+ for (const entry of entries) {
66
+ if (!entry.isDirectory()) continue;
67
+ const directory = path.join(base, entry.name);
68
+ const pkg = readWorkspacePackage(directory);
69
+ if (pkg?.name) index.set(pkg.name, directory);
70
+ }
71
+ }
72
+ return index;
73
+ }
74
+ /**
75
+ * Hashes an app's source AND every workspace package it depends on
76
+ * (transitively), for use as a redeploy trigger. Resolving the dependency
77
+ * closure from each `package.json` means a change to a shared `packages/*` an
78
+ * app depends on correctly retriggers that app's deploy — and adding a new app
79
+ * needs no hand-maintained list of which packages to hash.
80
+ *
81
+ * @param monorepoRoot Absolute repo root (holds `apps/` and `packages/`).
82
+ * @param appDirectory The app to hash, relative to the root (e.g. `apps/mesh`).
83
+ * @example
84
+ * triggers: [hashApp(monorepoRoot, "apps/mesh"), hash(env)]
85
+ */
86
+ function hashApp(monorepoRoot, appDirectory, options) {
87
+ const ignore = options?.ignore ?? DEFAULT_IGNORE;
88
+ const index = buildWorkspaceIndex(monorepoRoot);
89
+ const start = path.join(monorepoRoot, appDirectory);
90
+ const visited = /* @__PURE__ */ new Set();
91
+ const queue = [start];
92
+ while (queue.length > 0) {
93
+ const directory = queue.pop();
94
+ if (!directory || visited.has(directory)) continue;
95
+ visited.add(directory);
96
+ const pkg = readWorkspacePackage(directory);
97
+ if (!pkg) continue;
98
+ const deps = {
99
+ ...pkg.dependencies,
100
+ ...pkg.devDependencies
101
+ };
102
+ for (const depName of Object.keys(deps)) {
103
+ const depDirectory = index.get(depName);
104
+ if (depDirectory && !visited.has(depDirectory)) queue.push(depDirectory);
105
+ }
106
+ }
107
+ const digest = crypto.createHash("sha256");
108
+ for (const directory of [...visited].sort()) {
109
+ digest.update(`\0${path.relative(monorepoRoot, directory)}\0`);
110
+ hashDirInto(digest, directory, ignore);
38
111
  }
39
- walk(input);
40
112
  return digest.digest("hex");
41
113
  }
42
114
 
43
115
  //#endregion
44
- export { hash };
116
+ export { hash, hashApp };
45
117
  //# sourceMappingURL=hash.mjs.map
package/dist/hash.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"hash.mjs","names":[],"sources":["../src/hash.ts"],"sourcesContent":["import * as crypto from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as pulumi from \"@pulumi/pulumi\";\n\nconst DEFAULT_IGNORE = new Set([\n\t\"node_modules\",\n\t\"dist\",\n\t\".turbo\",\n\t\".next\",\n\t\".git\",\n\t\".vercel\",\n]);\n\ninterface HashOptions {\n\tignore?: Set<string>;\n}\n\n/**\n * Produces a stable SHA-256 hex digest for use as a resource/deploy trigger,\n * from either a source directory or an environment map.\n *\n * - **Directory** (`string`): recursively hashes file names + contents (build\n * and VCS directories skipped). Synchronous — returns a plain `string`.\n * - **Env map** (`Record<string, Input<string>>`): resolves the values, sorts\n * by key, and hashes them into a single non-secret `Output<string>`. Passing\n * secret `Output`s straight into a dynamic resource's inputs intermittently\n * races Pulumi's gRPC secret serialization (`Unexpected struct type`, issue\n * #16041) — the reason such deploys otherwise need `--parallel 1`. Collapsing\n * the env to one `unsecret` digest keeps the trigger moving on any change\n * while carrying no secret, so deploys are safe to (re)create at full\n * parallelism. Exposing the digest is safe: it is a one-way hash of\n * high-entropy secrets.\n *\n * @param input A source directory path, or a map of env var name to value.\n * @param options Directory mode only: `ignore` overrides the default skip set.\n * @returns `string` for a directory, `Output<string>` for an env map.\n * @example\n * triggers: [hash(appDir), hash(env)]\n */\nexport function hash(directory: string, options?: HashOptions): string;\nexport function hash(\n\tenv: Record<string, pulumi.Input<string>>,\n): pulumi.Output<string>;\nexport function hash(\n\tinput: string | Record<string, pulumi.Input<string>>,\n\toptions?: HashOptions,\n): string | pulumi.Output<string> {\n\tif (typeof input !== \"string\") {\n\t\tconst keys = Object.keys(input).sort();\n\n\t\treturn pulumi.unsecret(\n\t\t\tpulumi.all(keys.map((key) => input[key])).apply((values) => {\n\t\t\t\tconst digest = crypto.createHash(\"sha256\");\n\n\t\t\t\tfor (const [index, key] of keys.entries()) {\n\t\t\t\t\tdigest.update(`${key}=${values[index]}\\0`);\n\t\t\t\t}\n\n\t\t\t\treturn digest.digest(\"hex\");\n\t\t\t}),\n\t\t);\n\t}\n\n\tconst ignore = options?.ignore ?? DEFAULT_IGNORE;\n\tconst digest = crypto.createHash(\"sha256\");\n\n\tfunction walk(currentPath: string) {\n\t\tconst entries = fs.readdirSync(currentPath, { withFileTypes: true });\n\n\t\tfor (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {\n\t\t\tif (ignore.has(entry.name)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst fullPath = path.join(currentPath, entry.name);\n\n\t\t\tif (entry.isDirectory()) {\n\t\t\t\twalk(fullPath);\n\t\t\t} else if (entry.isFile()) {\n\t\t\t\tdigest.update(entry.name);\n\t\t\t\tdigest.update(fs.readFileSync(fullPath));\n\t\t\t}\n\t\t}\n\t}\n\n\twalk(input);\n\n\treturn digest.digest(\"hex\");\n}\n"],"mappings":";;;;;;;AAKA,MAAM,iBAAiB,IAAI,IAAI;CAC9B;CACA;CACA;CACA;CACA;CACA;AACD,CAAC;AAgCD,SAAgB,KACf,OACA,SACiC;CACjC,IAAI,OAAO,UAAU,UAAU;EAC9B,MAAM,OAAO,OAAO,KAAK,KAAK,EAAE,KAAK;EAErC,OAAO,OAAO,SACb,OAAO,IAAI,KAAK,KAAK,QAAQ,MAAM,IAAI,CAAC,EAAE,OAAO,WAAW;GAC3D,MAAM,SAAS,OAAO,WAAW,QAAQ;GAEzC,KAAK,MAAM,CAAC,OAAO,QAAQ,KAAK,QAAQ,GACvC,OAAO,OAAO,GAAG,IAAI,GAAG,OAAO,OAAO,GAAG;GAG1C,OAAO,OAAO,OAAO,KAAK;EAC3B,CAAC,CACF;CACD;CAEA,MAAM,SAAS,SAAS,UAAU;CAClC,MAAM,SAAS,OAAO,WAAW,QAAQ;CAEzC,SAAS,KAAK,aAAqB;EAClC,MAAM,UAAU,GAAG,YAAY,aAAa,EAAE,eAAe,KAAK,CAAC;EAEnE,KAAK,MAAM,SAAS,QAAQ,MAAM,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC,GAAG;GACzE,IAAI,OAAO,IAAI,MAAM,IAAI,GACxB;GAGD,MAAM,WAAW,KAAK,KAAK,aAAa,MAAM,IAAI;GAElD,IAAI,MAAM,YAAY,GACrB,KAAK,QAAQ;QACP,IAAI,MAAM,OAAO,GAAG;IAC1B,OAAO,OAAO,MAAM,IAAI;IACxB,OAAO,OAAO,GAAG,aAAa,QAAQ,CAAC;GACxC;EACD;CACD;CAEA,KAAK,KAAK;CAEV,OAAO,OAAO,OAAO,KAAK;AAC3B"}
1
+ {"version":3,"file":"hash.mjs","names":[],"sources":["../src/hash.ts"],"sourcesContent":["import * as crypto from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as pulumi from \"@pulumi/pulumi\";\n\nconst DEFAULT_IGNORE = new Set([\n\t\"node_modules\",\n\t\"dist\",\n\t\".turbo\",\n\t\".next\",\n\t\".git\",\n\t\".vercel\",\n]);\n\ninterface HashOptions {\n\tignore?: Set<string>;\n}\n\n/**\n * Produces a stable SHA-256 hex digest for use as a resource/deploy trigger,\n * from either a source directory or an environment map.\n *\n * - **Directory** (`string`): recursively hashes file names + contents (build\n * and VCS directories skipped). Synchronous — returns a plain `string`.\n * - **Env map** (`Record<string, Input<string>>`): resolves the values, sorts\n * by key, and hashes them into a single non-secret `Output<string>`. Passing\n * secret `Output`s straight into a dynamic resource's inputs intermittently\n * races Pulumi's gRPC secret serialization (`Unexpected struct type`, issue\n * #16041) — the reason such deploys otherwise need `--parallel 1`. Collapsing\n * the env to one `unsecret` digest keeps the trigger moving on any change\n * while carrying no secret, so deploys are safe to (re)create at full\n * parallelism. Exposing the digest is safe: it is a one-way hash of\n * high-entropy secrets.\n *\n * @param input A source directory path, or a map of env var name to value.\n * @param options Directory mode only: `ignore` overrides the default skip set.\n * @returns `string` for a directory, `Output<string>` for an env map.\n * @example\n * triggers: [hash(appDir), hash(env)]\n */\nexport function hash(directory: string, options?: HashOptions): string;\nexport function hash(\n\tenv: Record<string, pulumi.Input<string>>,\n): pulumi.Output<string>;\nexport function hash(\n\tinput: string | Record<string, pulumi.Input<string>>,\n\toptions?: HashOptions,\n): string | pulumi.Output<string> {\n\tif (typeof input !== \"string\") {\n\t\tconst keys = Object.keys(input).sort();\n\n\t\treturn pulumi.unsecret(\n\t\t\tpulumi.all(keys.map((key) => input[key])).apply((values) => {\n\t\t\t\tconst digest = crypto.createHash(\"sha256\");\n\n\t\t\t\tfor (const [index, key] of keys.entries()) {\n\t\t\t\t\tdigest.update(`${key}=${values[index]}\\0`);\n\t\t\t\t}\n\n\t\t\t\treturn digest.digest(\"hex\");\n\t\t\t}),\n\t\t);\n\t}\n\n\tconst ignore = options?.ignore ?? DEFAULT_IGNORE;\n\tconst digest = crypto.createHash(\"sha256\");\n\n\thashDirInto(digest, input, ignore);\n\n\treturn digest.digest(\"hex\");\n}\n\n/** Recursively folds a directory's file names + contents into `digest`. */\nfunction hashDirInto(\n\tdigest: crypto.Hash,\n\tcurrentPath: string,\n\tignore: Set<string>,\n): void {\n\tconst entries = fs.readdirSync(currentPath, { withFileTypes: true });\n\n\tfor (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {\n\t\tif (ignore.has(entry.name)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst fullPath = path.join(currentPath, entry.name);\n\n\t\tif (entry.isDirectory()) {\n\t\t\thashDirInto(digest, fullPath, ignore);\n\t\t} else if (entry.isFile()) {\n\t\t\tdigest.update(entry.name);\n\t\t\tdigest.update(fs.readFileSync(fullPath));\n\t\t}\n\t}\n}\n\ninterface WorkspacePackage {\n\tname?: string;\n\tdependencies?: Record<string, string>;\n\tdevDependencies?: Record<string, string>;\n}\n\nfunction readWorkspacePackage(directory: string): WorkspacePackage | undefined {\n\ttry {\n\t\treturn JSON.parse(\n\t\t\tfs.readFileSync(path.join(directory, \"package.json\"), \"utf8\"),\n\t\t) as WorkspacePackage;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Indexes every workspace package by its `package.json` `name` -> directory, by\n * scanning `apps/*` and `packages/*`. Robust to a package whose directory name\n * differs from its published name.\n */\nfunction buildWorkspaceIndex(monorepoRoot: string): Map<string, string> {\n\tconst index = new Map<string, string>();\n\n\tfor (const group of [\"apps\", \"packages\"]) {\n\t\tconst base = path.join(monorepoRoot, group);\n\n\t\tlet entries: fs.Dirent[];\n\n\t\ttry {\n\t\t\tentries = fs.readdirSync(base, { withFileTypes: true });\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\n\t\tfor (const entry of entries) {\n\t\t\tif (!entry.isDirectory()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst directory = path.join(base, entry.name);\n\t\t\tconst pkg = readWorkspacePackage(directory);\n\n\t\t\tif (pkg?.name) {\n\t\t\t\tindex.set(pkg.name, directory);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn index;\n}\n\n/**\n * Hashes an app's source AND every workspace package it depends on\n * (transitively), for use as a redeploy trigger. Resolving the dependency\n * closure from each `package.json` means a change to a shared `packages/*` an\n * app depends on correctly retriggers that app's deploy — and adding a new app\n * needs no hand-maintained list of which packages to hash.\n *\n * @param monorepoRoot Absolute repo root (holds `apps/` and `packages/`).\n * @param appDirectory The app to hash, relative to the root (e.g. `apps/mesh`).\n * @example\n * triggers: [hashApp(monorepoRoot, \"apps/mesh\"), hash(env)]\n */\nexport function hashApp(\n\tmonorepoRoot: string,\n\tappDirectory: string,\n\toptions?: HashOptions,\n): string {\n\tconst ignore = options?.ignore ?? DEFAULT_IGNORE;\n\tconst index = buildWorkspaceIndex(monorepoRoot);\n\n\tconst start = path.join(monorepoRoot, appDirectory);\n\tconst visited = new Set<string>();\n\tconst queue = [start];\n\n\twhile (queue.length > 0) {\n\t\tconst directory = queue.pop();\n\n\t\tif (!directory || visited.has(directory)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tvisited.add(directory);\n\n\t\tconst pkg = readWorkspacePackage(directory);\n\n\t\tif (!pkg) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst deps = { ...pkg.dependencies, ...pkg.devDependencies };\n\n\t\tfor (const depName of Object.keys(deps)) {\n\t\t\tconst depDirectory = index.get(depName);\n\n\t\t\t// Only workspace packages are in the index — external deps are ignored.\n\t\t\tif (depDirectory && !visited.has(depDirectory)) {\n\t\t\t\tqueue.push(depDirectory);\n\t\t\t}\n\t\t}\n\t}\n\n\tconst digest = crypto.createHash(\"sha256\");\n\n\t// Sort by directory so the digest is independent of traversal order; include\n\t// the relative path so moving content between packages changes the hash.\n\tfor (const directory of [...visited].sort()) {\n\t\tdigest.update(`\\0${path.relative(monorepoRoot, directory)}\\0`);\n\t\thashDirInto(digest, directory, ignore);\n\t}\n\n\treturn digest.digest(\"hex\");\n}\n"],"mappings":"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"}
@@ -73,14 +73,20 @@ var RailwayVolumeResourceProvider = class {
73
73
  }
74
74
  };
75
75
  }
76
- async read(_id, props) {
77
- const volumeId = await findVolumeByService(new require_railway_client.RailwayClient(props.token), props.projectId, props.serviceId);
78
- if (!volumeId) throw new Error("Railway volume not found during refresh");
76
+ async read(id, props) {
77
+ const client = new require_railway_client.RailwayClient(props.token);
78
+ let volumeId;
79
+ try {
80
+ volumeId = await findVolumeByService(client, props.projectId, props.serviceId);
81
+ } catch (error) {
82
+ _pulumi_pulumi.log.warn(`Railway volume refresh lookup failed; keeping existing state: ${String(error)}`);
83
+ }
84
+ const resolvedId = volumeId ?? props.volumeId ?? id;
79
85
  return {
80
- id: volumeId,
86
+ id: resolvedId,
81
87
  props: {
82
88
  ...props,
83
- volumeId
89
+ volumeId: resolvedId
84
90
  }
85
91
  };
86
92
  }
@@ -141,4 +147,5 @@ var RailwayVolume = class extends _pulumi_pulumi.ComponentResource {
141
147
 
142
148
  //#endregion
143
149
  exports.RailwayVolume = RailwayVolume;
150
+ exports.RailwayVolumeResourceProvider = RailwayVolumeResourceProvider;
144
151
  //# sourceMappingURL=volume.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"volume.cjs","names":["RailwayClient","pulumi"],"sources":["../../src/railway/volume.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport { RailwayClient } from \"./client\";\nimport type { RailwayEnvironment } from \"./environment\";\nimport type { RailwayProject } from \"./project\";\nimport type { RailwayProvider } from \"./provider\";\nimport type { RailwayService } from \"./service\";\n\n/** Resolved inputs for the Railway volume dynamic provider. */\nexport interface RailwayVolumeInputs {\n\t/** Railway API bearer token. */\n\ttoken: string;\n\n\t/** Railway project UUID. */\n\tprojectId: string;\n\n\t/** Railway service UUID to attach the volume to. */\n\tserviceId: string;\n\n\t/** Railway environment UUID (e.g. production). */\n\tenvironmentId: string;\n\n\t/** Absolute path inside the container where the volume is mounted (e.g. `\"/data\"`). */\n\tmountPath: string;\n}\n\n/** Persisted state for the Railway volume, extending inputs with the Railway-assigned ID. */\ninterface RailwayVolumeOutputs extends RailwayVolumeInputs {\n\t/** Railway-assigned volume UUID (set after create or adopt). */\n\tvolumeId: string;\n}\n\nconst VOLUME_CREATE = `\n mutation($input: VolumeCreateInput!) {\n volumeCreate(input: $input) {\n id\n name\n }\n }\n`;\n\nconst VOLUME_DELETE = `\n mutation($volumeId: String!) {\n volumeDelete(volumeId: $volumeId)\n }\n`;\n\nconst VOLUMES_QUERY = `\n query($projectId: String!) {\n project(id: $projectId) {\n volumes {\n edges {\n node {\n id\n name\n volumeInstances {\n edges {\n node {\n id\n mountPath\n serviceId\n }\n }\n }\n }\n }\n }\n }\n }\n`;\n\n/**\n * Finds an existing volume attached to a specific service within a project.\n */\nasync function findVolumeByService(\n\tclient: RailwayClient,\n\tprojectId: string,\n\tserviceId: string,\n): Promise<string | undefined> {\n\tconst result = await client.query<{\n\t\tproject: {\n\t\t\tvolumes: {\n\t\t\t\tedges: Array<{\n\t\t\t\t\tnode: {\n\t\t\t\t\t\tid: string;\n\t\t\t\t\t\tvolumeInstances: {\n\t\t\t\t\t\t\tedges: Array<{\n\t\t\t\t\t\t\t\tnode: { serviceId: string };\n\t\t\t\t\t\t\t}>;\n\t\t\t\t\t\t};\n\t\t\t\t\t};\n\t\t\t\t}>;\n\t\t\t};\n\t\t};\n\t}>(VOLUMES_QUERY, { projectId });\n\n\tconst match = result.project.volumes.edges.find((edge) =>\n\t\tedge.node.volumeInstances.edges.some(\n\t\t\t(vi) => vi.node.serviceId === serviceId,\n\t\t),\n\t);\n\n\treturn match?.node.id;\n}\n\n/**\n * Dynamic provider implementing CRUD for Railway persistent volumes.\n *\n * Uses adopt-or-create on `create()`: finds an existing volume by service\n * before creating a new one. Volumes are immutable after creation — changing\n * `serviceId`, `mountPath`, `environmentId`, or `projectId` triggers replacement.\n */\nclass RailwayVolumeResourceProvider implements pulumi.dynamic.ResourceProvider {\n\tasync create(\n\t\tinputs: RailwayVolumeInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new RailwayClient(inputs.token);\n\n\t\tlet volumeId = await findVolumeByService(\n\t\t\tclient,\n\t\t\tinputs.projectId,\n\t\t\tinputs.serviceId,\n\t\t);\n\n\t\tif (volumeId) {\n\t\t\tpulumi.log.info(`Adopting existing volume for service (${volumeId})`);\n\t\t} else {\n\t\t\tconst result = await client.query<{\n\t\t\t\tvolumeCreate: { id: string };\n\t\t\t}>(VOLUME_CREATE, {\n\t\t\t\tinput: {\n\t\t\t\t\tprojectId: inputs.projectId,\n\t\t\t\t\tserviceId: inputs.serviceId,\n\t\t\t\t\tenvironmentId: inputs.environmentId,\n\t\t\t\t\tmountPath: inputs.mountPath,\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tvolumeId = result.volumeCreate.id;\n\t\t}\n\n\t\treturn {\n\t\t\tid: volumeId,\n\t\t\touts: { ...inputs, volumeId },\n\t\t};\n\t}\n\n\tasync read(\n\t\t_id: string,\n\t\tprops: RailwayVolumeOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\tconst volumeId = await findVolumeByService(\n\t\t\tclient,\n\t\t\tprops.projectId,\n\t\t\tprops.serviceId,\n\t\t);\n\n\t\tif (!volumeId) {\n\t\t\tthrow new Error(\"Railway volume not found during refresh\");\n\t\t}\n\n\t\treturn { id: volumeId, props: { ...props, volumeId } };\n\t}\n\n\tasync delete(id: string, props: RailwayVolumeOutputs): Promise<void> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\ttry {\n\t\t\tawait client.query(VOLUME_DELETE, { volumeId: id });\n\t\t} catch {\n\t\t\tpulumi.log.warn(\n\t\t\t\t\"Failed to delete Railway volume (may already be deleted)\",\n\t\t\t);\n\t\t}\n\t}\n\n\tasync diff(\n\t\t_id: string,\n\t\tolds: RailwayVolumeOutputs,\n\t\tnews: RailwayVolumeInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\n\t\tif (olds.serviceId !== news.serviceId) {\n\t\t\treplaces.push(\"serviceId\");\n\t\t}\n\n\t\tif (olds.mountPath !== news.mountPath) {\n\t\t\treplaces.push(\"mountPath\");\n\t\t}\n\n\t\tif (olds.environmentId !== news.environmentId) {\n\t\t\treplaces.push(\"environmentId\");\n\t\t}\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\treplaces.push(\"projectId\");\n\t\t}\n\n\t\treturn {\n\t\t\tchanges: replaces.length > 0,\n\t\t\treplaces,\n\t\t\tdeleteBeforeReplace: true,\n\t\t};\n\t}\n}\n\n/** Internal dynamic resource — not part of the public API. */\nclass RailwayVolumeResource extends pulumi.dynamic.Resource {\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\ttoken: pulumi.Input<string>;\n\t\t\tprojectId: pulumi.Input<string>;\n\t\t\tserviceId: pulumi.Input<string>;\n\t\t\tenvironmentId: pulumi.Input<string>;\n\t\t\tmountPath: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew RailwayVolumeResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, volumeId: undefined },\n\t\t\topts,\n\t\t);\n\t}\n}\n\n/** Options type for RailwayVolume — replaces Pulumi's native `provider` field. */\ntype RailwayVolumeOptions = Omit<\n\tpulumi.ComponentResourceOptions,\n\t\"provider\"\n> & {\n\t/** Railway authentication context. */\n\tprovider: RailwayProvider;\n\n\t/** Railway project context. */\n\tproject: RailwayProject;\n\n\t/** Railway environment context. */\n\tenvironment: RailwayEnvironment;\n\n\t/** Railway service context. */\n\tservice: RailwayService;\n};\n\n/** Args for RailwayVolume. */\nexport interface RailwayVolumeArgs {\n\t/** Absolute path inside the container where the volume is mounted. */\n\tmountPath: pulumi.Input<string>;\n}\n\n/**\n * Manages a Railway persistent volume with adopt-or-create semantics.\n *\n * @example\n * ```typescript\n * new RailwayVolume(\"api-data\", {\n * mountPath: \"/data\",\n * }, { provider, project, environment, service });\n * ```\n */\nexport class RailwayVolume extends pulumi.ComponentResource {\n\tconstructor(\n\t\tname: string,\n\t\targs: RailwayVolumeArgs,\n\t\topts: RailwayVolumeOptions,\n\t) {\n\t\tconst { provider, project, environment, service, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:railway:Volume\", name, {}, pulumiOpts);\n\n\t\tnew RailwayVolumeResource(\n\t\t\t`${name}-resource`,\n\t\t\t{\n\t\t\t\ttoken: provider.token,\n\t\t\t\tprojectId: project.id,\n\t\t\t\tserviceId: service.id,\n\t\t\t\tenvironmentId: environment.id,\n\t\t\t\tmountPath: args.mountPath,\n\t\t\t},\n\t\t\t// Forward the consumer's resource options to the underlying resource. Pulumi\n\t\t\t// auto-inherits `provider`/`protect` from the parent component, but NOT options\n\t\t\t// like `retainOnDelete` — without this pass-through, setting `retainOnDelete` on a\n\t\t\t// RailwayVolume would silently never reach the actual cloud volume.\n\t\t\tpulumi.mergeOptions(pulumiOpts, { parent: this }),\n\t\t);\n\n\t\tthis.registerOutputs({});\n\t}\n}\n"],"mappings":";;;;;;;AA+BA,MAAM,gBAAgB;;;;;;;;AAStB,MAAM,gBAAgB;;;;;AAMtB,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BtB,eAAe,oBACd,QACA,WACA,WAC8B;CAwB9B,QANc,MAjBO,OAAO,MAezB,eAAe,EAAE,UAAU,CAAC,GAEV,QAAQ,QAAQ,MAAM,MAAM,SAChD,KAAK,KAAK,gBAAgB,MAAM,MAC9B,OAAO,GAAG,KAAK,cAAc,SAC/B,CAGU,GAAG,KAAK;AACpB;;;;;;;;AASA,IAAM,gCAAN,MAA+E;CAC9E,MAAM,OACL,QACuC;EACvC,MAAM,SAAS,IAAIA,qCAAc,OAAO,KAAK;EAE7C,IAAI,WAAW,MAAM,oBACpB,QACA,OAAO,WACP,OAAO,SACR;EAEA,IAAI,UACH,eAAO,IAAI,KAAK,yCAAyC,SAAS,EAAE;OAapE,YAAW,MAXU,OAAO,MAEzB,eAAe,EACjB,OAAO;GACN,WAAW,OAAO;GAClB,WAAW,OAAO;GAClB,eAAe,OAAO;GACtB,WAAW,OAAO;EACnB,EACD,CAAC,GAEiB,aAAa;EAGhC,OAAO;GACN,IAAI;GACJ,MAAM;IAAE,GAAG;IAAQ;GAAS;EAC7B;CACD;CAEA,MAAM,KACL,KACA,OACqC;EAGrC,MAAM,WAAW,MAAM,oBACtB,IAHkBA,qCAAc,MAAM,KAGjC,GACL,MAAM,WACN,MAAM,SACP;EAEA,IAAI,CAAC,UACJ,MAAM,IAAI,MAAM,yCAAyC;EAG1D,OAAO;GAAE,IAAI;GAAU,OAAO;IAAE,GAAG;IAAO;GAAS;EAAE;CACtD;CAEA,MAAM,OAAO,IAAY,OAA4C;EACpE,MAAM,SAAS,IAAIA,qCAAc,MAAM,KAAK;EAE5C,IAAI;GACH,MAAM,OAAO,MAAM,eAAe,EAAE,UAAU,GAAG,CAAC;EACnD,QAAQ;GACP,eAAO,IAAI,KACV,0DACD;EACD;CACD;CAEA,MAAM,KACL,KACA,MACA,MACqC;EACrC,MAAM,WAAqB,CAAC;EAE5B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,IAAI,KAAK,kBAAkB,KAAK,eAC/B,SAAS,KAAK,eAAe;EAG9B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,OAAO;GACN,SAAS,SAAS,SAAS;GAC3B;GACA,qBAAqB;EACtB;CACD;AACD;;AAGA,IAAM,wBAAN,cAAoCC,eAAO,QAAQ,SAAS;CAC3D,YACC,MACA,MAOA,MACC;EACD,MACC,IAAI,8BAA8B,GAClC,MACA;GAAE,GAAG;GAAM,UAAU;EAAU,GAC/B,IACD;CACD;AACD;;;;;;;;;;;AAoCA,IAAa,gBAAb,cAAmCA,eAAO,kBAAkB;CAC3D,YACC,MACA,MACA,MACC;EACD,MAAM,EAAE,UAAU,SAAS,aAAa,SAAS,GAAG,eAAe;EAEnE,MAAM,6BAA6B,MAAM,CAAC,GAAG,UAAU;EAEvD,IAAI,sBACH,GAAG,KAAK,YACR;GACC,OAAO,SAAS;GAChB,WAAW,QAAQ;GACnB,WAAW,QAAQ;GACnB,eAAe,YAAY;GAC3B,WAAW,KAAK;EACjB,GAKAA,eAAO,aAAa,YAAY,EAAE,QAAQ,KAAK,CAAC,CACjD;EAEA,KAAK,gBAAgB,CAAC,CAAC;CACxB;AACD"}
1
+ {"version":3,"file":"volume.cjs","names":["RailwayClient","pulumi"],"sources":["../../src/railway/volume.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport { RailwayClient } from \"./client\";\nimport type { RailwayEnvironment } from \"./environment\";\nimport type { RailwayProject } from \"./project\";\nimport type { RailwayProvider } from \"./provider\";\nimport type { RailwayService } from \"./service\";\n\n/** Resolved inputs for the Railway volume dynamic provider. */\nexport interface RailwayVolumeInputs {\n\t/** Railway API bearer token. */\n\ttoken: string;\n\n\t/** Railway project UUID. */\n\tprojectId: string;\n\n\t/** Railway service UUID to attach the volume to. */\n\tserviceId: string;\n\n\t/** Railway environment UUID (e.g. production). */\n\tenvironmentId: string;\n\n\t/** Absolute path inside the container where the volume is mounted (e.g. `\"/data\"`). */\n\tmountPath: string;\n}\n\n/** Persisted state for the Railway volume, extending inputs with the Railway-assigned ID. */\ninterface RailwayVolumeOutputs extends RailwayVolumeInputs {\n\t/** Railway-assigned volume UUID (set after create or adopt). */\n\tvolumeId: string;\n}\n\nconst VOLUME_CREATE = `\n mutation($input: VolumeCreateInput!) {\n volumeCreate(input: $input) {\n id\n name\n }\n }\n`;\n\nconst VOLUME_DELETE = `\n mutation($volumeId: String!) {\n volumeDelete(volumeId: $volumeId)\n }\n`;\n\nconst VOLUMES_QUERY = `\n query($projectId: String!) {\n project(id: $projectId) {\n volumes {\n edges {\n node {\n id\n name\n volumeInstances {\n edges {\n node {\n id\n mountPath\n serviceId\n }\n }\n }\n }\n }\n }\n }\n }\n`;\n\n/**\n * Finds an existing volume attached to a specific service within a project.\n */\nasync function findVolumeByService(\n\tclient: RailwayClient,\n\tprojectId: string,\n\tserviceId: string,\n): Promise<string | undefined> {\n\tconst result = await client.query<{\n\t\tproject: {\n\t\t\tvolumes: {\n\t\t\t\tedges: Array<{\n\t\t\t\t\tnode: {\n\t\t\t\t\t\tid: string;\n\t\t\t\t\t\tvolumeInstances: {\n\t\t\t\t\t\t\tedges: Array<{\n\t\t\t\t\t\t\t\tnode: { serviceId: string };\n\t\t\t\t\t\t\t}>;\n\t\t\t\t\t\t};\n\t\t\t\t\t};\n\t\t\t\t}>;\n\t\t\t};\n\t\t};\n\t}>(VOLUMES_QUERY, { projectId });\n\n\tconst match = result.project.volumes.edges.find((edge) =>\n\t\tedge.node.volumeInstances.edges.some(\n\t\t\t(vi) => vi.node.serviceId === serviceId,\n\t\t),\n\t);\n\n\treturn match?.node.id;\n}\n\n/**\n * Dynamic provider implementing CRUD for Railway persistent volumes.\n *\n * Uses adopt-or-create on `create()`: finds an existing volume by service\n * before creating a new one. Volumes are immutable after creation — changing\n * `serviceId`, `mountPath`, `environmentId`, or `projectId` triggers replacement.\n */\nexport class RailwayVolumeResourceProvider\n\timplements pulumi.dynamic.ResourceProvider\n{\n\tasync create(\n\t\tinputs: RailwayVolumeInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new RailwayClient(inputs.token);\n\n\t\tlet volumeId = await findVolumeByService(\n\t\t\tclient,\n\t\t\tinputs.projectId,\n\t\t\tinputs.serviceId,\n\t\t);\n\n\t\tif (volumeId) {\n\t\t\tpulumi.log.info(`Adopting existing volume for service (${volumeId})`);\n\t\t} else {\n\t\t\tconst result = await client.query<{\n\t\t\t\tvolumeCreate: { id: string };\n\t\t\t}>(VOLUME_CREATE, {\n\t\t\t\tinput: {\n\t\t\t\t\tprojectId: inputs.projectId,\n\t\t\t\t\tserviceId: inputs.serviceId,\n\t\t\t\t\tenvironmentId: inputs.environmentId,\n\t\t\t\t\tmountPath: inputs.mountPath,\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tvolumeId = result.volumeCreate.id;\n\t\t}\n\n\t\treturn {\n\t\t\tid: volumeId,\n\t\t\touts: { ...inputs, volumeId },\n\t\t};\n\t}\n\n\tasync read(\n\t\tid: string,\n\t\tprops: RailwayVolumeOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\tlet volumeId: string | undefined;\n\n\t\ttry {\n\t\t\tvolumeId = await findVolumeByService(\n\t\t\t\tclient,\n\t\t\t\tprops.projectId,\n\t\t\t\tprops.serviceId,\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tpulumi.log.warn(\n\t\t\t\t`Railway volume refresh lookup failed; keeping existing state: ${String(error)}`,\n\t\t\t);\n\t\t}\n\n\t\t// A project-level service's volume can be momentarily unresolvable at\n\t\t// refresh (eventual consistency / environment-scoped volume instances), so\n\t\t// fall back to the stored id rather than throwing — that turned a healthy\n\t\t// volume into a refresh error. A genuinely-deleted volume is re-adopted or\n\t\t// recreated on the next `up` (create is adopt-or-create). `read` never\n\t\t// fabricates drift and never hard-fails.\n\t\tconst resolvedId = volumeId ?? props.volumeId ?? id;\n\n\t\treturn { id: resolvedId, props: { ...props, volumeId: resolvedId } };\n\t}\n\n\tasync delete(id: string, props: RailwayVolumeOutputs): Promise<void> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\ttry {\n\t\t\tawait client.query(VOLUME_DELETE, { volumeId: id });\n\t\t} catch {\n\t\t\tpulumi.log.warn(\n\t\t\t\t\"Failed to delete Railway volume (may already be deleted)\",\n\t\t\t);\n\t\t}\n\t}\n\n\tasync diff(\n\t\t_id: string,\n\t\tolds: RailwayVolumeOutputs,\n\t\tnews: RailwayVolumeInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\n\t\tif (olds.serviceId !== news.serviceId) {\n\t\t\treplaces.push(\"serviceId\");\n\t\t}\n\n\t\tif (olds.mountPath !== news.mountPath) {\n\t\t\treplaces.push(\"mountPath\");\n\t\t}\n\n\t\tif (olds.environmentId !== news.environmentId) {\n\t\t\treplaces.push(\"environmentId\");\n\t\t}\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\treplaces.push(\"projectId\");\n\t\t}\n\n\t\treturn {\n\t\t\tchanges: replaces.length > 0,\n\t\t\treplaces,\n\t\t\tdeleteBeforeReplace: true,\n\t\t};\n\t}\n}\n\n/** Internal dynamic resource — not part of the public API. */\nclass RailwayVolumeResource extends pulumi.dynamic.Resource {\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\ttoken: pulumi.Input<string>;\n\t\t\tprojectId: pulumi.Input<string>;\n\t\t\tserviceId: pulumi.Input<string>;\n\t\t\tenvironmentId: pulumi.Input<string>;\n\t\t\tmountPath: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew RailwayVolumeResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, volumeId: undefined },\n\t\t\topts,\n\t\t);\n\t}\n}\n\n/** Options type for RailwayVolume — replaces Pulumi's native `provider` field. */\ntype RailwayVolumeOptions = Omit<\n\tpulumi.ComponentResourceOptions,\n\t\"provider\"\n> & {\n\t/** Railway authentication context. */\n\tprovider: RailwayProvider;\n\n\t/** Railway project context. */\n\tproject: RailwayProject;\n\n\t/** Railway environment context. */\n\tenvironment: RailwayEnvironment;\n\n\t/** Railway service context. */\n\tservice: RailwayService;\n};\n\n/** Args for RailwayVolume. */\nexport interface RailwayVolumeArgs {\n\t/** Absolute path inside the container where the volume is mounted. */\n\tmountPath: pulumi.Input<string>;\n}\n\n/**\n * Manages a Railway persistent volume with adopt-or-create semantics.\n *\n * @example\n * ```typescript\n * new RailwayVolume(\"api-data\", {\n * mountPath: \"/data\",\n * }, { provider, project, environment, service });\n * ```\n */\nexport class RailwayVolume extends pulumi.ComponentResource {\n\tconstructor(\n\t\tname: string,\n\t\targs: RailwayVolumeArgs,\n\t\topts: RailwayVolumeOptions,\n\t) {\n\t\tconst { provider, project, environment, service, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:railway:Volume\", name, {}, pulumiOpts);\n\n\t\tnew RailwayVolumeResource(\n\t\t\t`${name}-resource`,\n\t\t\t{\n\t\t\t\ttoken: provider.token,\n\t\t\t\tprojectId: project.id,\n\t\t\t\tserviceId: service.id,\n\t\t\t\tenvironmentId: environment.id,\n\t\t\t\tmountPath: args.mountPath,\n\t\t\t},\n\t\t\t// Forward the consumer's resource options to the underlying resource. Pulumi\n\t\t\t// auto-inherits `provider`/`protect` from the parent component, but NOT options\n\t\t\t// like `retainOnDelete` — without this pass-through, setting `retainOnDelete` on a\n\t\t\t// RailwayVolume would silently never reach the actual cloud volume.\n\t\t\tpulumi.mergeOptions(pulumiOpts, { parent: this }),\n\t\t);\n\n\t\tthis.registerOutputs({});\n\t}\n}\n"],"mappings":";;;;;;;AA+BA,MAAM,gBAAgB;;;;;;;;AAStB,MAAM,gBAAgB;;;;;AAMtB,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BtB,eAAe,oBACd,QACA,WACA,WAC8B;CAwB9B,QANc,MAjBO,OAAO,MAezB,eAAe,EAAE,UAAU,CAAC,GAEV,QAAQ,QAAQ,MAAM,MAAM,SAChD,KAAK,KAAK,gBAAgB,MAAM,MAC9B,OAAO,GAAG,KAAK,cAAc,SAC/B,CAGU,GAAG,KAAK;AACpB;;;;;;;;AASA,IAAa,gCAAb,MAEA;CACC,MAAM,OACL,QACuC;EACvC,MAAM,SAAS,IAAIA,qCAAc,OAAO,KAAK;EAE7C,IAAI,WAAW,MAAM,oBACpB,QACA,OAAO,WACP,OAAO,SACR;EAEA,IAAI,UACH,eAAO,IAAI,KAAK,yCAAyC,SAAS,EAAE;OAapE,YAAW,MAXU,OAAO,MAEzB,eAAe,EACjB,OAAO;GACN,WAAW,OAAO;GAClB,WAAW,OAAO;GAClB,eAAe,OAAO;GACtB,WAAW,OAAO;EACnB,EACD,CAAC,GAEiB,aAAa;EAGhC,OAAO;GACN,IAAI;GACJ,MAAM;IAAE,GAAG;IAAQ;GAAS;EAC7B;CACD;CAEA,MAAM,KACL,IACA,OACqC;EACrC,MAAM,SAAS,IAAIA,qCAAc,MAAM,KAAK;EAE5C,IAAI;EAEJ,IAAI;GACH,WAAW,MAAM,oBAChB,QACA,MAAM,WACN,MAAM,SACP;EACD,SAAS,OAAO;GACf,eAAO,IAAI,KACV,iEAAiE,OAAO,KAAK,GAC9E;EACD;EAQA,MAAM,aAAa,YAAY,MAAM,YAAY;EAEjD,OAAO;GAAE,IAAI;GAAY,OAAO;IAAE,GAAG;IAAO,UAAU;GAAW;EAAE;CACpE;CAEA,MAAM,OAAO,IAAY,OAA4C;EACpE,MAAM,SAAS,IAAIA,qCAAc,MAAM,KAAK;EAE5C,IAAI;GACH,MAAM,OAAO,MAAM,eAAe,EAAE,UAAU,GAAG,CAAC;EACnD,QAAQ;GACP,eAAO,IAAI,KACV,0DACD;EACD;CACD;CAEA,MAAM,KACL,KACA,MACA,MACqC;EACrC,MAAM,WAAqB,CAAC;EAE5B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,IAAI,KAAK,kBAAkB,KAAK,eAC/B,SAAS,KAAK,eAAe;EAG9B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,OAAO;GACN,SAAS,SAAS,SAAS;GAC3B;GACA,qBAAqB;EACtB;CACD;AACD;;AAGA,IAAM,wBAAN,cAAoCC,eAAO,QAAQ,SAAS;CAC3D,YACC,MACA,MAOA,MACC;EACD,MACC,IAAI,8BAA8B,GAClC,MACA;GAAE,GAAG;GAAM,UAAU;EAAU,GAC/B,IACD;CACD;AACD;;;;;;;;;;;AAoCA,IAAa,gBAAb,cAAmCA,eAAO,kBAAkB;CAC3D,YACC,MACA,MACA,MACC;EACD,MAAM,EAAE,UAAU,SAAS,aAAa,SAAS,GAAG,eAAe;EAEnE,MAAM,6BAA6B,MAAM,CAAC,GAAG,UAAU;EAEvD,IAAI,sBACH,GAAG,KAAK,YACR;GACC,OAAO,SAAS;GAChB,WAAW,QAAQ;GACnB,WAAW,QAAQ;GACnB,eAAe,YAAY;GAC3B,WAAW,KAAK;EACjB,GAKAA,eAAO,aAAa,YAAY,EAAE,QAAQ,KAAK,CAAC,CACjD;EAEA,KAAK,gBAAgB,CAAC,CAAC;CACxB;AACD"}
@@ -19,6 +19,24 @@ interface RailwayVolumeInputs {
19
19
  /** Absolute path inside the container where the volume is mounted (e.g. `"/data"`). */
20
20
  mountPath: string;
21
21
  }
22
+ /** Persisted state for the Railway volume, extending inputs with the Railway-assigned ID. */
23
+ interface RailwayVolumeOutputs extends RailwayVolumeInputs {
24
+ /** Railway-assigned volume UUID (set after create or adopt). */
25
+ volumeId: string;
26
+ }
27
+ /**
28
+ * Dynamic provider implementing CRUD for Railway persistent volumes.
29
+ *
30
+ * Uses adopt-or-create on `create()`: finds an existing volume by service
31
+ * before creating a new one. Volumes are immutable after creation — changing
32
+ * `serviceId`, `mountPath`, `environmentId`, or `projectId` triggers replacement.
33
+ */
34
+ declare class RailwayVolumeResourceProvider implements pulumi.dynamic.ResourceProvider {
35
+ create(inputs: RailwayVolumeInputs): Promise<pulumi.dynamic.CreateResult>;
36
+ read(id: string, props: RailwayVolumeOutputs): Promise<pulumi.dynamic.ReadResult>;
37
+ delete(id: string, props: RailwayVolumeOutputs): Promise<void>;
38
+ diff(_id: string, olds: RailwayVolumeOutputs, news: RailwayVolumeInputs): Promise<pulumi.dynamic.DiffResult>;
39
+ }
22
40
  /** Options type for RailwayVolume — replaces Pulumi's native `provider` field. */
23
41
  type RailwayVolumeOptions = Omit<pulumi.ComponentResourceOptions, "provider"> & {
24
42
  /** Railway authentication context. */provider: RailwayProvider; /** Railway project context. */
@@ -45,5 +63,5 @@ declare class RailwayVolume extends pulumi.ComponentResource {
45
63
  constructor(name: string, args: RailwayVolumeArgs, opts: RailwayVolumeOptions);
46
64
  }
47
65
  //#endregion
48
- export { RailwayVolume, RailwayVolumeArgs, RailwayVolumeInputs };
66
+ export { RailwayVolume, RailwayVolumeArgs, RailwayVolumeInputs, RailwayVolumeResourceProvider };
49
67
  //# sourceMappingURL=volume.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"volume.d.cts","names":[],"sources":["../../src/railway/volume.ts"],"mappings":";;;;;;;;;UAQiB,mBAAA;;EAEhB,KAAA;EAFmC;EAKnC,SAAA;EALmC;EAQnC,SAAA;EAHA;EAMA,aAAA;EAAA;EAGA,SAAA;AAAA;AAAS;AAAA,KAiNL,oBAAA,GAAuB,IAAA,CAC3B,MAAA,CAAO,wBAAA;EADiB,sCAKxB,QAAA,EAAU,eAAA,EAJV;EAOA,OAAA,EAAS,cAAA,EAHC;EAMV,WAAA,EAAa,kBAAA,EAAA;EAGb,OAAA,EAAS,cAAA;AAAA;;UAIO,iBAAA;EAjBhB;EAmBA,SAAA,EAAW,MAAA,CAAO,KAAK;AAAA;;;;;;;;;AANA;AAIxB;cAea,aAAA,SAAsB,MAAA,CAAO,iBAAA;cAExC,IAAA,UACA,IAAA,EAAM,iBAAA,EACN,IAAA,EAAM,oBAAA;AAAA"}
1
+ {"version":3,"file":"volume.d.cts","names":[],"sources":["../../src/railway/volume.ts"],"mappings":";;;;;;;;;UAQiB,mBAAA;;EAEhB,KAAA;EAFmC;EAKnC,SAAA;EALmC;EAQnC,SAAA;EAHA;EAMA,aAAA;EAAA;EAGA,SAAA;AAAA;AAAS;AAAA,UAIA,oBAAA,SAA6B,mBAAmB;EAA3B;EAE9B,QAAQ;AAAA;AAAA;AAmFT;;;;;;AAnFS,cAmFI,6BAAA,YACD,MAAA,CAAO,OAAA,CAAQ,gBAAA;EAEpB,MAAA,CACL,MAAA,EAAQ,mBAAA,GACN,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EAgCpB,IAAA,CACL,EAAA,UACA,KAAA,EAAO,oBAAA,GACL,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;EA4BpB,MAAA,CAAO,EAAA,UAAY,KAAA,EAAO,oBAAA,GAAuB,OAAA;EAYjD,IAAA,CACL,GAAA,UACA,IAAA,EAAM,oBAAA,EACN,IAAA,EAAM,mBAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;AAAA;;KAkDtB,oBAAA,GAAuB,IAAA,CAC3B,MAAA,CAAO,wBAAA;EAnDJ,sCAuDH,QAAA,EAAU,eAAA,EA1IgC;EA6I1C,OAAA,EAAS,cAAA,EA7IE;EAgJX,WAAA,EAAa,kBAAA,EAhJa;EAmJ1B,OAAA,EAAS,cAAA;AAAA;;UAIO,iBAAA;EAnJL;EAqJX,SAAA,EAAW,MAAA,CAAO,KAAK;AAAA;;;;;;;;;;;cAaX,aAAA,SAAsB,MAAA,CAAO,iBAAA;cAExC,IAAA,UACA,IAAA,EAAM,iBAAA,EACN,IAAA,EAAM,oBAAA;AAAA"}
@@ -19,6 +19,24 @@ interface RailwayVolumeInputs {
19
19
  /** Absolute path inside the container where the volume is mounted (e.g. `"/data"`). */
20
20
  mountPath: string;
21
21
  }
22
+ /** Persisted state for the Railway volume, extending inputs with the Railway-assigned ID. */
23
+ interface RailwayVolumeOutputs extends RailwayVolumeInputs {
24
+ /** Railway-assigned volume UUID (set after create or adopt). */
25
+ volumeId: string;
26
+ }
27
+ /**
28
+ * Dynamic provider implementing CRUD for Railway persistent volumes.
29
+ *
30
+ * Uses adopt-or-create on `create()`: finds an existing volume by service
31
+ * before creating a new one. Volumes are immutable after creation — changing
32
+ * `serviceId`, `mountPath`, `environmentId`, or `projectId` triggers replacement.
33
+ */
34
+ declare class RailwayVolumeResourceProvider implements pulumi.dynamic.ResourceProvider {
35
+ create(inputs: RailwayVolumeInputs): Promise<pulumi.dynamic.CreateResult>;
36
+ read(id: string, props: RailwayVolumeOutputs): Promise<pulumi.dynamic.ReadResult>;
37
+ delete(id: string, props: RailwayVolumeOutputs): Promise<void>;
38
+ diff(_id: string, olds: RailwayVolumeOutputs, news: RailwayVolumeInputs): Promise<pulumi.dynamic.DiffResult>;
39
+ }
22
40
  /** Options type for RailwayVolume — replaces Pulumi's native `provider` field. */
23
41
  type RailwayVolumeOptions = Omit<pulumi.ComponentResourceOptions, "provider"> & {
24
42
  /** Railway authentication context. */provider: RailwayProvider; /** Railway project context. */
@@ -45,5 +63,5 @@ declare class RailwayVolume extends pulumi.ComponentResource {
45
63
  constructor(name: string, args: RailwayVolumeArgs, opts: RailwayVolumeOptions);
46
64
  }
47
65
  //#endregion
48
- export { RailwayVolume, RailwayVolumeArgs, RailwayVolumeInputs };
66
+ export { RailwayVolume, RailwayVolumeArgs, RailwayVolumeInputs, RailwayVolumeResourceProvider };
49
67
  //# sourceMappingURL=volume.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"volume.d.mts","names":[],"sources":["../../src/railway/volume.ts"],"mappings":";;;;;;;;;UAQiB,mBAAA;;EAEhB,KAAA;EAFmC;EAKnC,SAAA;EALmC;EAQnC,SAAA;EAHA;EAMA,aAAA;EAAA;EAGA,SAAA;AAAA;AAAS;AAAA,KAiNL,oBAAA,GAAuB,IAAA,CAC3B,MAAA,CAAO,wBAAA;EADiB,sCAKxB,QAAA,EAAU,eAAA,EAJV;EAOA,OAAA,EAAS,cAAA,EAHC;EAMV,WAAA,EAAa,kBAAA,EAAA;EAGb,OAAA,EAAS,cAAA;AAAA;;UAIO,iBAAA;EAjBhB;EAmBA,SAAA,EAAW,MAAA,CAAO,KAAK;AAAA;;;;;;;;;AANA;AAIxB;cAea,aAAA,SAAsB,MAAA,CAAO,iBAAA;cAExC,IAAA,UACA,IAAA,EAAM,iBAAA,EACN,IAAA,EAAM,oBAAA;AAAA"}
1
+ {"version":3,"file":"volume.d.mts","names":[],"sources":["../../src/railway/volume.ts"],"mappings":";;;;;;;;;UAQiB,mBAAA;;EAEhB,KAAA;EAFmC;EAKnC,SAAA;EALmC;EAQnC,SAAA;EAHA;EAMA,aAAA;EAAA;EAGA,SAAA;AAAA;AAAS;AAAA,UAIA,oBAAA,SAA6B,mBAAmB;EAA3B;EAE9B,QAAQ;AAAA;AAAA;AAmFT;;;;;;AAnFS,cAmFI,6BAAA,YACD,MAAA,CAAO,OAAA,CAAQ,gBAAA;EAEpB,MAAA,CACL,MAAA,EAAQ,mBAAA,GACN,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EAgCpB,IAAA,CACL,EAAA,UACA,KAAA,EAAO,oBAAA,GACL,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;EA4BpB,MAAA,CAAO,EAAA,UAAY,KAAA,EAAO,oBAAA,GAAuB,OAAA;EAYjD,IAAA,CACL,GAAA,UACA,IAAA,EAAM,oBAAA,EACN,IAAA,EAAM,mBAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;AAAA;;KAkDtB,oBAAA,GAAuB,IAAA,CAC3B,MAAA,CAAO,wBAAA;EAnDJ,sCAuDH,QAAA,EAAU,eAAA,EA1IgC;EA6I1C,OAAA,EAAS,cAAA,EA7IE;EAgJX,WAAA,EAAa,kBAAA,EAhJa;EAmJ1B,OAAA,EAAS,cAAA;AAAA;;UAIO,iBAAA;EAnJL;EAqJX,SAAA,EAAW,MAAA,CAAO,KAAK;AAAA;;;;;;;;;;;cAaX,aAAA,SAAsB,MAAA,CAAO,iBAAA;cAExC,IAAA,UACA,IAAA,EAAM,iBAAA,EACN,IAAA,EAAM,oBAAA;AAAA"}
@@ -71,14 +71,20 @@ var RailwayVolumeResourceProvider = class {
71
71
  }
72
72
  };
73
73
  }
74
- async read(_id, props) {
75
- const volumeId = await findVolumeByService(new RailwayClient(props.token), props.projectId, props.serviceId);
76
- if (!volumeId) throw new Error("Railway volume not found during refresh");
74
+ async read(id, props) {
75
+ const client = new RailwayClient(props.token);
76
+ let volumeId;
77
+ try {
78
+ volumeId = await findVolumeByService(client, props.projectId, props.serviceId);
79
+ } catch (error) {
80
+ pulumi.log.warn(`Railway volume refresh lookup failed; keeping existing state: ${String(error)}`);
81
+ }
82
+ const resolvedId = volumeId ?? props.volumeId ?? id;
77
83
  return {
78
- id: volumeId,
84
+ id: resolvedId,
79
85
  props: {
80
86
  ...props,
81
- volumeId
87
+ volumeId: resolvedId
82
88
  }
83
89
  };
84
90
  }
@@ -138,5 +144,5 @@ var RailwayVolume = class extends pulumi.ComponentResource {
138
144
  };
139
145
 
140
146
  //#endregion
141
- export { RailwayVolume };
147
+ export { RailwayVolume, RailwayVolumeResourceProvider };
142
148
  //# sourceMappingURL=volume.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"volume.mjs","names":[],"sources":["../../src/railway/volume.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport { RailwayClient } from \"./client\";\nimport type { RailwayEnvironment } from \"./environment\";\nimport type { RailwayProject } from \"./project\";\nimport type { RailwayProvider } from \"./provider\";\nimport type { RailwayService } from \"./service\";\n\n/** Resolved inputs for the Railway volume dynamic provider. */\nexport interface RailwayVolumeInputs {\n\t/** Railway API bearer token. */\n\ttoken: string;\n\n\t/** Railway project UUID. */\n\tprojectId: string;\n\n\t/** Railway service UUID to attach the volume to. */\n\tserviceId: string;\n\n\t/** Railway environment UUID (e.g. production). */\n\tenvironmentId: string;\n\n\t/** Absolute path inside the container where the volume is mounted (e.g. `\"/data\"`). */\n\tmountPath: string;\n}\n\n/** Persisted state for the Railway volume, extending inputs with the Railway-assigned ID. */\ninterface RailwayVolumeOutputs extends RailwayVolumeInputs {\n\t/** Railway-assigned volume UUID (set after create or adopt). */\n\tvolumeId: string;\n}\n\nconst VOLUME_CREATE = `\n mutation($input: VolumeCreateInput!) {\n volumeCreate(input: $input) {\n id\n name\n }\n }\n`;\n\nconst VOLUME_DELETE = `\n mutation($volumeId: String!) {\n volumeDelete(volumeId: $volumeId)\n }\n`;\n\nconst VOLUMES_QUERY = `\n query($projectId: String!) {\n project(id: $projectId) {\n volumes {\n edges {\n node {\n id\n name\n volumeInstances {\n edges {\n node {\n id\n mountPath\n serviceId\n }\n }\n }\n }\n }\n }\n }\n }\n`;\n\n/**\n * Finds an existing volume attached to a specific service within a project.\n */\nasync function findVolumeByService(\n\tclient: RailwayClient,\n\tprojectId: string,\n\tserviceId: string,\n): Promise<string | undefined> {\n\tconst result = await client.query<{\n\t\tproject: {\n\t\t\tvolumes: {\n\t\t\t\tedges: Array<{\n\t\t\t\t\tnode: {\n\t\t\t\t\t\tid: string;\n\t\t\t\t\t\tvolumeInstances: {\n\t\t\t\t\t\t\tedges: Array<{\n\t\t\t\t\t\t\t\tnode: { serviceId: string };\n\t\t\t\t\t\t\t}>;\n\t\t\t\t\t\t};\n\t\t\t\t\t};\n\t\t\t\t}>;\n\t\t\t};\n\t\t};\n\t}>(VOLUMES_QUERY, { projectId });\n\n\tconst match = result.project.volumes.edges.find((edge) =>\n\t\tedge.node.volumeInstances.edges.some(\n\t\t\t(vi) => vi.node.serviceId === serviceId,\n\t\t),\n\t);\n\n\treturn match?.node.id;\n}\n\n/**\n * Dynamic provider implementing CRUD for Railway persistent volumes.\n *\n * Uses adopt-or-create on `create()`: finds an existing volume by service\n * before creating a new one. Volumes are immutable after creation — changing\n * `serviceId`, `mountPath`, `environmentId`, or `projectId` triggers replacement.\n */\nclass RailwayVolumeResourceProvider implements pulumi.dynamic.ResourceProvider {\n\tasync create(\n\t\tinputs: RailwayVolumeInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new RailwayClient(inputs.token);\n\n\t\tlet volumeId = await findVolumeByService(\n\t\t\tclient,\n\t\t\tinputs.projectId,\n\t\t\tinputs.serviceId,\n\t\t);\n\n\t\tif (volumeId) {\n\t\t\tpulumi.log.info(`Adopting existing volume for service (${volumeId})`);\n\t\t} else {\n\t\t\tconst result = await client.query<{\n\t\t\t\tvolumeCreate: { id: string };\n\t\t\t}>(VOLUME_CREATE, {\n\t\t\t\tinput: {\n\t\t\t\t\tprojectId: inputs.projectId,\n\t\t\t\t\tserviceId: inputs.serviceId,\n\t\t\t\t\tenvironmentId: inputs.environmentId,\n\t\t\t\t\tmountPath: inputs.mountPath,\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tvolumeId = result.volumeCreate.id;\n\t\t}\n\n\t\treturn {\n\t\t\tid: volumeId,\n\t\t\touts: { ...inputs, volumeId },\n\t\t};\n\t}\n\n\tasync read(\n\t\t_id: string,\n\t\tprops: RailwayVolumeOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\tconst volumeId = await findVolumeByService(\n\t\t\tclient,\n\t\t\tprops.projectId,\n\t\t\tprops.serviceId,\n\t\t);\n\n\t\tif (!volumeId) {\n\t\t\tthrow new Error(\"Railway volume not found during refresh\");\n\t\t}\n\n\t\treturn { id: volumeId, props: { ...props, volumeId } };\n\t}\n\n\tasync delete(id: string, props: RailwayVolumeOutputs): Promise<void> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\ttry {\n\t\t\tawait client.query(VOLUME_DELETE, { volumeId: id });\n\t\t} catch {\n\t\t\tpulumi.log.warn(\n\t\t\t\t\"Failed to delete Railway volume (may already be deleted)\",\n\t\t\t);\n\t\t}\n\t}\n\n\tasync diff(\n\t\t_id: string,\n\t\tolds: RailwayVolumeOutputs,\n\t\tnews: RailwayVolumeInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\n\t\tif (olds.serviceId !== news.serviceId) {\n\t\t\treplaces.push(\"serviceId\");\n\t\t}\n\n\t\tif (olds.mountPath !== news.mountPath) {\n\t\t\treplaces.push(\"mountPath\");\n\t\t}\n\n\t\tif (olds.environmentId !== news.environmentId) {\n\t\t\treplaces.push(\"environmentId\");\n\t\t}\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\treplaces.push(\"projectId\");\n\t\t}\n\n\t\treturn {\n\t\t\tchanges: replaces.length > 0,\n\t\t\treplaces,\n\t\t\tdeleteBeforeReplace: true,\n\t\t};\n\t}\n}\n\n/** Internal dynamic resource — not part of the public API. */\nclass RailwayVolumeResource extends pulumi.dynamic.Resource {\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\ttoken: pulumi.Input<string>;\n\t\t\tprojectId: pulumi.Input<string>;\n\t\t\tserviceId: pulumi.Input<string>;\n\t\t\tenvironmentId: pulumi.Input<string>;\n\t\t\tmountPath: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew RailwayVolumeResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, volumeId: undefined },\n\t\t\topts,\n\t\t);\n\t}\n}\n\n/** Options type for RailwayVolume — replaces Pulumi's native `provider` field. */\ntype RailwayVolumeOptions = Omit<\n\tpulumi.ComponentResourceOptions,\n\t\"provider\"\n> & {\n\t/** Railway authentication context. */\n\tprovider: RailwayProvider;\n\n\t/** Railway project context. */\n\tproject: RailwayProject;\n\n\t/** Railway environment context. */\n\tenvironment: RailwayEnvironment;\n\n\t/** Railway service context. */\n\tservice: RailwayService;\n};\n\n/** Args for RailwayVolume. */\nexport interface RailwayVolumeArgs {\n\t/** Absolute path inside the container where the volume is mounted. */\n\tmountPath: pulumi.Input<string>;\n}\n\n/**\n * Manages a Railway persistent volume with adopt-or-create semantics.\n *\n * @example\n * ```typescript\n * new RailwayVolume(\"api-data\", {\n * mountPath: \"/data\",\n * }, { provider, project, environment, service });\n * ```\n */\nexport class RailwayVolume extends pulumi.ComponentResource {\n\tconstructor(\n\t\tname: string,\n\t\targs: RailwayVolumeArgs,\n\t\topts: RailwayVolumeOptions,\n\t) {\n\t\tconst { provider, project, environment, service, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:railway:Volume\", name, {}, pulumiOpts);\n\n\t\tnew RailwayVolumeResource(\n\t\t\t`${name}-resource`,\n\t\t\t{\n\t\t\t\ttoken: provider.token,\n\t\t\t\tprojectId: project.id,\n\t\t\t\tserviceId: service.id,\n\t\t\t\tenvironmentId: environment.id,\n\t\t\t\tmountPath: args.mountPath,\n\t\t\t},\n\t\t\t// Forward the consumer's resource options to the underlying resource. Pulumi\n\t\t\t// auto-inherits `provider`/`protect` from the parent component, but NOT options\n\t\t\t// like `retainOnDelete` — without this pass-through, setting `retainOnDelete` on a\n\t\t\t// RailwayVolume would silently never reach the actual cloud volume.\n\t\t\tpulumi.mergeOptions(pulumiOpts, { parent: this }),\n\t\t);\n\n\t\tthis.registerOutputs({});\n\t}\n}\n"],"mappings":";;;;;AA+BA,MAAM,gBAAgB;;;;;;;;AAStB,MAAM,gBAAgB;;;;;AAMtB,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BtB,eAAe,oBACd,QACA,WACA,WAC8B;CAwB9B,QANc,MAjBO,OAAO,MAezB,eAAe,EAAE,UAAU,CAAC,GAEV,QAAQ,QAAQ,MAAM,MAAM,SAChD,KAAK,KAAK,gBAAgB,MAAM,MAC9B,OAAO,GAAG,KAAK,cAAc,SAC/B,CAGU,GAAG,KAAK;AACpB;;;;;;;;AASA,IAAM,gCAAN,MAA+E;CAC9E,MAAM,OACL,QACuC;EACvC,MAAM,SAAS,IAAI,cAAc,OAAO,KAAK;EAE7C,IAAI,WAAW,MAAM,oBACpB,QACA,OAAO,WACP,OAAO,SACR;EAEA,IAAI,UACH,OAAO,IAAI,KAAK,yCAAyC,SAAS,EAAE;OAapE,YAAW,MAXU,OAAO,MAEzB,eAAe,EACjB,OAAO;GACN,WAAW,OAAO;GAClB,WAAW,OAAO;GAClB,eAAe,OAAO;GACtB,WAAW,OAAO;EACnB,EACD,CAAC,GAEiB,aAAa;EAGhC,OAAO;GACN,IAAI;GACJ,MAAM;IAAE,GAAG;IAAQ;GAAS;EAC7B;CACD;CAEA,MAAM,KACL,KACA,OACqC;EAGrC,MAAM,WAAW,MAAM,oBACtB,IAHkB,cAAc,MAAM,KAGjC,GACL,MAAM,WACN,MAAM,SACP;EAEA,IAAI,CAAC,UACJ,MAAM,IAAI,MAAM,yCAAyC;EAG1D,OAAO;GAAE,IAAI;GAAU,OAAO;IAAE,GAAG;IAAO;GAAS;EAAE;CACtD;CAEA,MAAM,OAAO,IAAY,OAA4C;EACpE,MAAM,SAAS,IAAI,cAAc,MAAM,KAAK;EAE5C,IAAI;GACH,MAAM,OAAO,MAAM,eAAe,EAAE,UAAU,GAAG,CAAC;EACnD,QAAQ;GACP,OAAO,IAAI,KACV,0DACD;EACD;CACD;CAEA,MAAM,KACL,KACA,MACA,MACqC;EACrC,MAAM,WAAqB,CAAC;EAE5B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,IAAI,KAAK,kBAAkB,KAAK,eAC/B,SAAS,KAAK,eAAe;EAG9B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,OAAO;GACN,SAAS,SAAS,SAAS;GAC3B;GACA,qBAAqB;EACtB;CACD;AACD;;AAGA,IAAM,wBAAN,cAAoC,OAAO,QAAQ,SAAS;CAC3D,YACC,MACA,MAOA,MACC;EACD,MACC,IAAI,8BAA8B,GAClC,MACA;GAAE,GAAG;GAAM,UAAU;EAAU,GAC/B,IACD;CACD;AACD;;;;;;;;;;;AAoCA,IAAa,gBAAb,cAAmC,OAAO,kBAAkB;CAC3D,YACC,MACA,MACA,MACC;EACD,MAAM,EAAE,UAAU,SAAS,aAAa,SAAS,GAAG,eAAe;EAEnE,MAAM,6BAA6B,MAAM,CAAC,GAAG,UAAU;EAEvD,IAAI,sBACH,GAAG,KAAK,YACR;GACC,OAAO,SAAS;GAChB,WAAW,QAAQ;GACnB,WAAW,QAAQ;GACnB,eAAe,YAAY;GAC3B,WAAW,KAAK;EACjB,GAKA,OAAO,aAAa,YAAY,EAAE,QAAQ,KAAK,CAAC,CACjD;EAEA,KAAK,gBAAgB,CAAC,CAAC;CACxB;AACD"}
1
+ {"version":3,"file":"volume.mjs","names":[],"sources":["../../src/railway/volume.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport { RailwayClient } from \"./client\";\nimport type { RailwayEnvironment } from \"./environment\";\nimport type { RailwayProject } from \"./project\";\nimport type { RailwayProvider } from \"./provider\";\nimport type { RailwayService } from \"./service\";\n\n/** Resolved inputs for the Railway volume dynamic provider. */\nexport interface RailwayVolumeInputs {\n\t/** Railway API bearer token. */\n\ttoken: string;\n\n\t/** Railway project UUID. */\n\tprojectId: string;\n\n\t/** Railway service UUID to attach the volume to. */\n\tserviceId: string;\n\n\t/** Railway environment UUID (e.g. production). */\n\tenvironmentId: string;\n\n\t/** Absolute path inside the container where the volume is mounted (e.g. `\"/data\"`). */\n\tmountPath: string;\n}\n\n/** Persisted state for the Railway volume, extending inputs with the Railway-assigned ID. */\ninterface RailwayVolumeOutputs extends RailwayVolumeInputs {\n\t/** Railway-assigned volume UUID (set after create or adopt). */\n\tvolumeId: string;\n}\n\nconst VOLUME_CREATE = `\n mutation($input: VolumeCreateInput!) {\n volumeCreate(input: $input) {\n id\n name\n }\n }\n`;\n\nconst VOLUME_DELETE = `\n mutation($volumeId: String!) {\n volumeDelete(volumeId: $volumeId)\n }\n`;\n\nconst VOLUMES_QUERY = `\n query($projectId: String!) {\n project(id: $projectId) {\n volumes {\n edges {\n node {\n id\n name\n volumeInstances {\n edges {\n node {\n id\n mountPath\n serviceId\n }\n }\n }\n }\n }\n }\n }\n }\n`;\n\n/**\n * Finds an existing volume attached to a specific service within a project.\n */\nasync function findVolumeByService(\n\tclient: RailwayClient,\n\tprojectId: string,\n\tserviceId: string,\n): Promise<string | undefined> {\n\tconst result = await client.query<{\n\t\tproject: {\n\t\t\tvolumes: {\n\t\t\t\tedges: Array<{\n\t\t\t\t\tnode: {\n\t\t\t\t\t\tid: string;\n\t\t\t\t\t\tvolumeInstances: {\n\t\t\t\t\t\t\tedges: Array<{\n\t\t\t\t\t\t\t\tnode: { serviceId: string };\n\t\t\t\t\t\t\t}>;\n\t\t\t\t\t\t};\n\t\t\t\t\t};\n\t\t\t\t}>;\n\t\t\t};\n\t\t};\n\t}>(VOLUMES_QUERY, { projectId });\n\n\tconst match = result.project.volumes.edges.find((edge) =>\n\t\tedge.node.volumeInstances.edges.some(\n\t\t\t(vi) => vi.node.serviceId === serviceId,\n\t\t),\n\t);\n\n\treturn match?.node.id;\n}\n\n/**\n * Dynamic provider implementing CRUD for Railway persistent volumes.\n *\n * Uses adopt-or-create on `create()`: finds an existing volume by service\n * before creating a new one. Volumes are immutable after creation — changing\n * `serviceId`, `mountPath`, `environmentId`, or `projectId` triggers replacement.\n */\nexport class RailwayVolumeResourceProvider\n\timplements pulumi.dynamic.ResourceProvider\n{\n\tasync create(\n\t\tinputs: RailwayVolumeInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new RailwayClient(inputs.token);\n\n\t\tlet volumeId = await findVolumeByService(\n\t\t\tclient,\n\t\t\tinputs.projectId,\n\t\t\tinputs.serviceId,\n\t\t);\n\n\t\tif (volumeId) {\n\t\t\tpulumi.log.info(`Adopting existing volume for service (${volumeId})`);\n\t\t} else {\n\t\t\tconst result = await client.query<{\n\t\t\t\tvolumeCreate: { id: string };\n\t\t\t}>(VOLUME_CREATE, {\n\t\t\t\tinput: {\n\t\t\t\t\tprojectId: inputs.projectId,\n\t\t\t\t\tserviceId: inputs.serviceId,\n\t\t\t\t\tenvironmentId: inputs.environmentId,\n\t\t\t\t\tmountPath: inputs.mountPath,\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tvolumeId = result.volumeCreate.id;\n\t\t}\n\n\t\treturn {\n\t\t\tid: volumeId,\n\t\t\touts: { ...inputs, volumeId },\n\t\t};\n\t}\n\n\tasync read(\n\t\tid: string,\n\t\tprops: RailwayVolumeOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\tlet volumeId: string | undefined;\n\n\t\ttry {\n\t\t\tvolumeId = await findVolumeByService(\n\t\t\t\tclient,\n\t\t\t\tprops.projectId,\n\t\t\t\tprops.serviceId,\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tpulumi.log.warn(\n\t\t\t\t`Railway volume refresh lookup failed; keeping existing state: ${String(error)}`,\n\t\t\t);\n\t\t}\n\n\t\t// A project-level service's volume can be momentarily unresolvable at\n\t\t// refresh (eventual consistency / environment-scoped volume instances), so\n\t\t// fall back to the stored id rather than throwing — that turned a healthy\n\t\t// volume into a refresh error. A genuinely-deleted volume is re-adopted or\n\t\t// recreated on the next `up` (create is adopt-or-create). `read` never\n\t\t// fabricates drift and never hard-fails.\n\t\tconst resolvedId = volumeId ?? props.volumeId ?? id;\n\n\t\treturn { id: resolvedId, props: { ...props, volumeId: resolvedId } };\n\t}\n\n\tasync delete(id: string, props: RailwayVolumeOutputs): Promise<void> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\ttry {\n\t\t\tawait client.query(VOLUME_DELETE, { volumeId: id });\n\t\t} catch {\n\t\t\tpulumi.log.warn(\n\t\t\t\t\"Failed to delete Railway volume (may already be deleted)\",\n\t\t\t);\n\t\t}\n\t}\n\n\tasync diff(\n\t\t_id: string,\n\t\tolds: RailwayVolumeOutputs,\n\t\tnews: RailwayVolumeInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\n\t\tif (olds.serviceId !== news.serviceId) {\n\t\t\treplaces.push(\"serviceId\");\n\t\t}\n\n\t\tif (olds.mountPath !== news.mountPath) {\n\t\t\treplaces.push(\"mountPath\");\n\t\t}\n\n\t\tif (olds.environmentId !== news.environmentId) {\n\t\t\treplaces.push(\"environmentId\");\n\t\t}\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\treplaces.push(\"projectId\");\n\t\t}\n\n\t\treturn {\n\t\t\tchanges: replaces.length > 0,\n\t\t\treplaces,\n\t\t\tdeleteBeforeReplace: true,\n\t\t};\n\t}\n}\n\n/** Internal dynamic resource — not part of the public API. */\nclass RailwayVolumeResource extends pulumi.dynamic.Resource {\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\ttoken: pulumi.Input<string>;\n\t\t\tprojectId: pulumi.Input<string>;\n\t\t\tserviceId: pulumi.Input<string>;\n\t\t\tenvironmentId: pulumi.Input<string>;\n\t\t\tmountPath: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew RailwayVolumeResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, volumeId: undefined },\n\t\t\topts,\n\t\t);\n\t}\n}\n\n/** Options type for RailwayVolume — replaces Pulumi's native `provider` field. */\ntype RailwayVolumeOptions = Omit<\n\tpulumi.ComponentResourceOptions,\n\t\"provider\"\n> & {\n\t/** Railway authentication context. */\n\tprovider: RailwayProvider;\n\n\t/** Railway project context. */\n\tproject: RailwayProject;\n\n\t/** Railway environment context. */\n\tenvironment: RailwayEnvironment;\n\n\t/** Railway service context. */\n\tservice: RailwayService;\n};\n\n/** Args for RailwayVolume. */\nexport interface RailwayVolumeArgs {\n\t/** Absolute path inside the container where the volume is mounted. */\n\tmountPath: pulumi.Input<string>;\n}\n\n/**\n * Manages a Railway persistent volume with adopt-or-create semantics.\n *\n * @example\n * ```typescript\n * new RailwayVolume(\"api-data\", {\n * mountPath: \"/data\",\n * }, { provider, project, environment, service });\n * ```\n */\nexport class RailwayVolume extends pulumi.ComponentResource {\n\tconstructor(\n\t\tname: string,\n\t\targs: RailwayVolumeArgs,\n\t\topts: RailwayVolumeOptions,\n\t) {\n\t\tconst { provider, project, environment, service, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:railway:Volume\", name, {}, pulumiOpts);\n\n\t\tnew RailwayVolumeResource(\n\t\t\t`${name}-resource`,\n\t\t\t{\n\t\t\t\ttoken: provider.token,\n\t\t\t\tprojectId: project.id,\n\t\t\t\tserviceId: service.id,\n\t\t\t\tenvironmentId: environment.id,\n\t\t\t\tmountPath: args.mountPath,\n\t\t\t},\n\t\t\t// Forward the consumer's resource options to the underlying resource. Pulumi\n\t\t\t// auto-inherits `provider`/`protect` from the parent component, but NOT options\n\t\t\t// like `retainOnDelete` — without this pass-through, setting `retainOnDelete` on a\n\t\t\t// RailwayVolume would silently never reach the actual cloud volume.\n\t\t\tpulumi.mergeOptions(pulumiOpts, { parent: this }),\n\t\t);\n\n\t\tthis.registerOutputs({});\n\t}\n}\n"],"mappings":";;;;;AA+BA,MAAM,gBAAgB;;;;;;;;AAStB,MAAM,gBAAgB;;;;;AAMtB,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BtB,eAAe,oBACd,QACA,WACA,WAC8B;CAwB9B,QANc,MAjBO,OAAO,MAezB,eAAe,EAAE,UAAU,CAAC,GAEV,QAAQ,QAAQ,MAAM,MAAM,SAChD,KAAK,KAAK,gBAAgB,MAAM,MAC9B,OAAO,GAAG,KAAK,cAAc,SAC/B,CAGU,GAAG,KAAK;AACpB;;;;;;;;AASA,IAAa,gCAAb,MAEA;CACC,MAAM,OACL,QACuC;EACvC,MAAM,SAAS,IAAI,cAAc,OAAO,KAAK;EAE7C,IAAI,WAAW,MAAM,oBACpB,QACA,OAAO,WACP,OAAO,SACR;EAEA,IAAI,UACH,OAAO,IAAI,KAAK,yCAAyC,SAAS,EAAE;OAapE,YAAW,MAXU,OAAO,MAEzB,eAAe,EACjB,OAAO;GACN,WAAW,OAAO;GAClB,WAAW,OAAO;GAClB,eAAe,OAAO;GACtB,WAAW,OAAO;EACnB,EACD,CAAC,GAEiB,aAAa;EAGhC,OAAO;GACN,IAAI;GACJ,MAAM;IAAE,GAAG;IAAQ;GAAS;EAC7B;CACD;CAEA,MAAM,KACL,IACA,OACqC;EACrC,MAAM,SAAS,IAAI,cAAc,MAAM,KAAK;EAE5C,IAAI;EAEJ,IAAI;GACH,WAAW,MAAM,oBAChB,QACA,MAAM,WACN,MAAM,SACP;EACD,SAAS,OAAO;GACf,OAAO,IAAI,KACV,iEAAiE,OAAO,KAAK,GAC9E;EACD;EAQA,MAAM,aAAa,YAAY,MAAM,YAAY;EAEjD,OAAO;GAAE,IAAI;GAAY,OAAO;IAAE,GAAG;IAAO,UAAU;GAAW;EAAE;CACpE;CAEA,MAAM,OAAO,IAAY,OAA4C;EACpE,MAAM,SAAS,IAAI,cAAc,MAAM,KAAK;EAE5C,IAAI;GACH,MAAM,OAAO,MAAM,eAAe,EAAE,UAAU,GAAG,CAAC;EACnD,QAAQ;GACP,OAAO,IAAI,KACV,0DACD;EACD;CACD;CAEA,MAAM,KACL,KACA,MACA,MACqC;EACrC,MAAM,WAAqB,CAAC;EAE5B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,IAAI,KAAK,kBAAkB,KAAK,eAC/B,SAAS,KAAK,eAAe;EAG9B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,OAAO;GACN,SAAS,SAAS,SAAS;GAC3B;GACA,qBAAqB;EACtB;CACD;AACD;;AAGA,IAAM,wBAAN,cAAoC,OAAO,QAAQ,SAAS;CAC3D,YACC,MACA,MAOA,MACC;EACD,MACC,IAAI,8BAA8B,GAClC,MACA;GAAE,GAAG;GAAM,UAAU;EAAU,GAC/B,IACD;CACD;AACD;;;;;;;;;;;AAoCA,IAAa,gBAAb,cAAmC,OAAO,kBAAkB;CAC3D,YACC,MACA,MACA,MACC;EACD,MAAM,EAAE,UAAU,SAAS,aAAa,SAAS,GAAG,eAAe;EAEnE,MAAM,6BAA6B,MAAM,CAAC,GAAG,UAAU;EAEvD,IAAI,sBACH,GAAG,KAAK,YACR;GACC,OAAO,SAAS;GAChB,WAAW,QAAQ;GACnB,WAAW,QAAQ;GACnB,eAAe,YAAY;GAC3B,WAAW,KAAK;EACjB,GAKA,OAAO,aAAa,YAAY,EAAE,QAAQ,KAAK,CAAC,CACjD;EAEA,KAAK,gBAAgB,CAAC,CAAC;CACxB;AACD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@infracraft/pulumi",
3
- "version": "1.16.6",
3
+ "version": "1.16.8",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {