@geekmidas/cloud 1.1.1 → 10.0.0-alpha.0
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/{index-ByEJy40r.d.cts → index-B5CZ1xVf.d.cts} +17 -9
- package/dist/index-B5CZ1xVf.d.cts.map +1 -0
- package/dist/{index-DZ0QrJQr.d.mts → index-DhHRjduZ.d.mts} +17 -9
- package/dist/index-DhHRjduZ.d.mts.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/utils/index.cjs +1 -1
- package/dist/utils/index.d.cts +1 -1
- package/dist/utils/index.d.mts +1 -1
- package/dist/utils/index.mjs +1 -1
- package/dist/{utils-CtMjuIMR.cjs → utils-B1a2UuEO.cjs} +5 -5
- package/dist/utils-B1a2UuEO.cjs.map +1 -0
- package/dist/{utils-BdKG20_m.mjs → utils-DMOXJ27j.mjs} +5 -5
- package/dist/utils-DMOXJ27j.mjs.map +1 -0
- package/package.json +44 -7
- package/src/dokploy/Application.ts +259 -0
- package/src/dokploy/__tests__/Application.spec.ts +69 -0
- package/src/dokploy/index.ts +18 -0
- package/src/sst/__tests__/LinkedEnvironment.spec.ts +4 -1
- package/src/sst/__tests__/backends.spec.ts +249 -0
- package/src/sst/__tests__/bootstrap.spec.ts +140 -0
- package/src/sst/__tests__/database.spec.ts +119 -0
- package/src/sst/__tests__/fromManifest.spec.ts +266 -0
- package/src/sst/__tests__/provides.spec.ts +107 -0
- package/src/sst/__tests__/ses.spec.ts +93 -0
- package/src/sst/__tests__/surfaces.spec.ts +132 -0
- package/src/sst/__type-tests__/authorizers.type-test.ts +2 -2
- package/src/sst/__type-tests__/manifest.type-test.ts +3 -3
- package/src/sst/__type-tests__/messaging.type-test.ts +3 -3
- package/src/sst/__type-tests__/storage.type-test.ts +2 -2
- package/src/sst/{Api.ts → aws/Api.ts} +3 -3
- package/src/sst/aws/Cache.ts +259 -0
- package/src/sst/aws/Credential.ts +28 -0
- package/src/sst/{Cron.ts → aws/Cron.ts} +2 -2
- package/src/sst/aws/Database.ts +190 -0
- package/src/sst/aws/DatabaseBootstrap.ts +309 -0
- package/src/sst/aws/DerivedDatabase.ts +117 -0
- package/src/sst/aws/Email.ts +181 -0
- package/src/sst/aws/FileServer.ts +82 -0
- package/src/sst/{Function.ts → aws/Function.ts} +3 -3
- package/src/sst/aws/ObjectStorage.ts +76 -0
- package/src/sst/aws/Queue.ts +116 -0
- package/src/sst/aws/RestApiSurface.ts +92 -0
- package/src/sst/aws/Secret.ts +76 -0
- package/src/sst/aws/StaticSite.ts +92 -0
- package/src/sst/{Storage.ts → aws/Storage.ts} +3 -3
- package/src/sst/aws/Topic.ts +68 -0
- package/src/sst/aws/bootstrap/handler.ts +110 -0
- package/src/sst/aws/ses.ts +132 -0
- package/src/sst/errors.ts +65 -0
- package/src/sst/fromManifest.ts +899 -0
- package/src/sst/index.ts +62 -7
- package/src/sst/naming.ts +37 -16
- package/src/sst/tsconfig.json +2 -2
- package/src/sst/upstash.d.ts +38 -0
- package/dist/index-ByEJy40r.d.cts.map +0 -1
- package/dist/index-DZ0QrJQr.d.mts.map +0 -1
- package/dist/utils-BdKG20_m.mjs.map +0 -1
- package/dist/utils-CtMjuIMR.cjs.map +0 -1
- package/src/sst/Queue.ts +0 -46
- package/src/sst/Topic.ts +0 -37
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { s3Url } from '@geekmidas/storage/aws';
|
|
2
|
+
import { type GkmLinkable, ResourceType } from '../Linkable';
|
|
3
|
+
import type { StackType } from '../Stack';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `ObjectStorage` — a linkable S3 bucket (wraps `sst.aws.Bucket`), and the
|
|
7
|
+
* infra half of the `objects` construct.
|
|
8
|
+
*
|
|
9
|
+
* Its whole contribution is the link. SST injects the linked resource's
|
|
10
|
+
* properties at runtime and `@geekmidas/envkit`'s resolvers turn them into the
|
|
11
|
+
* env a construct reads — so composing a URL here would produce a value nothing
|
|
12
|
+
* ever reads.
|
|
13
|
+
*
|
|
14
|
+
* Usable on its own. `Stack.fromManifest` translates a manifest into these, but
|
|
15
|
+
* the component takes ordinary props so it works in a hand-written
|
|
16
|
+
* `sst.config.ts` with no manifest anywhere.
|
|
17
|
+
*/
|
|
18
|
+
export class ObjectStorage<
|
|
19
|
+
TStage extends string = string,
|
|
20
|
+
TDomain extends string = string,
|
|
21
|
+
>
|
|
22
|
+
extends sst.aws.Bucket
|
|
23
|
+
implements GkmLinkable
|
|
24
|
+
{
|
|
25
|
+
readonly _id!: string;
|
|
26
|
+
|
|
27
|
+
get _type() {
|
|
28
|
+
return ResourceType.ObjectStorage;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
constructor(
|
|
32
|
+
_stack: StackType<TStage, TDomain>,
|
|
33
|
+
name: string,
|
|
34
|
+
props: ObjectStorageProps = {},
|
|
35
|
+
) {
|
|
36
|
+
super(name, props);
|
|
37
|
+
this._id = name;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The values this bucket resolves onto anything that depends on it, keyed by
|
|
42
|
+
* role. `provideKey` turns a role into the env key the app declared, so
|
|
43
|
+
* `url` here is `UPLOADS_URL` there.
|
|
44
|
+
*
|
|
45
|
+
* Composed *here* because only the resource knows its own shape: the name may
|
|
46
|
+
* be supplied through props or generated, and the region is the bucket's, not
|
|
47
|
+
* the reader's. It stays a method rather than being inlined below so the
|
|
48
|
+
* contract can be asserted — declared keys against supplied ones — without a
|
|
49
|
+
* deploy.
|
|
50
|
+
*/
|
|
51
|
+
provides(): Record<string, $util.Input<string>> {
|
|
52
|
+
return {
|
|
53
|
+
url: $util
|
|
54
|
+
.all([this.name, this.nodes.bucket.region])
|
|
55
|
+
.apply(([bucket, region]) => s3Url.build({ bucket, region })),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The link carries those values, because the link is what reaches the running
|
|
61
|
+
* code: SST injects these properties and envkit's resolvers flatten them into
|
|
62
|
+
* env. Composing anywhere else produces a value nothing reads.
|
|
63
|
+
*
|
|
64
|
+
* The permissions `super` includes are untouched: what a link grants stays
|
|
65
|
+
* SST's business.
|
|
66
|
+
*/
|
|
67
|
+
override getSSTLink() {
|
|
68
|
+
const link = super.getSSTLink();
|
|
69
|
+
return {
|
|
70
|
+
...link,
|
|
71
|
+
properties: { ...link.properties, ...this.provides() },
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface ObjectStorageProps extends sst.aws.BucketArgs {}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// The codec alone, not the barrel: `@geekmidas/events/sqs` re-exports the
|
|
2
|
+
// publisher and its AWS SDK client, and a deploy config that only needs to
|
|
3
|
+
// *compose a URL* should not have to resolve a runtime it never calls.
|
|
4
|
+
import * as sqsUrl from '@geekmidas/events/sqs/url';
|
|
5
|
+
import { type GkmLinkable, ResourceType } from '../Linkable';
|
|
6
|
+
import { regionOfArn } from '../naming';
|
|
7
|
+
import type { StackType } from '../Stack';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* `Queue` — a linkable SQS queue (wraps `sst.aws.Queue`), the point-to-point
|
|
11
|
+
* work queue. Link it to a producer and the runtime resolves `<NAME>_URL`,
|
|
12
|
+
* `<NAME>_ARN`, and a `<NAME>_PUBLISHER_CONNECTION_STRING` (`sqs://?queueUrl=…`)
|
|
13
|
+
* that `@geekmidas/events`'s `Publisher.fromConnectionString` consumes. Its
|
|
14
|
+
* single consumer is wired by `QueueSubscriber`.
|
|
15
|
+
*
|
|
16
|
+
* SST's native `Queue` link exposes only `url`, so `getSSTLink` is overridden to
|
|
17
|
+
* also expose `arn` (what the resolver needs). `QueueProps` extends
|
|
18
|
+
* `sst.aws.QueueArgs`. Source-only (extends ambient `sst.aws.*`); see docs §2.
|
|
19
|
+
*/
|
|
20
|
+
export class Queue<
|
|
21
|
+
TStage extends string = string,
|
|
22
|
+
TDomain extends string = string,
|
|
23
|
+
>
|
|
24
|
+
extends sst.aws.Queue
|
|
25
|
+
implements GkmLinkable
|
|
26
|
+
{
|
|
27
|
+
readonly _id!: string;
|
|
28
|
+
|
|
29
|
+
get _type() {
|
|
30
|
+
return ResourceType.Queue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
constructor(
|
|
34
|
+
_stack: StackType<TStage, TDomain>,
|
|
35
|
+
name: string,
|
|
36
|
+
props: QueueProps = {},
|
|
37
|
+
) {
|
|
38
|
+
const { queueName, ...args } = props;
|
|
39
|
+
|
|
40
|
+
super(name, {
|
|
41
|
+
...args,
|
|
42
|
+
// A supplied name has to satisfy SQS's rule; an auto-generated one is
|
|
43
|
+
// Pulumi's problem and it already handles it.
|
|
44
|
+
...(queueName
|
|
45
|
+
? {
|
|
46
|
+
transform: {
|
|
47
|
+
...args.transform,
|
|
48
|
+
queue: { name: fifoName(queueName, args.fifo) },
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
: {}),
|
|
52
|
+
});
|
|
53
|
+
this._id = name;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The values this queue resolves onto anything that publishes to it, keyed
|
|
58
|
+
* by role. One key, the producer's: a worker is reached *through* the queue,
|
|
59
|
+
* so there is no second address and nothing can depend on the handler.
|
|
60
|
+
*
|
|
61
|
+
* The region is read out of the queue's own ARN rather than left for the SDK
|
|
62
|
+
* to infer. `AWS_REGION` inside a Lambda is the *function's* region, so a
|
|
63
|
+
* connection string that omits it works until the day the queue lives
|
|
64
|
+
* somewhere else and then fails at runtime with nothing to point at.
|
|
65
|
+
*/
|
|
66
|
+
provides(): Record<string, $util.Input<string>> {
|
|
67
|
+
return {
|
|
68
|
+
publisherConnectionString: $util
|
|
69
|
+
.all([this.url, this.arn])
|
|
70
|
+
.apply(([queueUrl, arn]) =>
|
|
71
|
+
sqsUrl.build({ queueUrl, region: regionOfArn(arn) }),
|
|
72
|
+
),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
override getSSTLink() {
|
|
77
|
+
const link = super.getSSTLink();
|
|
78
|
+
return {
|
|
79
|
+
...link,
|
|
80
|
+
properties: { ...link.properties, arn: this.arn, ...this.provides() },
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface QueueProps extends sst.aws.QueueArgs {
|
|
86
|
+
/**
|
|
87
|
+
* The physical queue name, where the auto-generated one will not do.
|
|
88
|
+
*
|
|
89
|
+
* Normalised for SQS's FIFO rule — see {@link fifoName}. This is a separate
|
|
90
|
+
* prop rather than a `transform` because the rule is easy to not know and
|
|
91
|
+
* expensive to discover: the deploy fails at the API call, long after the
|
|
92
|
+
* plan looked fine.
|
|
93
|
+
*/
|
|
94
|
+
queueName?: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* A FIFO queue's name must end in `.fifo`, and a standard queue's must not.
|
|
99
|
+
*
|
|
100
|
+
* AWS rejects either mistake at the API call rather than at plan time, so the
|
|
101
|
+
* component fixes it where the fact lives instead of leaving it as something
|
|
102
|
+
* every caller has to remember. Appending is safe: `.fifo` counts toward the
|
|
103
|
+
* 80-character limit, so a name already carrying it must not get a second one.
|
|
104
|
+
*/
|
|
105
|
+
export function fifoName(
|
|
106
|
+
name: string,
|
|
107
|
+
fifo: sst.aws.QueueArgs['fifo'],
|
|
108
|
+
): string {
|
|
109
|
+
// `fifo` is an Input and may be an object (`{ contentBasedDeduplication }`),
|
|
110
|
+
// which is still FIFO — only `false` and `undefined` are not.
|
|
111
|
+
const isFifo = fifo !== undefined && fifo !== false;
|
|
112
|
+
|
|
113
|
+
if (!isFifo) return name.replace(/\.fifo$/, '');
|
|
114
|
+
|
|
115
|
+
return name.endsWith('.fifo') ? name : `${name}.fifo`;
|
|
116
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { type GkmLinkable, ResourceType } from '../Linkable';
|
|
2
|
+
import type { StackType } from '../Stack';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `RestApiSurface` — an HTTP API, and the infra half of the `rest-api` kind.
|
|
6
|
+
*
|
|
7
|
+
* Deliberately *not* `Api`, which takes a route table and validates each
|
|
8
|
+
* route's environment at synth. That component is the endpoint pipeline's, and
|
|
9
|
+
* it needs routes; this one is the manifest's, and the manifest's `rest-api`
|
|
10
|
+
* node currently carries `endpoints: []` for an application's own API.
|
|
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.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export interface RestApiSurfaceProps extends sst.aws.ApiGatewayV2Args {
|
|
24
|
+
/**
|
|
25
|
+
* Who may call this surface, resolved after everything is provisioned.
|
|
26
|
+
*
|
|
27
|
+
* A promise rather than a value, because the answer depends on constructs
|
|
28
|
+
* that do not exist yet when this one is built — see `provides()`.
|
|
29
|
+
*/
|
|
30
|
+
callers?: Promise<{ trustedOrigins: string; cookieDomain?: string }>;
|
|
31
|
+
}
|
|
32
|
+
export class RestApiSurface<
|
|
33
|
+
TStage extends string = string,
|
|
34
|
+
TDomain extends string = string,
|
|
35
|
+
>
|
|
36
|
+
extends sst.aws.ApiGatewayV2
|
|
37
|
+
implements GkmLinkable
|
|
38
|
+
{
|
|
39
|
+
readonly _id!: string;
|
|
40
|
+
|
|
41
|
+
get _type() {
|
|
42
|
+
return ResourceType.ApiGatewayV2;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private readonly callers: Promise<{
|
|
46
|
+
trustedOrigins: string;
|
|
47
|
+
cookieDomain?: string;
|
|
48
|
+
}>;
|
|
49
|
+
|
|
50
|
+
constructor(
|
|
51
|
+
_stack: StackType<TStage, TDomain>,
|
|
52
|
+
name: string,
|
|
53
|
+
props: RestApiSurfaceProps = {},
|
|
54
|
+
) {
|
|
55
|
+
const { callers, ...args } = props;
|
|
56
|
+
|
|
57
|
+
super(name, args);
|
|
58
|
+
this._id = name;
|
|
59
|
+
this.callers = callers ?? Promise.resolve({ trustedOrigins: '' });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Where the surface answers, and who may call it.
|
|
64
|
+
*
|
|
65
|
+
* `trustedOrigins` and `cookieDomain` are declared by the construct and are
|
|
66
|
+
* *caller*-derived — read off the graph, not off this resource — so they are
|
|
67
|
+
* supplied through props rather than composed here. A surface knows its own
|
|
68
|
+
* address and nothing about who points at it.
|
|
69
|
+
*/
|
|
70
|
+
provides(): Record<string, $util.Input<string>> {
|
|
71
|
+
return {
|
|
72
|
+
url: this.url,
|
|
73
|
+
// Lazy, and it has to be. A surface's callers include the site, and
|
|
74
|
+
// the site's own build needs *this* surface's address — so the two
|
|
75
|
+
// values are circular even though the two resources are not. A
|
|
76
|
+
// promise resolved after everything is provisioned breaks it at the
|
|
77
|
+
// value level, which is the level the cycle is actually on.
|
|
78
|
+
trustedOrigins: $util.output(this.callers.then((c) => c.trustedOrigins)),
|
|
79
|
+
cookieDomain: $util.output(
|
|
80
|
+
this.callers.then((c) => c.cookieDomain ?? ''),
|
|
81
|
+
),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
override getSSTLink() {
|
|
86
|
+
const link = super.getSSTLink();
|
|
87
|
+
return {
|
|
88
|
+
...link,
|
|
89
|
+
properties: { ...link.properties, ...this.provides() },
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { type GkmLinkable, ResourceType } from '../Linkable';
|
|
2
|
+
import type { StackType } from '../Stack';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `Secret` — a linkable SST secret, and the infra half of the `secret` kind.
|
|
6
|
+
*
|
|
7
|
+
* The odd one among the kinds: it provisions no cloud resource of its own. An
|
|
8
|
+
* SST secret is a value held in the app's own encrypted state and set out of
|
|
9
|
+
* band (`sst secret set AuthSecret …`), which is why the component is thin and
|
|
10
|
+
* why the *deploy* fails rather than the *runtime* when nobody set one — SST
|
|
11
|
+
* raises `SecretMissingError` at synth, which is the earliest anything can.
|
|
12
|
+
*
|
|
13
|
+
* It provides a value rather than an address, which is the whole reason it is a
|
|
14
|
+
* node instead of a field on whatever needs it: the thing that generates a
|
|
15
|
+
* signing key, the thing that stores it, and the thing that reads it are three
|
|
16
|
+
* different systems deployed, and one derived string locally.
|
|
17
|
+
*
|
|
18
|
+
* Usable on its own. `Stack.fromManifest` translates a manifest into these, but
|
|
19
|
+
* the component takes an ordinary placeholder so it works in a hand-written
|
|
20
|
+
* `sst.config.ts` with no manifest anywhere.
|
|
21
|
+
*/
|
|
22
|
+
export class Secret<
|
|
23
|
+
TStage extends string = string,
|
|
24
|
+
TDomain extends string = string,
|
|
25
|
+
>
|
|
26
|
+
extends sst.Secret
|
|
27
|
+
implements GkmLinkable
|
|
28
|
+
{
|
|
29
|
+
readonly _id!: string;
|
|
30
|
+
|
|
31
|
+
get _type() {
|
|
32
|
+
return ResourceType.SSTSecret;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
constructor(
|
|
36
|
+
_stack: StackType<TStage, TDomain>,
|
|
37
|
+
name: string,
|
|
38
|
+
props: SecretProps = {},
|
|
39
|
+
) {
|
|
40
|
+
super(name, props.placeholder);
|
|
41
|
+
this._id = name;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The one value this secret resolves onto anything that depends on it.
|
|
46
|
+
*
|
|
47
|
+
* `value` is the role, so `provideKey` turns it into the key the app
|
|
48
|
+
* declared. Note that a secret's key is its *name* rather than
|
|
49
|
+
* `<NAME>_VALUE` — `Auth` signs with `AUTH_SECRET`, which is also what
|
|
50
|
+
* better-auth's own tooling looks for — so the manifest declares that key
|
|
51
|
+
* directly and this contributes the value behind it.
|
|
52
|
+
*/
|
|
53
|
+
provides(): Record<string, $util.Input<string>> {
|
|
54
|
+
return { value: this.value };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
override getSSTLink() {
|
|
58
|
+
const link = super.getSSTLink();
|
|
59
|
+
return {
|
|
60
|
+
...link,
|
|
61
|
+
properties: { ...link.properties, ...this.provides() },
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface SecretProps {
|
|
67
|
+
/**
|
|
68
|
+
* A value to fall back to when none is set.
|
|
69
|
+
*
|
|
70
|
+
* Deliberately not a default for anything that signs: a placeholder signing
|
|
71
|
+
* key is one every deploy of every app shares, so leaving this unset — and
|
|
72
|
+
* letting the deploy fail — is the correct handling for a real secret. It
|
|
73
|
+
* exists for the values that are secrets by habit rather than by need.
|
|
74
|
+
*/
|
|
75
|
+
placeholder?: string;
|
|
76
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { type GkmLinkable, ResourceType } from '../Linkable';
|
|
2
|
+
import type { StackType } from '../Stack';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `StaticSite` — a built frontend on a distribution, and the infra half of the
|
|
6
|
+
* `site` kind.
|
|
7
|
+
*
|
|
8
|
+
* The same infrastructure a file server is, which is why they are neighbours
|
|
9
|
+
* here: a bucket, a distribution, a certificate and a domain. What differs is
|
|
10
|
+
* only what sits in the bucket — a build output rather than live contents.
|
|
11
|
+
*
|
|
12
|
+
* Its `environment` is the interesting part and is not composed here. The values
|
|
13
|
+
* a site inlines are derived from its *edges* by `publicEnvFor`, filtered by
|
|
14
|
+
* which provided values may be shipped to a browser, and renamed by the
|
|
15
|
+
* variant's prefix. That derivation is shared with the local target, so a site
|
|
16
|
+
* built by `gkm dev` and the same site built by a deploy inline the same names.
|
|
17
|
+
*/
|
|
18
|
+
export class StaticSite<
|
|
19
|
+
TStage extends string = string,
|
|
20
|
+
TDomain extends string = string,
|
|
21
|
+
>
|
|
22
|
+
extends sst.aws.StaticSite
|
|
23
|
+
implements GkmLinkable
|
|
24
|
+
{
|
|
25
|
+
readonly _id!: string;
|
|
26
|
+
|
|
27
|
+
get _type() {
|
|
28
|
+
return ResourceType.StaticSite;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
constructor(
|
|
32
|
+
_stack: StackType<TStage, TDomain>,
|
|
33
|
+
name: string,
|
|
34
|
+
props: StaticSiteProps = {},
|
|
35
|
+
) {
|
|
36
|
+
const { variant = 'static', ...args } = props;
|
|
37
|
+
|
|
38
|
+
super(name, {
|
|
39
|
+
// The variant's build, unless the caller supplied one. A site is
|
|
40
|
+
// source until something builds it — uploading `path` directly is what
|
|
41
|
+
// puts `src/` and `node_modules/` in a bucket — and *how* to build is
|
|
42
|
+
// exactly what the variant knows and the declaration deliberately does
|
|
43
|
+
// not.
|
|
44
|
+
build: BUILDS[variant],
|
|
45
|
+
...args,
|
|
46
|
+
});
|
|
47
|
+
this._id = name;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Where the site is served.
|
|
52
|
+
*
|
|
53
|
+
* Worth providing even though a browser gets there by typing it: an email
|
|
54
|
+
* templating a link back to the console needs this URL, and that is otherwise
|
|
55
|
+
* one more hand-maintained variable — the exact class of thing declaring a
|
|
56
|
+
* site was meant to remove.
|
|
57
|
+
*/
|
|
58
|
+
provides(): Record<string, $util.Input<string>> {
|
|
59
|
+
return { url: this.url };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
override getSSTLink() {
|
|
63
|
+
const link = super.getSSTLink();
|
|
64
|
+
return {
|
|
65
|
+
...link,
|
|
66
|
+
properties: { ...link.properties, ...this.provides() },
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface StaticSiteProps extends sst.aws.StaticSiteArgs {
|
|
72
|
+
/** Which framework builds it, for the default build command. */
|
|
73
|
+
variant?: 'static' | 'next' | 'tanstack';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* How each variant builds, and where it leaves the result.
|
|
78
|
+
*
|
|
79
|
+
* One neutral name from the construct, one build per framework — the same
|
|
80
|
+
* arrangement the `VITE_`/`NEXT_PUBLIC_` prefixes have, and for the same
|
|
81
|
+
* reason: the framework changes the code you write, so it changes this and
|
|
82
|
+
* nothing else.
|
|
83
|
+
*/
|
|
84
|
+
const BUILDS: Record<
|
|
85
|
+
NonNullable<StaticSiteProps['variant']>,
|
|
86
|
+
{ command: string; output: string }
|
|
87
|
+
> = {
|
|
88
|
+
static: { command: 'npm run build', output: 'dist' },
|
|
89
|
+
tanstack: { command: 'npm run build', output: 'dist' },
|
|
90
|
+
// Next's static export lands in `out`, not `dist`.
|
|
91
|
+
next: { command: 'npm run build', output: 'out' },
|
|
92
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type GkmLinkable, ResourceType } from '
|
|
2
|
-
import type { StackType } from '
|
|
1
|
+
import { type GkmLinkable, ResourceType } from '../Linkable';
|
|
2
|
+
import type { StackType } from '../Stack';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* `Storage` — a linkable S3 bucket (wraps `sst.aws.Bucket`). Link it to a
|
|
@@ -39,7 +39,7 @@ export class Storage<
|
|
|
39
39
|
readonly _id!: string;
|
|
40
40
|
|
|
41
41
|
get _type() {
|
|
42
|
-
return ResourceType.
|
|
42
|
+
return ResourceType.ObjectStorage;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
constructor(
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// The codec alone — see the note in `Queue.ts`.
|
|
2
|
+
import * as snsUrl from '@geekmidas/events/sns/url';
|
|
3
|
+
import { type GkmLinkable, ResourceType } from '../Linkable';
|
|
4
|
+
import { regionOfArn } from '../naming';
|
|
5
|
+
import type { StackType } from '../Stack';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `Topic` — a linkable SNS topic (wraps `sst.aws.SnsTopic`), the pub/sub fan-out
|
|
9
|
+
* bus. Link it to a publisher and the runtime resolves `<NAME>_ARN` and a
|
|
10
|
+
* `<NAME>_PUBLISHER_CONNECTION_STRING` (`sns://?topicArn=…`) that
|
|
11
|
+
* `@geekmidas/events`'s `Publisher.fromConnectionString` consumes. Subscribers
|
|
12
|
+
* attach via `TopicSubscriber`/`Subscriber`.
|
|
13
|
+
*
|
|
14
|
+
* `StorageProps`-style: `TopicProps` extends `sst.aws.SnsTopicArgs`.
|
|
15
|
+
* Source-only (extends ambient `sst.aws.*`); see docs §2.
|
|
16
|
+
*/
|
|
17
|
+
export class Topic<
|
|
18
|
+
TStage extends string = string,
|
|
19
|
+
TDomain extends string = string,
|
|
20
|
+
>
|
|
21
|
+
extends sst.aws.SnsTopic
|
|
22
|
+
implements GkmLinkable
|
|
23
|
+
{
|
|
24
|
+
readonly _id!: string;
|
|
25
|
+
|
|
26
|
+
get _type() {
|
|
27
|
+
return ResourceType.SnsTopic;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
constructor(
|
|
31
|
+
_stack: StackType<TStage, TDomain>,
|
|
32
|
+
name: string,
|
|
33
|
+
props: TopicProps = {},
|
|
34
|
+
) {
|
|
35
|
+
super(name, props);
|
|
36
|
+
this._id = name;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The publisher's connection string, and nothing else.
|
|
41
|
+
*
|
|
42
|
+
* A subscriber is *bound* to a topic rather than depending on it, so the
|
|
43
|
+
* binding is an edge the deploy target reads and not a key anyone can hold —
|
|
44
|
+
* which is what keeps a subscriber from being able to publish.
|
|
45
|
+
*
|
|
46
|
+
* The region comes out of the topic's own ARN, for the same reason it does
|
|
47
|
+
* on a queue: the reader's region is not the resource's.
|
|
48
|
+
*/
|
|
49
|
+
provides(): Record<string, $util.Input<string>> {
|
|
50
|
+
return {
|
|
51
|
+
publisherConnectionString: $util
|
|
52
|
+
.output(this.arn)
|
|
53
|
+
.apply((topicArn) =>
|
|
54
|
+
snsUrl.build({ topicArn, region: regionOfArn(topicArn) }),
|
|
55
|
+
),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
override getSSTLink() {
|
|
60
|
+
const link = super.getSSTLink();
|
|
61
|
+
return {
|
|
62
|
+
...link,
|
|
63
|
+
properties: { ...link.properties, ...this.provides() },
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface TopicProps extends sst.aws.SnsTopicArgs {}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The database bootstrap — role and schema DDL, run inside the VPC.
|
|
3
|
+
*
|
|
4
|
+
* SST provisions a cluster; nothing in Pulumi runs SQL. So the statements a
|
|
5
|
+
* tenant needs — its owner, its runtime role, its schema, and the grants between
|
|
6
|
+
* them — have to be applied by something that can reach the database, which on
|
|
7
|
+
* AWS means a Lambda in the same VPC.
|
|
8
|
+
*
|
|
9
|
+
* It connects **as the cluster master**, because that is the only credential
|
|
10
|
+
* that exists before any role does. That is also why this runs once per deploy
|
|
11
|
+
* and nothing else holds those credentials: the master reaches this function
|
|
12
|
+
* through its link and reaches nothing else.
|
|
13
|
+
*
|
|
14
|
+
* The statements come from `@geekmidas/db/pg/roles`, the same generator the
|
|
15
|
+
* local target uses. One implementation is what stops "works locally, fails
|
|
16
|
+
* deployed" — and it means the grants a developer tested against are the grants
|
|
17
|
+
* production gets.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { roleStatements } from '@geekmidas/db/pg/roles';
|
|
21
|
+
import pg from 'pg';
|
|
22
|
+
|
|
23
|
+
/** One tenant to bootstrap. */
|
|
24
|
+
export interface BootstrapTenant {
|
|
25
|
+
/** The construct id, so a failure can name it. */
|
|
26
|
+
id: string;
|
|
27
|
+
schema: string;
|
|
28
|
+
runtime: string;
|
|
29
|
+
owner: string;
|
|
30
|
+
reader?: string;
|
|
31
|
+
passwords: { runtime: string; owner: string; reader?: string };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface BootstrapEvent {
|
|
35
|
+
/** The master credential, from the cluster's link. */
|
|
36
|
+
master: {
|
|
37
|
+
host: string;
|
|
38
|
+
port: number;
|
|
39
|
+
database: string;
|
|
40
|
+
username: string;
|
|
41
|
+
password: string;
|
|
42
|
+
};
|
|
43
|
+
tenants: BootstrapTenant[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface BootstrapResult {
|
|
47
|
+
applied: { tenant: string; describe: string; created: boolean }[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Apply every tenant's DDL, convergently.
|
|
52
|
+
*
|
|
53
|
+
* Each statement asks whether it is needed first, exactly as the local applier
|
|
54
|
+
* does, so a re-deploy is free and a half-applied run recovers by running again.
|
|
55
|
+
* That matters more here than locally: a Lambda can time out mid-way, and the
|
|
56
|
+
* recovery has to be "invoke it again" rather than "work out what happened".
|
|
57
|
+
*/
|
|
58
|
+
export async function handler(event: BootstrapEvent): Promise<BootstrapResult> {
|
|
59
|
+
const client = new pg.Client({
|
|
60
|
+
host: event.master.host,
|
|
61
|
+
port: event.master.port,
|
|
62
|
+
database: event.master.database,
|
|
63
|
+
user: event.master.username,
|
|
64
|
+
password: event.master.password,
|
|
65
|
+
// The cluster is inside the VPC and RDS presents a certificate this
|
|
66
|
+
// runtime does not carry a root for. Encrypted, unverified — which is the
|
|
67
|
+
// same posture the AWS SDK's own RDS helpers take, and worth naming rather
|
|
68
|
+
// than leaving as an unexplained `false`.
|
|
69
|
+
ssl: { rejectUnauthorized: false },
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
await client.connect();
|
|
73
|
+
|
|
74
|
+
const applied: BootstrapResult['applied'] = [];
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
for (const tenant of event.tenants) {
|
|
78
|
+
for (const statement of roleStatements(tenant)) {
|
|
79
|
+
if (statement.exists) {
|
|
80
|
+
const { rows } = await client.query(
|
|
81
|
+
statement.exists.sql,
|
|
82
|
+
statement.exists.values,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
if (rows.length > 0) {
|
|
86
|
+
applied.push({
|
|
87
|
+
tenant: tenant.id,
|
|
88
|
+
describe: statement.describe,
|
|
89
|
+
created: false,
|
|
90
|
+
});
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
await client.query(statement.sql);
|
|
96
|
+
applied.push({
|
|
97
|
+
tenant: tenant.id,
|
|
98
|
+
describe: statement.describe,
|
|
99
|
+
// A grant has no existence check and re-granting is a no-op, so it
|
|
100
|
+
// reports unchanged — a converged deploy still reads as converged.
|
|
101
|
+
created: Boolean(statement.exists),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} finally {
|
|
106
|
+
await client.end();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { applied };
|
|
110
|
+
}
|