@geekmidas/cloud 10.0.0-alpha.44 → 10.0.0-alpha.46
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/README.md +14 -6
- package/package.json +7 -7
- package/src/sst/__tests__/api.spec.ts +0 -46
- package/src/sst/__tests__/fromManifest.spec.ts +167 -2
- package/src/sst/__tests__/function.spec.ts +0 -71
- package/src/sst/aws/Api.ts +0 -43
- package/src/sst/aws/Cron.ts +5 -45
- package/src/sst/aws/Function.ts +9 -38
- package/src/sst/aws/Queue.ts +4 -1
- package/src/sst/aws/RestApiSurface.ts +36 -14
- package/src/sst/fromManifest.ts +133 -19
- package/src/sst/__type-tests__/manifest.type-test.ts +0 -77
package/README.md
CHANGED
|
@@ -105,17 +105,25 @@ const cron = new Cron(stack, 'Nightly', { processor: fn, schedule: 'rate(1 day)'
|
|
|
105
105
|
|
|
106
106
|
### From a `gkm build` manifest
|
|
107
107
|
|
|
108
|
-
`gkm build`
|
|
109
|
-
|
|
108
|
+
The root `gkm build` writes the application's manifest to
|
|
109
|
+
`.gkm/manifest/aws.ts` (types in `@geekmidas/manifest`), and `fromManifest`
|
|
110
|
+
maps it straight into infra:
|
|
110
111
|
|
|
111
112
|
```ts
|
|
112
|
-
|
|
113
|
+
// sst.config.ts
|
|
114
|
+
const { App, fromManifest, Stack } = await import('@geekmidas/cloud/sst');
|
|
115
|
+
const { backends, constructs } = await import('./.gkm/manifest/aws.js');
|
|
113
116
|
|
|
114
|
-
|
|
115
|
-
const workers = Function.fromManifest(stack, functionsManifest, { links: [db] });
|
|
116
|
-
const crons = Cron.fromManifest(stack, cronsManifest, { links: [db] });
|
|
117
|
+
fromManifest(new Stack(app, 'Shop'), constructs, { Database: { vpc } }, backends);
|
|
117
118
|
```
|
|
118
119
|
|
|
120
|
+
`constructs` is every declared construct keyed by id; the overrides are keyed
|
|
121
|
+
by the same ids. One call provisions the resources, each `RestApi`'s endpoints
|
|
122
|
+
(each a Lambda linked only to its own dependencies, placed in the database's
|
|
123
|
+
VPC when it reaches one, with the `iam` authorizer enforced by the gateway),
|
|
124
|
+
queue consumers, every function (a Lambda with an IAM-authorized URL) and every
|
|
125
|
+
cron (a Lambda on its schedule).
|
|
126
|
+
|
|
119
127
|
## `@geekmidas/cloud/utils` (runtime)
|
|
120
128
|
|
|
121
129
|
```ts
|
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.46",
|
|
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.46",
|
|
45
|
+
"@geekmidas/manifest": "10.0.0-alpha.46"
|
|
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.46",
|
|
62
|
+
"@geekmidas/storage": "^10.0.0-alpha.46",
|
|
63
|
+
"@geekmidas/events": "^10.0.0-alpha.46",
|
|
64
|
+
"@geekmidas/db": "^10.0.0-alpha.46"
|
|
65
65
|
},
|
|
66
66
|
"peerDependenciesMeta": {
|
|
67
67
|
"@geekmidas/manifest": {
|
|
@@ -183,49 +183,3 @@ describe('Api', () => {
|
|
|
183
183
|
expect(recorded(api).authorizers).toEqual([]);
|
|
184
184
|
});
|
|
185
185
|
});
|
|
186
|
-
|
|
187
|
-
describe('Api.fromManifest', () => {
|
|
188
|
-
it('turns built routes, flat or partitioned, into the route table', () => {
|
|
189
|
-
const routes = {
|
|
190
|
-
public: [
|
|
191
|
-
{
|
|
192
|
-
method: 'GET',
|
|
193
|
-
path: '/orders',
|
|
194
|
-
handler: '.gkm/aws-lambda/routes/orders.handler',
|
|
195
|
-
environment: ['DB_URL'],
|
|
196
|
-
authorizer: 'iam',
|
|
197
|
-
timeout: 30,
|
|
198
|
-
memorySize: 1024,
|
|
199
|
-
},
|
|
200
|
-
],
|
|
201
|
-
internal: [
|
|
202
|
-
{
|
|
203
|
-
method: 'POST',
|
|
204
|
-
path: '/jobs',
|
|
205
|
-
handler: '.gkm/aws-lambda/routes/jobs.handler',
|
|
206
|
-
environment: [],
|
|
207
|
-
},
|
|
208
|
-
],
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
const api = Api.fromManifest(stack, 'Http', routes as never, {
|
|
212
|
-
links: [db],
|
|
213
|
-
});
|
|
214
|
-
const { routes: recordedRoutes } = recorded(api);
|
|
215
|
-
|
|
216
|
-
expect(recordedRoutes.map((r) => r.key)).toEqual([
|
|
217
|
-
'GET /orders',
|
|
218
|
-
'POST /jobs',
|
|
219
|
-
]);
|
|
220
|
-
expect(recordedRoutes[0]!.handler).toMatchObject({
|
|
221
|
-
timeout: '30 seconds',
|
|
222
|
-
memory: '1024 MB',
|
|
223
|
-
link: [db],
|
|
224
|
-
});
|
|
225
|
-
expect(recordedRoutes[0]!.args).toEqual({ auth: { iam: true } });
|
|
226
|
-
expect(recordedRoutes[1]!.handler).toMatchObject({
|
|
227
|
-
timeout: undefined,
|
|
228
|
-
memory: undefined,
|
|
229
|
-
});
|
|
230
|
-
});
|
|
231
|
-
});
|
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import { resolveEnvKeys } from '@geekmidas/envkit/sst';
|
|
2
2
|
import { NoUrlForStage, providedKeyFor, provideKey } from '@geekmidas/manifest';
|
|
3
3
|
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { App } from '../App';
|
|
5
|
+
import { Cron } from '../aws/Cron';
|
|
6
|
+
import { Database } from '../aws/Database';
|
|
7
|
+
import { DatabaseSchema } from '../aws/DerivedDatabase';
|
|
8
|
+
import { Function } from '../aws/Function';
|
|
4
9
|
import { ObjectStorage } from '../aws/ObjectStorage';
|
|
10
|
+
import { RestApiSurface } from '../aws/RestApiSurface';
|
|
5
11
|
import { type ProvidesMismatch, UnknownDeclarationKind } from '../errors';
|
|
6
12
|
import {
|
|
7
13
|
assertProvides,
|
|
14
|
+
mountRoutes,
|
|
8
15
|
PROVISIONED_ELSEWHERE,
|
|
9
16
|
type ProvisionedManifest,
|
|
17
|
+
provisionCompute,
|
|
10
18
|
provisionerFor,
|
|
11
19
|
resolveEdges,
|
|
12
20
|
subscribeConsumers,
|
|
21
|
+
vpcFor,
|
|
13
22
|
} from '../fromManifest';
|
|
14
23
|
|
|
15
24
|
/**
|
|
@@ -363,7 +372,7 @@ describe('a queue’s one consumer', () => {
|
|
|
363
372
|
provides: ['EMAILS_PUBLISHER_CONNECTION_STRING'],
|
|
364
373
|
worker: {
|
|
365
374
|
id: 'EmailsWorker',
|
|
366
|
-
handler: '
|
|
375
|
+
handler: 'apps/api/.gkm/aws/queues/emails.handler',
|
|
367
376
|
dependencies: [{ target: 'Uploads', kind: 'objects' }],
|
|
368
377
|
},
|
|
369
378
|
},
|
|
@@ -372,7 +381,7 @@ describe('a queue’s one consumer', () => {
|
|
|
372
381
|
);
|
|
373
382
|
|
|
374
383
|
expect(consumed).toEqual([
|
|
375
|
-
{ handler: '
|
|
384
|
+
{ handler: 'apps/api/.gkm/aws/queues/emails.handler', link: [uploads] },
|
|
376
385
|
]);
|
|
377
386
|
});
|
|
378
387
|
|
|
@@ -391,6 +400,162 @@ describe('a queue’s one consumer', () => {
|
|
|
391
400
|
});
|
|
392
401
|
});
|
|
393
402
|
|
|
403
|
+
describe('a surface’s endpoints', () => {
|
|
404
|
+
const routesOf = (surface: RestApiSurface) =>
|
|
405
|
+
(
|
|
406
|
+
surface as unknown as {
|
|
407
|
+
routes: { key: string; handler: any; args: unknown }[];
|
|
408
|
+
}
|
|
409
|
+
).routes;
|
|
410
|
+
|
|
411
|
+
it('are mounted from the handlers the build wrote, each linked to what it depends on', () => {
|
|
412
|
+
// An API Gateway with no routes 404s everything — which is what a deploy
|
|
413
|
+
// produced while the routes sat in the manifest with nothing reading them.
|
|
414
|
+
const api = new RestApiSurface({} as never, 'Api');
|
|
415
|
+
const uploads = { _id: 'Uploads', _type: 'sst.aws.Bucket' };
|
|
416
|
+
|
|
417
|
+
mountRoutes(
|
|
418
|
+
{
|
|
419
|
+
Uploads: { kind: 'objects', id: 'Uploads', provides: ['UPLOADS_URL'] },
|
|
420
|
+
Api: {
|
|
421
|
+
kind: 'rest-api',
|
|
422
|
+
id: 'Api',
|
|
423
|
+
provides: ['API_URL'],
|
|
424
|
+
endpoints: [
|
|
425
|
+
{
|
|
426
|
+
id: 'ApiGET/users/{id}',
|
|
427
|
+
method: 'GET',
|
|
428
|
+
path: '/users/{id}',
|
|
429
|
+
handler: 'apps/api/.gkm/aws/routes/getUser.handler',
|
|
430
|
+
dependencies: [{ target: 'Uploads', kind: 'objects' }],
|
|
431
|
+
authorizer: 'none',
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
id: 'ApiGET/health',
|
|
435
|
+
method: 'GET',
|
|
436
|
+
path: '/health',
|
|
437
|
+
handler: 'apps/api/.gkm/aws/routes/health.handler',
|
|
438
|
+
dependencies: [],
|
|
439
|
+
},
|
|
440
|
+
],
|
|
441
|
+
},
|
|
442
|
+
} as never,
|
|
443
|
+
{ Uploads: uploads, Api: api } as unknown as ProvisionedManifest,
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
const [getUser, health] = routesOf(api);
|
|
447
|
+
expect(getUser?.key).toBe('GET /users/{id}');
|
|
448
|
+
expect(getUser?.handler.handler).toBe(
|
|
449
|
+
'apps/api/.gkm/aws/routes/getUser.handler',
|
|
450
|
+
);
|
|
451
|
+
expect(getUser?.handler.link).toEqual([uploads]);
|
|
452
|
+
expect(getUser?.args).toEqual({});
|
|
453
|
+
// Least privilege: an endpoint is linked to its own edges, not the app's.
|
|
454
|
+
expect(health?.key).toBe('GET /health');
|
|
455
|
+
expect(health?.handler.link).toEqual([]);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it('leaves IAM to the gateway, and every other authorizer to the handler', () => {
|
|
459
|
+
const api = new RestApiSurface({} as never, 'Api');
|
|
460
|
+
|
|
461
|
+
api.mount({
|
|
462
|
+
method: 'POST',
|
|
463
|
+
path: '/x',
|
|
464
|
+
handler: 'x.handler',
|
|
465
|
+
authorizer: 'iam',
|
|
466
|
+
});
|
|
467
|
+
api.mount({
|
|
468
|
+
method: 'POST',
|
|
469
|
+
path: '/y',
|
|
470
|
+
handler: 'y.handler',
|
|
471
|
+
authorizer: 'session',
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
const [x, y] = routesOf(api);
|
|
475
|
+
expect(x?.args).toEqual({ auth: { iam: true } });
|
|
476
|
+
expect(y?.args).toEqual({});
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it('runs a handler that reaches a database inside the database’s network', () => {
|
|
480
|
+
const vpc = { id: 'stub-vpc' } as never;
|
|
481
|
+
const cluster = new Database({} as never, 'Orders', { vpc });
|
|
482
|
+
const tenant = new DatabaseSchema('Billing', cluster, 'billing');
|
|
483
|
+
|
|
484
|
+
expect(vpcFor([cluster as never])).toBe(vpc);
|
|
485
|
+
// A schema tenant lives in its parent's cluster, so in its network too.
|
|
486
|
+
expect(vpcFor([{ _id: 'Uploads' } as never, tenant as never])).toBe(vpc);
|
|
487
|
+
expect(vpcFor([{ _id: 'Uploads' } as never])).toBeUndefined();
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
describe('functions and crons', () => {
|
|
492
|
+
const stack = new App({
|
|
493
|
+
name: 'shop',
|
|
494
|
+
stage: 'prod',
|
|
495
|
+
domain: 'example.com',
|
|
496
|
+
hostedZoneId: 'Z',
|
|
497
|
+
region: 'eu-west-1',
|
|
498
|
+
}).stack('jobs');
|
|
499
|
+
const argsOf = (component: unknown) =>
|
|
500
|
+
(component as { args: Record<string, any> }).args;
|
|
501
|
+
|
|
502
|
+
it('are deployed from the declarations, each linked to its own edges', () => {
|
|
503
|
+
// The `manifest.crons` / `manifest.functions` tables they were built from
|
|
504
|
+
// are gone: the declarations carry the handler, schedule and edges.
|
|
505
|
+
const vpc = { id: 'stub-vpc' } as never;
|
|
506
|
+
const orders = new Database({} as never, 'Orders', { vpc });
|
|
507
|
+
const uploads = { _id: 'Uploads', _type: 'sst.aws.Bucket' };
|
|
508
|
+
const provisioned = {
|
|
509
|
+
Orders: orders,
|
|
510
|
+
Uploads: uploads,
|
|
511
|
+
} as unknown as ProvisionedManifest;
|
|
512
|
+
|
|
513
|
+
const [digest] = provisionCompute(
|
|
514
|
+
stack,
|
|
515
|
+
{
|
|
516
|
+
Orders: { kind: 'database', id: 'Orders' },
|
|
517
|
+
Uploads: { kind: 'objects', id: 'Uploads', provides: ['UPLOADS_URL'] },
|
|
518
|
+
Invoice: {
|
|
519
|
+
kind: 'function',
|
|
520
|
+
id: 'Invoice',
|
|
521
|
+
handler: 'apps/api/.gkm/aws/functions/invoice.handler',
|
|
522
|
+
dependencies: [{ target: 'Uploads', kind: 'objects' }],
|
|
523
|
+
provides: ['INVOICE_URL'],
|
|
524
|
+
},
|
|
525
|
+
Digest: {
|
|
526
|
+
kind: 'cron',
|
|
527
|
+
id: 'Digest',
|
|
528
|
+
handler: 'apps/api/.gkm/aws/crons/digest.handler',
|
|
529
|
+
schedule: 'rate(1 day)',
|
|
530
|
+
dependencies: [{ target: 'Orders', kind: 'database' }],
|
|
531
|
+
},
|
|
532
|
+
} as never,
|
|
533
|
+
provisioned,
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
const invoice = provisioned.Invoice as unknown as Function;
|
|
537
|
+
expect(invoice).toBeInstanceOf(Function);
|
|
538
|
+
expect(argsOf(invoice)).toMatchObject({
|
|
539
|
+
name: stack.logicalPrefixedName('Invoice'),
|
|
540
|
+
handler: 'apps/api/.gkm/aws/functions/invoice.handler',
|
|
541
|
+
link: [uploads],
|
|
542
|
+
url: { authorization: 'iam' },
|
|
543
|
+
});
|
|
544
|
+
expect(argsOf(invoice).vpc).toBeUndefined();
|
|
545
|
+
|
|
546
|
+
// A cron is a schedule, not something to link to; its Lambda reaches
|
|
547
|
+
// the database, so it runs in the database's network.
|
|
548
|
+
expect(provisioned.Digest).toBeUndefined();
|
|
549
|
+
expect(digest).toBeInstanceOf(Cron);
|
|
550
|
+
expect(argsOf(digest).schedule).toBe('rate(1 day)');
|
|
551
|
+
expect(argsOf(digest!.processor)).toMatchObject({
|
|
552
|
+
handler: 'apps/api/.gkm/aws/crons/digest.handler',
|
|
553
|
+
link: [orders],
|
|
554
|
+
vpc,
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
|
|
394
559
|
describe('kinds provisioned elsewhere', () => {
|
|
395
560
|
it('skips a worker, which is a process rather than a resource', () => {
|
|
396
561
|
// A worker in the manifest used to throw UnknownDeclarationKind at synth,
|
|
@@ -79,46 +79,6 @@ describe('Function', () => {
|
|
|
79
79
|
invalidVars: ['SMTP_URL'],
|
|
80
80
|
});
|
|
81
81
|
});
|
|
82
|
-
|
|
83
|
-
it('builds one function per manifest entry, named for the stack', () => {
|
|
84
|
-
const functions = Function.fromManifest(
|
|
85
|
-
stack,
|
|
86
|
-
{
|
|
87
|
-
billing: [
|
|
88
|
-
{
|
|
89
|
-
name: 'Invoice',
|
|
90
|
-
handler: '.gkm/aws-lambda/functions/invoice.handler',
|
|
91
|
-
environment: ['DB_URL'],
|
|
92
|
-
timeout: 60,
|
|
93
|
-
memorySize: 512,
|
|
94
|
-
},
|
|
95
|
-
],
|
|
96
|
-
ops: [
|
|
97
|
-
{
|
|
98
|
-
name: 'Ping',
|
|
99
|
-
handler: '.gkm/aws-lambda/functions/ping.handler',
|
|
100
|
-
environment: [],
|
|
101
|
-
},
|
|
102
|
-
],
|
|
103
|
-
} as never,
|
|
104
|
-
{ links: [db] },
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
expect(functions.map((f) => argsOf(f))).toEqual([
|
|
108
|
-
expect.objectContaining({
|
|
109
|
-
name: stack.logicalPrefixedName('Invoice'),
|
|
110
|
-
timeout: '60 seconds',
|
|
111
|
-
memory: '512 MB',
|
|
112
|
-
link: [db],
|
|
113
|
-
}),
|
|
114
|
-
expect.objectContaining({
|
|
115
|
-
name: stack.logicalPrefixedName('Ping'),
|
|
116
|
-
timeout: undefined,
|
|
117
|
-
memory: undefined,
|
|
118
|
-
link: [],
|
|
119
|
-
}),
|
|
120
|
-
]);
|
|
121
|
-
});
|
|
122
82
|
});
|
|
123
83
|
|
|
124
84
|
describe('Cron', () => {
|
|
@@ -136,37 +96,6 @@ describe('Cron', () => {
|
|
|
136
96
|
function: processor.arn,
|
|
137
97
|
});
|
|
138
98
|
});
|
|
139
|
-
|
|
140
|
-
it('builds a validated function and a schedule per manifest cron', () => {
|
|
141
|
-
const crons = Cron.fromManifest(
|
|
142
|
-
stack,
|
|
143
|
-
[
|
|
144
|
-
{
|
|
145
|
-
name: 'Digest',
|
|
146
|
-
handler: '.gkm/aws-lambda/crons/digest.handler',
|
|
147
|
-
schedule: 'cron(0 8 * * ? *)',
|
|
148
|
-
environment: ['DB_URL'],
|
|
149
|
-
timeout: 120,
|
|
150
|
-
memorySize: 1024,
|
|
151
|
-
},
|
|
152
|
-
{
|
|
153
|
-
name: 'Sweep',
|
|
154
|
-
handler: '.gkm/aws-lambda/crons/sweep.handler',
|
|
155
|
-
schedule: 'rate(1 hour)',
|
|
156
|
-
environment: [],
|
|
157
|
-
},
|
|
158
|
-
] as never,
|
|
159
|
-
{ links: [db], enabled: true },
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
const [digest, sweep] = crons.map((c) => argsOf(c));
|
|
163
|
-
expect(digest).toMatchObject({
|
|
164
|
-
schedule: 'cron(0 8 * * ? *)',
|
|
165
|
-
enabled: true,
|
|
166
|
-
function: expect.stringContaining('DigestFunction'),
|
|
167
|
-
});
|
|
168
|
-
expect(sweep).toMatchObject({ schedule: 'rate(1 hour)' });
|
|
169
|
-
});
|
|
170
99
|
});
|
|
171
100
|
|
|
172
101
|
describe('App and Stack', () => {
|
package/src/sst/aws/Api.ts
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { EnvValidationError } from '@geekmidas/envkit/sst';
|
|
3
|
-
import {
|
|
4
|
-
flattenManifestField,
|
|
5
|
-
type ManifestField,
|
|
6
|
-
type RouteInfo,
|
|
7
|
-
} from '@geekmidas/manifest';
|
|
8
3
|
import { type GkmLinkable, ResourceType } from '../Linkable';
|
|
9
4
|
import { LinkedEnvironment } from '../LinkedEnvironment';
|
|
10
5
|
import type { StackType } from '../Stack';
|
|
@@ -157,44 +152,6 @@ export class Api<
|
|
|
157
152
|
}
|
|
158
153
|
}
|
|
159
154
|
|
|
160
|
-
/**
|
|
161
|
-
* Build an `Api` from a `gkm build` manifest's `routes` field (flat or
|
|
162
|
-
* partitioned): each `RouteInfo` becomes a route (env vars, authorizer,
|
|
163
|
-
* timeout/memory mapped). Supply `authorizers` (JWT/Lambda settings),
|
|
164
|
-
* `links`, and any native args via `props`.
|
|
165
|
-
*
|
|
166
|
-
* ```ts
|
|
167
|
-
* import { manifest } from './.gkm/manifest/aws';
|
|
168
|
-
* Api.fromManifest(stack, 'Api', manifest.routes, { links: [db] });
|
|
169
|
-
* ```
|
|
170
|
-
*/
|
|
171
|
-
static fromManifest<
|
|
172
|
-
TAuthorizers extends Record<string, unknown> = {},
|
|
173
|
-
TStage extends string = string,
|
|
174
|
-
TDomain extends string = string,
|
|
175
|
-
>(
|
|
176
|
-
stack: StackType<TStage, TDomain>,
|
|
177
|
-
id: string,
|
|
178
|
-
routes: ManifestField<RouteInfo>,
|
|
179
|
-
props: Omit<ApiProps<TAuthorizers>, 'routes'> = {},
|
|
180
|
-
): Api<TAuthorizers, TStage, TDomain> {
|
|
181
|
-
const routeTable = flattenManifestField(routes).map(
|
|
182
|
-
(route): Route<AuthorizerName<TAuthorizers>> => ({
|
|
183
|
-
method: route.method as Route['method'],
|
|
184
|
-
path: route.path,
|
|
185
|
-
handler: route.handler,
|
|
186
|
-
environment: route.environment,
|
|
187
|
-
authorizer: route.authorizer as AuthorizerName<TAuthorizers>,
|
|
188
|
-
timeout: route.timeout ? `${route.timeout} seconds` : undefined,
|
|
189
|
-
memory: route.memorySize ? `${route.memorySize} MB` : undefined,
|
|
190
|
-
}),
|
|
191
|
-
);
|
|
192
|
-
return new Api(stack, id, {
|
|
193
|
-
...props,
|
|
194
|
-
routes: routeTable,
|
|
195
|
-
} as ApiProps<TAuthorizers>);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
155
|
/** Resolves a route's `authorizer` name to the SST `auth` option. */
|
|
199
156
|
private static buildRouteAuth(
|
|
200
157
|
route: Route<string>,
|
package/src/sst/aws/Cron.ts
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type CronInfo,
|
|
3
|
-
flattenManifestField,
|
|
4
|
-
type ManifestField,
|
|
5
|
-
} from '@geekmidas/manifest';
|
|
6
|
-
import type { GkmLinkable } from '../Linkable';
|
|
7
1
|
import type { StackType } from '../Stack';
|
|
8
|
-
import { Function } from './Function';
|
|
2
|
+
import type { Function } from './Function';
|
|
9
3
|
|
|
10
4
|
export type CronExpressionValue = number | '*' | '?' | `${number}/${number}`;
|
|
11
5
|
export type CronExpressionDay =
|
|
@@ -52,6 +46,9 @@ export class Cron<
|
|
|
52
46
|
TStage extends string = string,
|
|
53
47
|
TDomain extends string = string,
|
|
54
48
|
> extends sst.aws.CronV2 {
|
|
49
|
+
/** The function it invokes. */
|
|
50
|
+
readonly processor: CronProps['processor'];
|
|
51
|
+
|
|
55
52
|
constructor(
|
|
56
53
|
_stack: StackType<TStage, TDomain>,
|
|
57
54
|
name: string,
|
|
@@ -63,44 +60,7 @@ export class Cron<
|
|
|
63
60
|
schedule,
|
|
64
61
|
function: processor.arn,
|
|
65
62
|
});
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Build one `Cron` per entry in a `gkm build` manifest's `crons` field (flat
|
|
70
|
-
* or partitioned). Each cron's handler becomes a validated `Function` (the
|
|
71
|
-
* cron's target), so pass `links` for that function's env validation;
|
|
72
|
-
* remaining `props` are CronV2 args.
|
|
73
|
-
*
|
|
74
|
-
* ```ts
|
|
75
|
-
* Cron.fromManifest(stack, manifest.crons, { links: [db] });
|
|
76
|
-
* ```
|
|
77
|
-
*/
|
|
78
|
-
static fromManifest<
|
|
79
|
-
TStage extends string = string,
|
|
80
|
-
TDomain extends string = string,
|
|
81
|
-
>(
|
|
82
|
-
stack: StackType<TStage, TDomain>,
|
|
83
|
-
crons: ManifestField<CronInfo>,
|
|
84
|
-
props: Omit<CronProps, 'processor' | 'schedule'> & {
|
|
85
|
-
links?: GkmLinkable[];
|
|
86
|
-
} = {},
|
|
87
|
-
): Cron<TStage, TDomain>[] {
|
|
88
|
-
const { links, ...cronArgs } = props;
|
|
89
|
-
return flattenManifestField(crons).map((cron) => {
|
|
90
|
-
const processor = new Function(stack, `${cron.name}Function`, {
|
|
91
|
-
name: stack.logicalPrefixedName(cron.name),
|
|
92
|
-
handler: cron.handler,
|
|
93
|
-
envVars: cron.environment,
|
|
94
|
-
links,
|
|
95
|
-
timeout: cron.timeout ? `${cron.timeout} seconds` : undefined,
|
|
96
|
-
memory: cron.memorySize ? `${cron.memorySize} MB` : undefined,
|
|
97
|
-
});
|
|
98
|
-
return new Cron(stack, cron.name, {
|
|
99
|
-
...cronArgs,
|
|
100
|
-
processor,
|
|
101
|
-
schedule: cron.schedule as CronSchedule,
|
|
102
|
-
});
|
|
103
|
-
});
|
|
63
|
+
this.processor = processor;
|
|
104
64
|
}
|
|
105
65
|
}
|
|
106
66
|
|
package/src/sst/aws/Function.ts
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
1
|
import type { EnvValidator, ValidationResult } from '@geekmidas/envkit/sst';
|
|
2
|
-
import {
|
|
3
|
-
type FunctionInfo,
|
|
4
|
-
flattenManifestField,
|
|
5
|
-
type ManifestField,
|
|
6
|
-
} from '@geekmidas/manifest';
|
|
7
2
|
import { type GkmLinkable, ResourceType } from '../Linkable';
|
|
8
3
|
import { LinkedEnvironment } from '../LinkedEnvironment';
|
|
9
4
|
import type { StackType } from '../Stack';
|
|
@@ -66,9 +61,10 @@ export class Function<
|
|
|
66
61
|
super(id, {
|
|
67
62
|
...fnArgs,
|
|
68
63
|
environment: mergedEnvironment,
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
link:
|
|
64
|
+
// Resolved from `links` when `envVars` names what it needs; otherwise
|
|
65
|
+
// the `link` it was given — the edges a manifest declared.
|
|
66
|
+
link:
|
|
67
|
+
envVars.length > 0 ? linked.resolveLink(envVars) : (fnArgs.link ?? []),
|
|
72
68
|
runtime: runtime ?? 'nodejs24.x',
|
|
73
69
|
logging: logging ?? { format: 'json' },
|
|
74
70
|
});
|
|
@@ -78,40 +74,15 @@ export class Function<
|
|
|
78
74
|
this.envVars = envVars;
|
|
79
75
|
}
|
|
80
76
|
|
|
77
|
+
/** Its address, for whatever declared an edge to it. */
|
|
78
|
+
provides(): Record<string, $util.Input<string>> {
|
|
79
|
+
return { url: this.url };
|
|
80
|
+
}
|
|
81
|
+
|
|
81
82
|
/** Re-runs validation and returns the result (does not throw). */
|
|
82
83
|
validate(): ValidationResult {
|
|
83
84
|
return this.validator.validate(this.envVars);
|
|
84
85
|
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Build one `Function` per entry in a `gkm build` manifest's `functions`
|
|
88
|
-
* field (flat or partitioned). Shared `props` (e.g. `links`) apply to every
|
|
89
|
-
* function.
|
|
90
|
-
*
|
|
91
|
-
* ```ts
|
|
92
|
-
* Function.fromManifest(stack, manifest.functions, { links: [db] });
|
|
93
|
-
* ```
|
|
94
|
-
*/
|
|
95
|
-
static fromManifest<
|
|
96
|
-
TStage extends string = string,
|
|
97
|
-
TDomain extends string = string,
|
|
98
|
-
>(
|
|
99
|
-
stack: StackType<TStage, TDomain>,
|
|
100
|
-
functions: ManifestField<FunctionInfo>,
|
|
101
|
-
props: Omit<FunctionProps, 'handler'> = {},
|
|
102
|
-
): Function<TStage, TDomain>[] {
|
|
103
|
-
return flattenManifestField(functions).map(
|
|
104
|
-
(fn) =>
|
|
105
|
-
new Function(stack, fn.name, {
|
|
106
|
-
...props,
|
|
107
|
-
name: stack.logicalPrefixedName(fn.name),
|
|
108
|
-
handler: fn.handler,
|
|
109
|
-
envVars: fn.environment,
|
|
110
|
-
timeout: fn.timeout ? `${fn.timeout} seconds` : undefined,
|
|
111
|
-
memory: fn.memorySize ? `${fn.memorySize} MB` : undefined,
|
|
112
|
-
}),
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
86
|
}
|
|
116
87
|
|
|
117
88
|
export interface FunctionProps extends sst.aws.FunctionArgs {
|
package/src/sst/aws/Queue.ts
CHANGED
|
@@ -87,6 +87,7 @@ export class Queue<
|
|
|
87
87
|
handler: consumer.handler,
|
|
88
88
|
link: [this, ...(consumer.link ?? [])],
|
|
89
89
|
runtime: 'nodejs24.x',
|
|
90
|
+
...(consumer.vpc ? { vpc: consumer.vpc } : {}),
|
|
90
91
|
...(consumer.timeout ? { timeout: consumer.timeout } : {}),
|
|
91
92
|
},
|
|
92
93
|
consumer.batchSize ? { batch: { size: consumer.batchSize } } : {},
|
|
@@ -104,10 +105,12 @@ export class Queue<
|
|
|
104
105
|
|
|
105
106
|
/** The queue's one consumer, as {@link Queue.consume} subscribes it. */
|
|
106
107
|
export interface QueueConsumer {
|
|
107
|
-
/** The built handler —
|
|
108
|
+
/** The built handler — `apps/api/.gkm/aws/queues/emails.handler`. */
|
|
108
109
|
handler: string;
|
|
109
110
|
/** What the consumer depends on; the queue itself is always linked. */
|
|
110
111
|
link?: unknown[];
|
|
112
|
+
/** The database's network, when the consumer reaches one. */
|
|
113
|
+
vpc?: sst.aws.Vpc;
|
|
111
114
|
timeout?: `${number} seconds` | `${number} minutes`;
|
|
112
115
|
batchSize?: number;
|
|
113
116
|
}
|
|
@@ -4,20 +4,10 @@ import type { StackType } from '../Stack';
|
|
|
4
4
|
/**
|
|
5
5
|
* `RestApiSurface` — an HTTP API, and the infra half of the `rest-api` kind.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* So what this provisions is the surface and nothing on it. An API Gateway with
|
|
13
|
-
* no routes 404s everything, which is the honest state of things — and it is
|
|
14
|
-
* still worth provisioning, because the *address* is what everything downstream
|
|
15
|
-
* needs: a site inlines it as `VITE_API_URL`, an auth server puts it on its
|
|
16
|
-
* trusted-origin list, and the cookie domain derives from it. Those are blocked
|
|
17
|
-
* on the API existing, not on it answering.
|
|
18
|
-
*
|
|
19
|
-
* The two collapse into one component when the endpoint merge lands and the
|
|
20
|
-
* surface node carries its routes.
|
|
7
|
+
* Provisioned before its routes, because its *address* is what everything
|
|
8
|
+
* downstream needs: a site inlines it as `VITE_API_URL`, an auth server puts it
|
|
9
|
+
* on its trusted-origin list, and the cookie domain derives from it. The routes
|
|
10
|
+
* are mounted once everything they link to exists — see {@link mount}.
|
|
21
11
|
*/
|
|
22
12
|
|
|
23
13
|
export interface RestApiSurfaceProps extends sst.aws.ApiGatewayV2Args {
|
|
@@ -82,6 +72,25 @@ export class RestApiSurface<
|
|
|
82
72
|
};
|
|
83
73
|
}
|
|
84
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Mount one endpoint: a Lambda running the handler the build wrote for it,
|
|
77
|
+
* linked to exactly what that endpoint depends on.
|
|
78
|
+
*/
|
|
79
|
+
mount(endpoint: SurfaceRoute) {
|
|
80
|
+
return this.route(
|
|
81
|
+
`${endpoint.method} ${endpoint.path}`,
|
|
82
|
+
{
|
|
83
|
+
handler: endpoint.handler,
|
|
84
|
+
link: endpoint.link ?? [],
|
|
85
|
+
runtime: 'nodejs24.x',
|
|
86
|
+
...(endpoint.vpc ? { vpc: endpoint.vpc } : {}),
|
|
87
|
+
},
|
|
88
|
+
// `iam` is the one authorizer the gateway enforces itself; every other
|
|
89
|
+
// is the endpoint's own, checked in the handler.
|
|
90
|
+
endpoint.authorizer === 'iam' ? { auth: { iam: true } } : {},
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
85
94
|
override getSSTLink() {
|
|
86
95
|
const link = super.getSSTLink();
|
|
87
96
|
return {
|
|
@@ -90,3 +99,16 @@ export class RestApiSurface<
|
|
|
90
99
|
};
|
|
91
100
|
}
|
|
92
101
|
}
|
|
102
|
+
|
|
103
|
+
/** One endpoint, as {@link RestApiSurface.mount} mounts it. */
|
|
104
|
+
export interface SurfaceRoute {
|
|
105
|
+
method: string;
|
|
106
|
+
path: string;
|
|
107
|
+
/** The built handler — `apps/api/.gkm/aws/routes/getUser.handler`. */
|
|
108
|
+
handler: string;
|
|
109
|
+
/** What the endpoint depends on, and nothing else. */
|
|
110
|
+
link?: unknown[];
|
|
111
|
+
/** The database's network, when the endpoint reaches one. */
|
|
112
|
+
vpc?: sst.aws.Vpc;
|
|
113
|
+
authorizer?: string;
|
|
114
|
+
}
|
package/src/sst/fromManifest.ts
CHANGED
|
@@ -41,12 +41,14 @@ import {
|
|
|
41
41
|
withCacheTable,
|
|
42
42
|
} from './aws/Cache';
|
|
43
43
|
import { Credential } from './aws/Credential';
|
|
44
|
+
import { Cron, type CronSchedule } from './aws/Cron';
|
|
44
45
|
import { Database, DatabaseNeedsVpc, type DatabaseProps } from './aws/Database';
|
|
45
46
|
import { DatabaseBootstrap } from './aws/DatabaseBootstrap';
|
|
46
47
|
import { DatabaseReader, DatabaseSchema } from './aws/DerivedDatabase';
|
|
47
48
|
import { Email, EmailNeedsSender } from './aws/Email';
|
|
48
49
|
import { ExternalApi, type ExternalApiProps } from './aws/ExternalApi';
|
|
49
50
|
import { FileServer } from './aws/FileServer';
|
|
51
|
+
import { Function } from './aws/Function';
|
|
50
52
|
import { ObjectStorage } from './aws/ObjectStorage';
|
|
51
53
|
import { Queue } from './aws/Queue';
|
|
52
54
|
import { RestApiSurface } from './aws/RestApiSurface';
|
|
@@ -378,17 +380,12 @@ const PROVISIONERS: Partial<Record<DeclarationKind, Provisioner>> = {
|
|
|
378
380
|
},
|
|
379
381
|
|
|
380
382
|
/**
|
|
381
|
-
* The surface, without its routes.
|
|
383
|
+
* The surface, without its routes yet.
|
|
382
384
|
*
|
|
383
|
-
*
|
|
384
|
-
*
|
|
385
|
-
* it is
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
* Provisioning it anyway is not ceremony. Its address is what a site inlines
|
|
389
|
-
* as `VITE_API_URL`, what an auth server puts on its trusted-origin list, and
|
|
390
|
-
* what the cookie domain derives from — so everything downstream of the API
|
|
391
|
-
* is blocked on the API *existing*, not on it answering.
|
|
385
|
+
* Its address is what a site inlines as `VITE_API_URL`, what an auth server
|
|
386
|
+
* puts on its trusted-origin list, and what the cookie domain derives from —
|
|
387
|
+
* so it is provisioned in order, and its endpoints are mounted by
|
|
388
|
+
* {@link mountRoutes} once everything they link to exists.
|
|
392
389
|
*/
|
|
393
390
|
'rest-api': (stack, d, props, context) =>
|
|
394
391
|
new RestApiSurface(stack, d.id, {
|
|
@@ -593,9 +590,7 @@ function rootCluster(component: Provisioned): Database {
|
|
|
593
590
|
* printed: one line per route, naming what it can reach.
|
|
594
591
|
*
|
|
595
592
|
* A surface with no routes says so rather than printing nothing, because "no
|
|
596
|
-
* routes
|
|
597
|
-
* application's own API that is currently the true state, pending the endpoint
|
|
598
|
-
* merge.
|
|
593
|
+
* routes" and "this printed nothing" look identical otherwise.
|
|
599
594
|
*
|
|
600
595
|
* Pure, so what gets printed can be asserted without a deploy.
|
|
601
596
|
*/
|
|
@@ -751,21 +746,132 @@ export function isServed(id: string, manifest: ConstructManifest): boolean {
|
|
|
751
746
|
}
|
|
752
747
|
|
|
753
748
|
/**
|
|
754
|
-
* Kinds
|
|
749
|
+
* Kinds the in-order pass skips.
|
|
755
750
|
*
|
|
756
751
|
* - `mobile-app` ships through EAS and the stores. It is in the manifest for
|
|
757
752
|
* what it makes the surfaces trust, which `callersOf` reads off the graph.
|
|
758
753
|
* - `worker` is a process, not a resource: what it runs is provisioned as
|
|
759
754
|
* what it is — a queue's consumer by {@link subscribeConsumers}.
|
|
760
|
-
* - `cron` and `function` are Lambdas
|
|
761
|
-
*
|
|
755
|
+
* - `cron` and `function` are Lambdas linked to what they depend on, so they
|
|
756
|
+
* are provisioned once all of it exists, by {@link provisionCompute}.
|
|
762
757
|
*/
|
|
763
758
|
export const PROVISIONED_ELSEWHERE: ReadonlySet<DeclarationKind> =
|
|
764
759
|
new Set<DeclarationKind>(['mobile-app', 'worker', 'cron', 'function']);
|
|
765
760
|
|
|
761
|
+
/**
|
|
762
|
+
* Every function and cron: a Lambda running the handler the build wrote,
|
|
763
|
+
* linked to exactly what it depends on, in the database's network when it
|
|
764
|
+
* reaches one. A cron is that Lambda on its schedule; a function is reached
|
|
765
|
+
* at its URL, which only callers granted IAM access can invoke.
|
|
766
|
+
*
|
|
767
|
+
* After everything else is provisioned, because each links to things
|
|
768
|
+
* `provisionOrder` does not order before it.
|
|
769
|
+
*/
|
|
770
|
+
export function provisionCompute(
|
|
771
|
+
stack: StackType,
|
|
772
|
+
manifest: ConstructManifest,
|
|
773
|
+
provisioned: ProvisionedManifest,
|
|
774
|
+
): Cron[] {
|
|
775
|
+
const crons: Cron[] = [];
|
|
776
|
+
|
|
777
|
+
for (const [id, declaration] of Object.entries(manifest)) {
|
|
778
|
+
if (declaration?.kind !== 'function' && declaration?.kind !== 'cron') {
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const { link } = resolveEdges(declaration.dependencies, provisioned);
|
|
783
|
+
const vpc = vpcFor(link);
|
|
784
|
+
const lambda = new Function(
|
|
785
|
+
stack,
|
|
786
|
+
declaration.kind === 'cron' ? `${id}Function` : id,
|
|
787
|
+
{
|
|
788
|
+
name: stack.logicalPrefixedName(id),
|
|
789
|
+
handler: declaration.handler,
|
|
790
|
+
link,
|
|
791
|
+
...(vpc ? { vpc } : {}),
|
|
792
|
+
...(declaration.kind === 'function'
|
|
793
|
+
? { url: { authorization: 'iam' } }
|
|
794
|
+
: {}),
|
|
795
|
+
},
|
|
796
|
+
);
|
|
797
|
+
|
|
798
|
+
if (declaration.kind === 'cron') {
|
|
799
|
+
crons.push(
|
|
800
|
+
new Cron(stack, id, {
|
|
801
|
+
processor: lambda,
|
|
802
|
+
schedule: declaration.schedule as CronSchedule,
|
|
803
|
+
}),
|
|
804
|
+
);
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
assertProvides(
|
|
809
|
+
id,
|
|
810
|
+
declaration.provides,
|
|
811
|
+
Object.keys(lambda.provides()).map((role) =>
|
|
812
|
+
providedKeyFor(id, declaration.kind, role),
|
|
813
|
+
),
|
|
814
|
+
);
|
|
815
|
+
provisioned[id] = lambda;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
return crons;
|
|
819
|
+
}
|
|
820
|
+
|
|
766
821
|
/** A provisioned queue: what {@link subscribeConsumers} subscribes through. */
|
|
767
822
|
interface Consumable {
|
|
768
|
-
consume(consumer: {
|
|
823
|
+
consume(consumer: {
|
|
824
|
+
handler: string;
|
|
825
|
+
link?: unknown[];
|
|
826
|
+
vpc?: sst.aws.Vpc;
|
|
827
|
+
}): unknown;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* The network a function has to run in to reach what it links to.
|
|
832
|
+
*
|
|
833
|
+
* A database is private to its VPC, so a handler linked to one — or to a
|
|
834
|
+
* schema tenant or reader of one — runs inside it, or its connection times out.
|
|
835
|
+
*/
|
|
836
|
+
export function vpcFor(link: readonly Provisioned[]): sst.aws.Vpc | undefined {
|
|
837
|
+
for (const component of link) {
|
|
838
|
+
const cluster = rootCluster(component);
|
|
839
|
+
if (cluster instanceof Database) return cluster.vpc;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
return undefined;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Mount every surface's endpoints — the handlers the build wrote, each linked
|
|
847
|
+
* to exactly what that endpoint depends on.
|
|
848
|
+
*
|
|
849
|
+
* After every construct is provisioned, for the reason consumers are: an
|
|
850
|
+
* endpoint links to things `provisionOrder` does not order before its surface.
|
|
851
|
+
*/
|
|
852
|
+
export function mountRoutes(
|
|
853
|
+
manifest: ConstructManifest,
|
|
854
|
+
provisioned: ProvisionedManifest,
|
|
855
|
+
): void {
|
|
856
|
+
for (const [id, declaration] of Object.entries(manifest)) {
|
|
857
|
+
if (declaration?.kind !== 'rest-api') continue;
|
|
858
|
+
|
|
859
|
+
const surface = provisioned[id];
|
|
860
|
+
if (!(surface instanceof RestApiSurface)) continue;
|
|
861
|
+
|
|
862
|
+
for (const endpoint of declaration.endpoints) {
|
|
863
|
+
const { link } = resolveEdges(endpoint.dependencies, provisioned);
|
|
864
|
+
const vpc = vpcFor(link);
|
|
865
|
+
surface.mount({
|
|
866
|
+
method: endpoint.method,
|
|
867
|
+
path: endpoint.path,
|
|
868
|
+
handler: endpoint.handler,
|
|
869
|
+
link,
|
|
870
|
+
...(vpc ? { vpc } : {}),
|
|
871
|
+
...(endpoint.authorizer ? { authorizer: endpoint.authorizer } : {}),
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
}
|
|
769
875
|
}
|
|
770
876
|
|
|
771
877
|
/**
|
|
@@ -786,7 +892,12 @@ export function subscribeConsumers(
|
|
|
786
892
|
if (!queue) continue;
|
|
787
893
|
|
|
788
894
|
const { link } = resolveEdges(declaration.worker.dependencies, provisioned);
|
|
789
|
-
|
|
895
|
+
const vpc = vpcFor(link);
|
|
896
|
+
queue.consume({
|
|
897
|
+
handler: declaration.worker.handler,
|
|
898
|
+
link,
|
|
899
|
+
...(vpc ? { vpc } : {}),
|
|
900
|
+
});
|
|
790
901
|
}
|
|
791
902
|
}
|
|
792
903
|
|
|
@@ -902,8 +1013,11 @@ export function fromManifest(
|
|
|
902
1013
|
provisioned[id] = component;
|
|
903
1014
|
}
|
|
904
1015
|
|
|
905
|
-
//
|
|
1016
|
+
// The functions and crons, each queue's one consumer and each surface's
|
|
1017
|
+
// endpoints, once everything they link to exists.
|
|
1018
|
+
provisionCompute(stack, manifest, provisioned);
|
|
906
1019
|
subscribeConsumers(manifest, provisioned);
|
|
1020
|
+
mountRoutes(manifest, provisioned);
|
|
907
1021
|
|
|
908
1022
|
for (const line of describeRoutes(manifest)) console.log(line);
|
|
909
1023
|
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
// Type-level checks that the `fromManifest` integrators accept the unified
|
|
2
|
-
// `gkm build` manifest shape (`export const manifest = { … } as const`) — both
|
|
3
|
-
// the flat and the partitioned `ManifestField` forms. Checked by `ts:check:sst`.
|
|
4
|
-
|
|
5
|
-
import type { Manifest, ManifestField, RouteInfo } from '@geekmidas/manifest';
|
|
6
|
-
import { App } from '../App';
|
|
7
|
-
import { Api } from '../aws/Api';
|
|
8
|
-
import { Cron } from '../aws/Cron';
|
|
9
|
-
import { Function } from '../aws/Function';
|
|
10
|
-
import { type GkmLinkable, ResourceType } from '../Linkable';
|
|
11
|
-
|
|
12
|
-
const stack = new App({
|
|
13
|
-
name: 'a',
|
|
14
|
-
stage: 'dev',
|
|
15
|
-
domain: 'example.com',
|
|
16
|
-
hostedZoneId: 'Z',
|
|
17
|
-
region: 'us-east-1',
|
|
18
|
-
}).stack('api');
|
|
19
|
-
|
|
20
|
-
const db: GkmLinkable = { _id: 'db', _type: ResourceType.Postgres };
|
|
21
|
-
|
|
22
|
-
// Mirrors a generated `manifest/aws.ts` — one object, `as const`.
|
|
23
|
-
const manifest = {
|
|
24
|
-
routes: [
|
|
25
|
-
{
|
|
26
|
-
path: '/users/{id}',
|
|
27
|
-
method: 'GET',
|
|
28
|
-
handler: 'users.handler',
|
|
29
|
-
authorizer: 'none',
|
|
30
|
-
environment: ['DB_HOST'],
|
|
31
|
-
timeout: 30,
|
|
32
|
-
memorySize: 1024,
|
|
33
|
-
},
|
|
34
|
-
],
|
|
35
|
-
functions: [
|
|
36
|
-
{ name: 'worker', handler: 'worker.handler', environment: ['DB_URL'] },
|
|
37
|
-
],
|
|
38
|
-
crons: [
|
|
39
|
-
{ name: 'nightly', handler: 'nightly.handler', schedule: 'rate(1 day)' },
|
|
40
|
-
],
|
|
41
|
-
} as const satisfies Manifest;
|
|
42
|
-
|
|
43
|
-
// Each integrator takes the manifest *field*.
|
|
44
|
-
export const api = Api.fromManifest(stack, 'Api', manifest.routes, {
|
|
45
|
-
links: [db],
|
|
46
|
-
authorizers: { jwt: { issuer: 'https://i', audiences: ['a'] } },
|
|
47
|
-
});
|
|
48
|
-
export const workers = Function.fromManifest(stack, manifest.functions, {
|
|
49
|
-
links: [db],
|
|
50
|
-
});
|
|
51
|
-
export const crons = Cron.fromManifest(stack, manifest.crons, { links: [db] });
|
|
52
|
-
|
|
53
|
-
// The partitioned `ManifestField` form is also accepted.
|
|
54
|
-
const partitionedRoutes = {
|
|
55
|
-
admin: [
|
|
56
|
-
{
|
|
57
|
-
path: '/admin',
|
|
58
|
-
method: 'GET',
|
|
59
|
-
handler: 'admin.handler',
|
|
60
|
-
authorizer: 'iam',
|
|
61
|
-
},
|
|
62
|
-
],
|
|
63
|
-
default: [
|
|
64
|
-
{
|
|
65
|
-
path: '/health',
|
|
66
|
-
method: 'GET',
|
|
67
|
-
handler: 'health.handler',
|
|
68
|
-
authorizer: 'none',
|
|
69
|
-
},
|
|
70
|
-
],
|
|
71
|
-
} satisfies ManifestField<RouteInfo>;
|
|
72
|
-
export const partitionedApi = Api.fromManifest(
|
|
73
|
-
stack,
|
|
74
|
-
'PartApi',
|
|
75
|
-
partitionedRoutes,
|
|
76
|
-
{ links: [db] },
|
|
77
|
-
);
|