@geekmidas/cloud 10.0.0-alpha.13 → 10.0.0-alpha.14
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geekmidas/cloud",
|
|
3
|
-
"version": "10.0.0-alpha.
|
|
3
|
+
"version": "10.0.0-alpha.14",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"lodash.snakecase": "~4.1.1",
|
|
44
|
-
"@geekmidas/envkit": "10.0.0-alpha.
|
|
45
|
-
"@geekmidas/manifest": "10.0.0-alpha.
|
|
44
|
+
"@geekmidas/envkit": "10.0.0-alpha.14",
|
|
45
|
+
"@geekmidas/manifest": "10.0.0-alpha.14"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/lodash.get": "~4.4.9",
|
|
@@ -58,10 +58,10 @@
|
|
|
58
58
|
"@pulumi/aws": ">=7.47.0",
|
|
59
59
|
"@pulumi/random": ">=4.21.2",
|
|
60
60
|
"@pulumi/pulumi": ">=3.263.0",
|
|
61
|
-
"@geekmidas/manifest": "^10.0.0-alpha.
|
|
62
|
-
"@geekmidas/storage": "^10.0.0-alpha.
|
|
63
|
-
"@geekmidas/events": "^10.0.0-alpha.
|
|
64
|
-
"@geekmidas/db": "^10.0.0-alpha.
|
|
61
|
+
"@geekmidas/manifest": "^10.0.0-alpha.14",
|
|
62
|
+
"@geekmidas/storage": "^10.0.0-alpha.14",
|
|
63
|
+
"@geekmidas/events": "^10.0.0-alpha.14",
|
|
64
|
+
"@geekmidas/db": "^10.0.0-alpha.14"
|
|
65
65
|
},
|
|
66
66
|
"peerDependenciesMeta": {
|
|
67
67
|
"@geekmidas/manifest": {
|
|
@@ -195,6 +195,45 @@ const provider: dynamic.ResourceProvider<
|
|
|
195
195
|
},
|
|
196
196
|
};
|
|
197
197
|
|
|
198
|
+
/**
|
|
199
|
+
* A Dokploy API call that answered with an error status.
|
|
200
|
+
*
|
|
201
|
+
* Defined here, beside the provider, rather than imported: the provider's
|
|
202
|
+
* functions are serialised into state, and Pulumi's closure serialiser
|
|
203
|
+
* captures a class declared in the same module by value — once, shared by
|
|
204
|
+
* every function that refers to it, so `instanceof` still holds in the
|
|
205
|
+
* deployed provider. An imported class would have to be resolvable as a
|
|
206
|
+
* module where the provider runs, which is the obscure failure the note on
|
|
207
|
+
* `call` below is about.
|
|
208
|
+
*
|
|
209
|
+
* Serialisation rewrites the class into a plain function and drops its link to
|
|
210
|
+
* `Error.prototype`, so the constructor sets `message` and `stack` itself and
|
|
211
|
+
* the class says how to print itself. `Application.provider.spec.ts` runs the
|
|
212
|
+
* serialised provider to keep all of this true.
|
|
213
|
+
*/
|
|
214
|
+
export class DokployCallFailed extends Error {
|
|
215
|
+
constructor(
|
|
216
|
+
readonly path: string,
|
|
217
|
+
readonly status: number,
|
|
218
|
+
readonly statusText: string,
|
|
219
|
+
readonly detail: string,
|
|
220
|
+
) {
|
|
221
|
+
const message = `Dokploy ${path} failed: ${status} ${statusText}${detail ? ` — ${detail}` : ''}. Check the endpoint and API token, and Dokploy's own logs for the request.`;
|
|
222
|
+
super(message);
|
|
223
|
+
// Serialised, this constructor becomes a plain function whose super call
|
|
224
|
+
// does not initialise `this`: without these the provider host, which
|
|
225
|
+
// reports `e.message` and logs `${e}: ${e.stack}`, would show nothing.
|
|
226
|
+
this.message = message;
|
|
227
|
+
Error.captureStackTrace?.(this, DokployCallFailed);
|
|
228
|
+
this.name = 'DokployCallFailed';
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** What `${error}` prints once serialisation has dropped Error's own. */
|
|
232
|
+
override toString(): string {
|
|
233
|
+
return `${this.name}: ${this.message}`;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
198
237
|
/**
|
|
199
238
|
* One call to Dokploy.
|
|
200
239
|
*
|
|
@@ -218,10 +257,11 @@ async function call<T>(
|
|
|
218
257
|
|
|
219
258
|
if (!response.ok) {
|
|
220
259
|
const detail = await response.text().catch(() => '');
|
|
221
|
-
throw new
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
260
|
+
throw new DokployCallFailed(
|
|
261
|
+
path,
|
|
262
|
+
response.status,
|
|
263
|
+
response.statusText,
|
|
264
|
+
detail,
|
|
225
265
|
);
|
|
226
266
|
}
|
|
227
267
|
|
|
@@ -230,7 +270,7 @@ async function call<T>(
|
|
|
230
270
|
|
|
231
271
|
/** Whether an error means the thing is already absent. */
|
|
232
272
|
function isNotFound(error: unknown): boolean {
|
|
233
|
-
return error instanceof
|
|
273
|
+
return error instanceof DokployCallFailed && error.status === 404;
|
|
234
274
|
}
|
|
235
275
|
|
|
236
276
|
export interface ApplicationArgs {
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
import { runInNewContext } from 'node:vm';
|
|
2
|
+
import { runtime } from '@pulumi/pulumi';
|
|
1
3
|
import { HttpResponse, http } from 'msw';
|
|
2
4
|
import { setupServer } from 'msw/node';
|
|
3
5
|
import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest';
|
|
4
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
DokployCallFailed,
|
|
8
|
+
applicationProvider as provider,
|
|
9
|
+
} from '../Application';
|
|
5
10
|
|
|
6
11
|
/**
|
|
7
12
|
* The provider against Dokploy's tRPC API, served by MSW: what each lifecycle
|
|
@@ -132,8 +137,13 @@ describe('Dokploy application provider', () => {
|
|
|
132
137
|
}),
|
|
133
138
|
);
|
|
134
139
|
await expect(provider.delete!('app-1', outs)).rejects.toThrow(
|
|
135
|
-
|
|
140
|
+
DokployCallFailed,
|
|
136
141
|
);
|
|
142
|
+
await expect(provider.delete!('app-1', outs)).rejects.toMatchObject({
|
|
143
|
+
path: 'application.remove',
|
|
144
|
+
status: 500,
|
|
145
|
+
detail: 'database is locked',
|
|
146
|
+
});
|
|
137
147
|
|
|
138
148
|
server.resetHandlers();
|
|
139
149
|
answer(
|
|
@@ -141,8 +151,56 @@ describe('Dokploy application provider', () => {
|
|
|
141
151
|
'application.create',
|
|
142
152
|
() => new HttpResponse(null, { status: 401, statusText: 'Unauthorized' }),
|
|
143
153
|
);
|
|
144
|
-
await expect(provider.create!(inputs)).rejects.
|
|
145
|
-
|
|
154
|
+
await expect(provider.create!(inputs)).rejects.toMatchObject({
|
|
155
|
+
name: 'DokployCallFailed',
|
|
156
|
+
status: 401,
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* What actually runs: Pulumi serialises the provider into state and the
|
|
162
|
+
* deploy runs that text, not this module. Serialisation rewrites
|
|
163
|
+
* `DokployCallFailed` into a plain function that drops `Error.prototype` and
|
|
164
|
+
* never initialises `message` — which the provider host reports — so this
|
|
165
|
+
* runs the serialised provider and checks the error still says something.
|
|
166
|
+
*/
|
|
167
|
+
it('still recognises a 404, and reports a failure, once serialised', async () => {
|
|
168
|
+
const serialised = await runtime.serializeFunction(() => provider, {
|
|
169
|
+
isFactoryFunction: true,
|
|
170
|
+
});
|
|
171
|
+
const module = { exports: {} as Record<string, typeof provider> };
|
|
172
|
+
runInNewContext(serialised.text, {
|
|
173
|
+
module,
|
|
174
|
+
exports: module.exports,
|
|
175
|
+
require,
|
|
176
|
+
global: globalThis,
|
|
177
|
+
fetch: (...args: Parameters<typeof fetch>) => fetch(...args),
|
|
178
|
+
Error,
|
|
179
|
+
});
|
|
180
|
+
const deployed = module.exports[serialised.exportName]!;
|
|
181
|
+
|
|
182
|
+
answer('post', 'application.remove', () =>
|
|
183
|
+
HttpResponse.json({}, { status: 404, statusText: 'Not Found' }),
|
|
184
|
+
);
|
|
185
|
+
await expect(deployed.delete!('app-1', outs)).resolves.toBeUndefined();
|
|
186
|
+
|
|
187
|
+
server.resetHandlers();
|
|
188
|
+
answer('post', 'application.remove', () =>
|
|
189
|
+
HttpResponse.text('database is locked', {
|
|
190
|
+
status: 500,
|
|
191
|
+
statusText: 'Internal Server Error',
|
|
192
|
+
}),
|
|
193
|
+
);
|
|
194
|
+
const failure = await deployed.delete!('app-1', outs).catch(
|
|
195
|
+
(error: unknown) => error as Error & { status: number },
|
|
196
|
+
);
|
|
197
|
+
expect(failure).toMatchObject({ name: 'DokployCallFailed', status: 500 });
|
|
198
|
+
expect(failure?.message).toContain(
|
|
199
|
+
'Dokploy application.remove failed: 500 Internal Server Error — database is locked',
|
|
200
|
+
);
|
|
201
|
+
expect(failure?.stack).toBeTruthy();
|
|
202
|
+
expect(`${failure}`).toMatch(
|
|
203
|
+
/^DokployCallFailed: Dokploy application\.remove failed/,
|
|
146
204
|
);
|
|
147
205
|
});
|
|
148
206
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { App } from '../App';
|
|
3
|
-
import { Api } from '../aws/Api';
|
|
3
|
+
import { Api, RoutesMissingEnvironment } from '../aws/Api';
|
|
4
4
|
import { type GkmLinkable, ResourceType } from '../Linkable';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -167,6 +167,7 @@ describe('Api', () => {
|
|
|
167
167
|
],
|
|
168
168
|
});
|
|
169
169
|
|
|
170
|
+
expect(build).toThrow(RoutesMissingEnvironment);
|
|
170
171
|
expect(build).toThrow(/Http POST \/pay[\s\S]*STRIPE_KEY/);
|
|
171
172
|
expect(build).toThrow(/Http GET \/mail[\s\S]*SMTP_URL/);
|
|
172
173
|
});
|
package/src/sst/aws/Api.ts
CHANGED
|
@@ -153,7 +153,7 @@ export class Api<
|
|
|
153
153
|
// Fail the whole synth if any route is misconfigured, with one actionable
|
|
154
154
|
// message per offending route.
|
|
155
155
|
if (failures.length) {
|
|
156
|
-
throw new
|
|
156
|
+
throw new RoutesMissingEnvironment(id, failures);
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
159
|
|
|
@@ -307,3 +307,18 @@ export interface ApiProps<TAuthorizers extends Record<string, unknown> = {}>
|
|
|
307
307
|
* may override it. */
|
|
308
308
|
runtime?: sst.aws.FunctionArgs['runtime'];
|
|
309
309
|
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Routes that read variables nothing links or whitelists. Collected across
|
|
313
|
+
* every route so one synth reports all of them, each with its own suggestions,
|
|
314
|
+
* rather than stopping at the first.
|
|
315
|
+
*/
|
|
316
|
+
export class RoutesMissingEnvironment extends Error {
|
|
317
|
+
constructor(
|
|
318
|
+
readonly api: string,
|
|
319
|
+
readonly failures: readonly EnvValidationError[],
|
|
320
|
+
) {
|
|
321
|
+
super(failures.map((f) => f.message).join('\n\n'));
|
|
322
|
+
this.name = 'RoutesMissingEnvironment';
|
|
323
|
+
}
|
|
324
|
+
}
|