@geekmidas/cloud 10.0.0-alpha.37 → 10.0.0-alpha.39
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 +7 -7
- package/src/sst/__tests__/fromManifest.integration.spec.ts +27 -0
- package/src/sst/__tests__/fromManifest.spec.ts +141 -5
- package/src/sst/aws/Credential.ts +42 -9
- package/src/sst/aws/ExternalApi.ts +58 -0
- package/src/sst/aws/Queue.ts +31 -1
- package/src/sst/fromManifest.ts +58 -4
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.39",
|
|
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.39",
|
|
45
|
+
"@geekmidas/manifest": "10.0.0-alpha.39"
|
|
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.39",
|
|
62
|
+
"@geekmidas/storage": "^10.0.0-alpha.39",
|
|
63
|
+
"@geekmidas/events": "^10.0.0-alpha.39",
|
|
64
|
+
"@geekmidas/db": "^10.0.0-alpha.39"
|
|
65
65
|
},
|
|
66
66
|
"peerDependenciesMeta": {
|
|
67
67
|
"@geekmidas/manifest": {
|
|
@@ -44,6 +44,12 @@ function manifest(): ConstructManifest {
|
|
|
44
44
|
id: 'Emails',
|
|
45
45
|
fifo: true,
|
|
46
46
|
provides: keys('Emails', 'queue', ['publisherConnectionString']),
|
|
47
|
+
// The queue's one consumer, as the build folds it in.
|
|
48
|
+
worker: {
|
|
49
|
+
id: 'EmailsWorker',
|
|
50
|
+
handler: '.gkm/aws-lambda/queues/emails.handler',
|
|
51
|
+
dependencies: [{ target: 'Uploads', kind: 'objects' }],
|
|
52
|
+
},
|
|
47
53
|
},
|
|
48
54
|
Users: {
|
|
49
55
|
kind: 'topic',
|
|
@@ -172,6 +178,27 @@ describe('fromManifest', () => {
|
|
|
172
178
|
expect(argsOf(provisioned.Orders)).toMatchObject({ vpc, version: '18' });
|
|
173
179
|
});
|
|
174
180
|
|
|
181
|
+
it('subscribes each queue’s one consumer, linked to the queue and what it reaches', () => {
|
|
182
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
183
|
+
|
|
184
|
+
const provisioned = fromManifest(stack, manifest(), overrides, {
|
|
185
|
+
cache: 'db',
|
|
186
|
+
email: 'smtp',
|
|
187
|
+
});
|
|
188
|
+
const { subscribed } = provisioned.Emails as unknown as {
|
|
189
|
+
subscribed: { subscriber: { handler: string; link: unknown[] } }[];
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
expect(subscribed).toHaveLength(1);
|
|
193
|
+
expect(subscribed[0]?.subscriber.handler).toBe(
|
|
194
|
+
'.gkm/aws-lambda/queues/emails.handler',
|
|
195
|
+
);
|
|
196
|
+
expect(subscribed[0]?.subscriber.link).toEqual([
|
|
197
|
+
provisioned.Emails,
|
|
198
|
+
provisioned.Uploads,
|
|
199
|
+
]);
|
|
200
|
+
});
|
|
201
|
+
|
|
175
202
|
it('builds the site against the addresses its edges resolve to', () => {
|
|
176
203
|
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
177
204
|
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { resolveEnvKeys } from '@geekmidas/envkit/sst';
|
|
2
|
-
import { providedKeyFor, provideKey } from '@geekmidas/manifest';
|
|
2
|
+
import { NoUrlForStage, providedKeyFor, provideKey } from '@geekmidas/manifest';
|
|
3
3
|
import { describe, expect, it } from 'vitest';
|
|
4
4
|
import { ObjectStorage } from '../aws/ObjectStorage';
|
|
5
5
|
import { type ProvidesMismatch, UnknownDeclarationKind } from '../errors';
|
|
6
6
|
import {
|
|
7
7
|
assertProvides,
|
|
8
|
+
PROVISIONED_ELSEWHERE,
|
|
8
9
|
type ProvisionedManifest,
|
|
9
10
|
provisionerFor,
|
|
10
11
|
resolveEdges,
|
|
12
|
+
subscribeConsumers,
|
|
11
13
|
} from '../fromManifest';
|
|
12
14
|
|
|
13
15
|
/**
|
|
@@ -249,18 +251,152 @@ describe('the key a provided role becomes', () => {
|
|
|
249
251
|
describe('Credential', () => {
|
|
250
252
|
it('provides under the role the declaration named', async () => {
|
|
251
253
|
// The role *is* the contract: providing `value` against a declared
|
|
252
|
-
// `
|
|
254
|
+
// `STRIPE_CREDENTIALS` supplies `STRIPE_VALUE`, and assertProvides
|
|
253
255
|
// rejects the stack at synth. Which is the check working.
|
|
254
256
|
const { Credential } = await import('../aws/Credential');
|
|
255
257
|
|
|
256
258
|
expect(
|
|
257
259
|
Object.keys(new Credential({} as never, 'Stripe').provides()),
|
|
258
|
-
).toEqual(['
|
|
260
|
+
).toEqual(['credentials']);
|
|
259
261
|
});
|
|
260
262
|
|
|
261
263
|
it('agrees with what the construct declares', () => {
|
|
262
|
-
expect(providedKeyFor('Stripe', 'credential', '
|
|
263
|
-
'
|
|
264
|
+
expect(providedKeyFor('Stripe', 'credential', 'credentials')).toBe(
|
|
265
|
+
'STRIPE_CREDENTIALS',
|
|
264
266
|
);
|
|
265
267
|
});
|
|
268
|
+
|
|
269
|
+
it('links under the key the construct reads, not the secret’s bare name', async () => {
|
|
270
|
+
const { Credential } = await import('../aws/Credential');
|
|
271
|
+
const stripe = new Credential({} as never, 'Stripe');
|
|
272
|
+
|
|
273
|
+
expect(resolveEnvKeys({ Stripe: { type: stripe._type } })).toEqual([
|
|
274
|
+
'STRIPE_CREDENTIALS',
|
|
275
|
+
]);
|
|
276
|
+
expect(stripe.getSSTLink().properties).toEqual(stripe.provides());
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('is what a function depending on it is linked to', async () => {
|
|
280
|
+
const { Credential } = await import('../aws/Credential');
|
|
281
|
+
const stripe = new Credential({} as never, 'Stripe');
|
|
282
|
+
|
|
283
|
+
const { link, envKeys } = resolveEdges(
|
|
284
|
+
[{ target: 'Stripe', kind: 'credential' }],
|
|
285
|
+
{ Stripe: stripe },
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
expect(link).toEqual([stripe]);
|
|
289
|
+
expect(envKeys).toEqual(['STRIPE_CREDENTIALS']);
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
describe('ExternalApi', () => {
|
|
294
|
+
const stack = (stage: string) => ({ stage }) as never;
|
|
295
|
+
|
|
296
|
+
it('provides the URL for its stage and its credentials, under the declared keys', async () => {
|
|
297
|
+
const { ExternalApi } = await import('../aws/ExternalApi');
|
|
298
|
+
const payfast = new ExternalApi(stack('production'), 'PayFast', {
|
|
299
|
+
url: {
|
|
300
|
+
production: 'https://www.payfast.co.za',
|
|
301
|
+
default: 'https://sandbox.payfast.co.za',
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
expect(payfast.provides().url).toBe('https://www.payfast.co.za');
|
|
306
|
+
expect(
|
|
307
|
+
Object.keys(payfast.provides()).map((role) =>
|
|
308
|
+
providedKeyFor('PayFast', 'external-api', role),
|
|
309
|
+
),
|
|
310
|
+
).toEqual(['PAY_FAST_URL', 'PAY_FAST_CREDENTIALS']);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('falls back to the default URL for a stage it does not name', async () => {
|
|
314
|
+
const { ExternalApi } = await import('../aws/ExternalApi');
|
|
315
|
+
const payfast = new ExternalApi(stack('staging'), 'PayFast', {
|
|
316
|
+
url: {
|
|
317
|
+
production: 'https://www.payfast.co.za',
|
|
318
|
+
default: 'https://sandbox.payfast.co.za',
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
expect(payfast.provides().url).toBe('https://sandbox.payfast.co.za');
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('fails synth for a stage it has no URL for', async () => {
|
|
326
|
+
const { ExternalApi } = await import('../aws/ExternalApi');
|
|
327
|
+
|
|
328
|
+
expect(
|
|
329
|
+
() =>
|
|
330
|
+
new ExternalApi(stack('staging'), 'PayFast', {
|
|
331
|
+
url: { production: 'https://www.payfast.co.za' },
|
|
332
|
+
}),
|
|
333
|
+
).toThrow(NoUrlForStage);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('flattens into the same keys at runtime', () => {
|
|
337
|
+
expect(
|
|
338
|
+
resolveEnvKeys({ PayFast: { type: 'gkm:aws:ExternalApi' } }).sort(),
|
|
339
|
+
).toEqual(['PAY_FAST_CREDENTIALS', 'PAY_FAST_URL']);
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
describe('a queue’s one consumer', () => {
|
|
344
|
+
it('is subscribed from the handler the build wrote, linked to what it depends on', () => {
|
|
345
|
+
const consumed: { handler: string; link?: unknown[] }[] = [];
|
|
346
|
+
const uploads = { _id: 'Uploads', _type: 'sst.aws.Bucket' };
|
|
347
|
+
const provisioned = {
|
|
348
|
+
Uploads: uploads,
|
|
349
|
+
Emails: {
|
|
350
|
+
_id: 'Emails',
|
|
351
|
+
_type: 'sst.aws.Queue',
|
|
352
|
+
consume: (consumer: { handler: string; link?: unknown[] }) =>
|
|
353
|
+
consumed.push(consumer),
|
|
354
|
+
},
|
|
355
|
+
} as unknown as ProvisionedManifest;
|
|
356
|
+
|
|
357
|
+
subscribeConsumers(
|
|
358
|
+
{
|
|
359
|
+
Uploads: { kind: 'objects', id: 'Uploads', provides: ['UPLOADS_URL'] },
|
|
360
|
+
Emails: {
|
|
361
|
+
kind: 'queue',
|
|
362
|
+
id: 'Emails',
|
|
363
|
+
provides: ['EMAILS_PUBLISHER_CONNECTION_STRING'],
|
|
364
|
+
worker: {
|
|
365
|
+
id: 'EmailsWorker',
|
|
366
|
+
handler: '.gkm/aws-lambda/queues/emails.handler',
|
|
367
|
+
dependencies: [{ target: 'Uploads', kind: 'objects' }],
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
} as never,
|
|
371
|
+
provisioned,
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
expect(consumed).toEqual([
|
|
375
|
+
{ handler: '.gkm/aws-lambda/queues/emails.handler', link: [uploads] },
|
|
376
|
+
]);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it('subscribes through SST, with the queue itself linked', async () => {
|
|
380
|
+
const { Queue } = await import('../aws/Queue');
|
|
381
|
+
const queue = new Queue({} as never, 'Emails');
|
|
382
|
+
|
|
383
|
+
queue.consume({ handler: 'emails.handler', link: [], batchSize: 5 });
|
|
384
|
+
|
|
385
|
+
const [{ subscriber, args }] = (
|
|
386
|
+
queue as unknown as { subscribed: { subscriber: any; args: unknown }[] }
|
|
387
|
+
).subscribed;
|
|
388
|
+
expect(subscriber.handler).toBe('emails.handler');
|
|
389
|
+
expect(subscriber.link).toEqual([queue]);
|
|
390
|
+
expect(args).toEqual({ batch: { size: 5 } });
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
describe('kinds provisioned elsewhere', () => {
|
|
395
|
+
it('skips a worker, which is a process rather than a resource', () => {
|
|
396
|
+
// A worker in the manifest used to throw UnknownDeclarationKind at synth,
|
|
397
|
+
// so any app with background work could not be deployed to AWS at all.
|
|
398
|
+
expect(PROVISIONED_ELSEWHERE.has('worker')).toBe(true);
|
|
399
|
+
expect(PROVISIONED_ELSEWHERE.has('mobile-app')).toBe(true);
|
|
400
|
+
expect(PROVISIONED_ELSEWHERE.has('queue')).toBe(false);
|
|
401
|
+
});
|
|
266
402
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type GkmLinkable, ResourceType } from '../Linkable';
|
|
2
|
+
import type { StackType } from '../Stack';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* `Credential` — a third-party credential SST holds, and the infra half of the
|
|
@@ -8,21 +9,53 @@ import { Secret, type SecretProps } from './Secret';
|
|
|
8
9
|
* the lifecycle rather than the mechanism: a secret is generated and rotated by
|
|
9
10
|
* the platform, while a credential is issued by someone else and has a shape
|
|
10
11
|
* the construct validates on the way in. Both are values you set out of band
|
|
11
|
-
* with `sst secret set
|
|
12
|
+
* with `sst secret set`, under the construct's id.
|
|
12
13
|
*
|
|
13
|
-
* The role is `
|
|
14
|
+
* The role is `credentials` rather than `value`, and that is not cosmetic. The
|
|
14
15
|
* role *is* the contract — `providedKeyFor` turns it into the key the app
|
|
15
16
|
* declared — so a credential providing `value` would supply `STRIPE_VALUE`
|
|
16
|
-
* against a declared `
|
|
17
|
-
* stack at synth.
|
|
17
|
+
* against a declared `STRIPE_CREDENTIALS`, and `assertProvides` would reject
|
|
18
|
+
* the stack at synth.
|
|
19
|
+
*
|
|
20
|
+
* It holds an `sst.Secret` rather than being one, and reports its own type,
|
|
21
|
+
* for the same reason one level down: the type decides the keys a link
|
|
22
|
+
* resolves to. As an SST secret it resolved to `STRIPE`, so a function that
|
|
23
|
+
* declared `STRIPE_CREDENTIALS` was linked to nothing.
|
|
18
24
|
*/
|
|
19
25
|
export class Credential<
|
|
20
26
|
TStage extends string = string,
|
|
21
27
|
TDomain extends string = string,
|
|
22
|
-
>
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
> implements GkmLinkable
|
|
29
|
+
{
|
|
30
|
+
readonly _id: string;
|
|
31
|
+
|
|
32
|
+
private readonly secret: sst.Secret;
|
|
33
|
+
|
|
34
|
+
get _type() {
|
|
35
|
+
return ResourceType.Credential;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
constructor(
|
|
39
|
+
_stack: StackType<TStage, TDomain>,
|
|
40
|
+
name: string,
|
|
41
|
+
props: CredentialProps = {},
|
|
42
|
+
) {
|
|
43
|
+
this._id = name;
|
|
44
|
+
// The id itself, as it always was: a value already set with
|
|
45
|
+
// `sst secret set Stripe …` is still the one read.
|
|
46
|
+
this.secret = new sst.Secret(name, props.placeholder);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
provides(): Record<string, $util.Input<string>> {
|
|
50
|
+
return { credentials: this.secret.value };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
getSSTLink() {
|
|
54
|
+
return { properties: { ...this.provides() } };
|
|
25
55
|
}
|
|
26
56
|
}
|
|
27
57
|
|
|
28
|
-
export interface CredentialProps
|
|
58
|
+
export interface CredentialProps {
|
|
59
|
+
/** A value until one is set, for a preview stage. */
|
|
60
|
+
placeholder?: string;
|
|
61
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { externalApiUrl } from '@geekmidas/manifest';
|
|
2
|
+
import { type GkmLinkable, ResourceType } from '../Linkable';
|
|
3
|
+
import type { StackType } from '../Stack';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `ExternalApi` — the infra half of the `external-api` kind.
|
|
7
|
+
*
|
|
8
|
+
* Nothing is provisioned: the API is somebody else's. What this holds is the
|
|
9
|
+
* two facts a function needs to call it on this stage — the URL the
|
|
10
|
+
* declaration names for it, and the credentials the provider issued, kept as
|
|
11
|
+
* an SST secret and set out of band:
|
|
12
|
+
*
|
|
13
|
+
* ```
|
|
14
|
+
* sst secret set PolarCredentials '{"clientId":"…","clientSecret":"…"}' --stage prod
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export class ExternalApi<
|
|
18
|
+
TStage extends string = string,
|
|
19
|
+
TDomain extends string = string,
|
|
20
|
+
> implements GkmLinkable
|
|
21
|
+
{
|
|
22
|
+
readonly _id: string;
|
|
23
|
+
|
|
24
|
+
private readonly url: string;
|
|
25
|
+
|
|
26
|
+
private readonly credentials: sst.Secret;
|
|
27
|
+
|
|
28
|
+
get _type() {
|
|
29
|
+
return ResourceType.ExternalApi;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
constructor(
|
|
33
|
+
stack: StackType<TStage, TDomain>,
|
|
34
|
+
name: string,
|
|
35
|
+
props: ExternalApiProps,
|
|
36
|
+
) {
|
|
37
|
+
this._id = name;
|
|
38
|
+
// Read at synth, so a stage the declaration has no URL for fails the
|
|
39
|
+
// deploy — with the stage named — rather than the first request.
|
|
40
|
+
this.url = externalApiUrl({ id: name, url: props.url }, stack.stage);
|
|
41
|
+
this.credentials = new sst.Secret(`${name}Credentials`, props.placeholder);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
provides(): Record<string, $util.Input<string>> {
|
|
45
|
+
return { url: this.url, credentials: this.credentials.value };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
getSSTLink() {
|
|
49
|
+
return { properties: { ...this.provides() } };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ExternalApiProps {
|
|
54
|
+
/** The declaration's `url` — one for every stage, or one per stage name. */
|
|
55
|
+
url: string | Readonly<Record<string, string>>;
|
|
56
|
+
/** A value for the credentials secret until one is set, for a preview stage. */
|
|
57
|
+
placeholder?: string;
|
|
58
|
+
}
|
package/src/sst/aws/Queue.ts
CHANGED
|
@@ -11,7 +11,8 @@ import type { StackType } from '../Stack';
|
|
|
11
11
|
* work queue. Link it to a producer and the runtime resolves `<NAME>_URL`,
|
|
12
12
|
* `<NAME>_ARN`, and a `<NAME>_PUBLISHER_CONNECTION_STRING` (`sqs://?queueUrl=…`)
|
|
13
13
|
* that `@geekmidas/events`'s `Publisher.fromConnectionString` consumes. Its
|
|
14
|
-
*
|
|
14
|
+
* one consumer — the handler the queue was declared with — is subscribed by
|
|
15
|
+
* {@link Queue.consume}.
|
|
15
16
|
*
|
|
16
17
|
* SST's native `Queue` link exposes only `url`, so `getSSTLink` is overridden to
|
|
17
18
|
* also expose `arn` (what the resolver needs). `QueueProps` extends
|
|
@@ -73,6 +74,25 @@ export class Queue<
|
|
|
73
74
|
};
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Subscribe the queue's one consumer: a Lambda running `handler`, linked to
|
|
79
|
+
* what the consumer depends on and to this queue itself.
|
|
80
|
+
*
|
|
81
|
+
* A queue has exactly one consumer, so this is called once per queue — by
|
|
82
|
+
* `fromManifest`, from the handler the build wrote for it.
|
|
83
|
+
*/
|
|
84
|
+
consume(consumer: QueueConsumer) {
|
|
85
|
+
return this.subscribe(
|
|
86
|
+
{
|
|
87
|
+
handler: consumer.handler,
|
|
88
|
+
link: [this, ...(consumer.link ?? [])],
|
|
89
|
+
runtime: 'nodejs24.x',
|
|
90
|
+
...(consumer.timeout ? { timeout: consumer.timeout } : {}),
|
|
91
|
+
},
|
|
92
|
+
consumer.batchSize ? { batch: { size: consumer.batchSize } } : {},
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
76
96
|
override getSSTLink() {
|
|
77
97
|
const link = super.getSSTLink();
|
|
78
98
|
return {
|
|
@@ -82,6 +102,16 @@ export class Queue<
|
|
|
82
102
|
}
|
|
83
103
|
}
|
|
84
104
|
|
|
105
|
+
/** The queue's one consumer, as {@link Queue.consume} subscribes it. */
|
|
106
|
+
export interface QueueConsumer {
|
|
107
|
+
/** The built handler — `.gkm/aws-lambda/queues/emails.handler`. */
|
|
108
|
+
handler: string;
|
|
109
|
+
/** What the consumer depends on; the queue itself is always linked. */
|
|
110
|
+
link?: unknown[];
|
|
111
|
+
timeout?: `${number} seconds` | `${number} minutes`;
|
|
112
|
+
batchSize?: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
85
115
|
export interface QueueProps extends sst.aws.QueueArgs {
|
|
86
116
|
/**
|
|
87
117
|
* The physical queue name, where the auto-generated one will not do.
|
package/src/sst/fromManifest.ts
CHANGED
|
@@ -45,6 +45,7 @@ import { Database, DatabaseNeedsVpc, type DatabaseProps } from './aws/Database';
|
|
|
45
45
|
import { DatabaseBootstrap } from './aws/DatabaseBootstrap';
|
|
46
46
|
import { DatabaseReader, DatabaseSchema } from './aws/DerivedDatabase';
|
|
47
47
|
import { Email, EmailNeedsSender } from './aws/Email';
|
|
48
|
+
import { ExternalApi, type ExternalApiProps } from './aws/ExternalApi';
|
|
48
49
|
import { FileServer } from './aws/FileServer';
|
|
49
50
|
import { ObjectStorage } from './aws/ObjectStorage';
|
|
50
51
|
import { Queue } from './aws/Queue';
|
|
@@ -399,6 +400,19 @@ const PROVISIONERS: Partial<Record<DeclarationKind, Provisioner>> = {
|
|
|
399
400
|
|
|
400
401
|
// The same storage as a secret, under a different role — see the component.
|
|
401
402
|
credential: (stack, d, props) => new Credential(stack, d.id, props),
|
|
403
|
+
|
|
404
|
+
// Nothing to provision: the URL this stage calls, and the credentials kept
|
|
405
|
+
// as a secret.
|
|
406
|
+
'external-api': (stack, d, props) => {
|
|
407
|
+
if (d.kind !== 'external-api') {
|
|
408
|
+
throw new UnknownDeclarationKind(d.kind, []);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return new ExternalApi(stack, d.id, {
|
|
412
|
+
...(props as Partial<ExternalApiProps>),
|
|
413
|
+
url: d.url,
|
|
414
|
+
});
|
|
415
|
+
},
|
|
402
416
|
};
|
|
403
417
|
|
|
404
418
|
/**
|
|
@@ -736,6 +750,46 @@ export function isServed(id: string, manifest: ConstructManifest): boolean {
|
|
|
736
750
|
);
|
|
737
751
|
}
|
|
738
752
|
|
|
753
|
+
/**
|
|
754
|
+
* Kinds this stack provisions nothing for of their own.
|
|
755
|
+
*
|
|
756
|
+
* - `mobile-app` ships through EAS and the stores. It is in the manifest for
|
|
757
|
+
* what it makes the surfaces trust, which `callersOf` reads off the graph.
|
|
758
|
+
* - `worker` is a process, not a resource: what it runs is provisioned as
|
|
759
|
+
* what it is — a queue's consumer by {@link subscribeConsumers}.
|
|
760
|
+
* - `cron` and `function` are Lambdas built from `manifest.crons` and
|
|
761
|
+
* `manifest.functions` (`Cron.fromManifest`, `Function.fromManifest`).
|
|
762
|
+
*/
|
|
763
|
+
export const PROVISIONED_ELSEWHERE: ReadonlySet<DeclarationKind> =
|
|
764
|
+
new Set<DeclarationKind>(['mobile-app', 'worker', 'cron', 'function']);
|
|
765
|
+
|
|
766
|
+
/** A provisioned queue: what {@link subscribeConsumers} subscribes through. */
|
|
767
|
+
interface Consumable {
|
|
768
|
+
consume(consumer: { handler: string; link?: unknown[] }): unknown;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Subscribe every queue's one consumer — the handler the build wrote for it,
|
|
773
|
+
* linked to what that consumer depends on.
|
|
774
|
+
*
|
|
775
|
+
* After every construct is provisioned, because a consumer links to things
|
|
776
|
+
* that `provisionOrder` does not order before the queue.
|
|
777
|
+
*/
|
|
778
|
+
export function subscribeConsumers(
|
|
779
|
+
manifest: ConstructManifest,
|
|
780
|
+
provisioned: ProvisionedManifest,
|
|
781
|
+
): void {
|
|
782
|
+
for (const [id, declaration] of Object.entries(manifest)) {
|
|
783
|
+
if (declaration?.kind !== 'queue') continue;
|
|
784
|
+
|
|
785
|
+
const queue = provisioned[id] as unknown as Consumable | undefined;
|
|
786
|
+
if (!queue) continue;
|
|
787
|
+
|
|
788
|
+
const { link } = resolveEdges(declaration.worker.dependencies, provisioned);
|
|
789
|
+
queue.consume({ handler: declaration.worker.handler, link });
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
739
793
|
/** The provisioner for a kind. Pure — the lookup is testable without Pulumi. */
|
|
740
794
|
export function provisionerFor(kind: DeclarationKind): Provisioner {
|
|
741
795
|
const provisioner = PROVISIONERS[kind];
|
|
@@ -824,10 +878,7 @@ export function fromManifest(
|
|
|
824
878
|
for (const id of [...rest, ...sites]) {
|
|
825
879
|
const declaration = manifest[id];
|
|
826
880
|
if (!declaration) continue;
|
|
827
|
-
|
|
828
|
-
// in the manifest for what it makes the surfaces trust, which
|
|
829
|
-
// `callersOf` reads off the graph.
|
|
830
|
-
if (declaration.kind === 'mobile-app') continue;
|
|
881
|
+
if (PROVISIONED_ELSEWHERE.has(declaration.kind)) continue;
|
|
831
882
|
|
|
832
883
|
const component = provisionerFor(declaration.kind)(
|
|
833
884
|
stack,
|
|
@@ -851,6 +902,9 @@ export function fromManifest(
|
|
|
851
902
|
provisioned[id] = component;
|
|
852
903
|
}
|
|
853
904
|
|
|
905
|
+
// Each queue's one consumer, once everything it links to exists.
|
|
906
|
+
subscribeConsumers(manifest, provisioned);
|
|
907
|
+
|
|
854
908
|
for (const line of describeRoutes(manifest)) console.log(line);
|
|
855
909
|
|
|
856
910
|
// Now everything exists, so every surface's callers can be resolved — the
|