@geekmidas/cloud 10.0.0-alpha.50 → 10.0.0-alpha.51
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/__type-tests__/overrides.type-test.ts +152 -0
- package/src/sst/fromManifest.ts +24 -24
- package/src/sst/index.ts +5 -1
- package/src/sst/overrides.ts +133 -0
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.51",
|
|
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.51",
|
|
45
|
+
"@geekmidas/manifest": "10.0.0-alpha.51"
|
|
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.51",
|
|
62
|
+
"@geekmidas/storage": "^10.0.0-alpha.51",
|
|
63
|
+
"@geekmidas/events": "^10.0.0-alpha.51",
|
|
64
|
+
"@geekmidas/db": "^10.0.0-alpha.51"
|
|
65
65
|
},
|
|
66
66
|
"peerDependenciesMeta": {
|
|
67
67
|
"@geekmidas/manifest": {
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// Type-level tests for `fromManifest`'s overrides, inferred from the manifest.
|
|
2
|
+
// Checked by `ts:check:sst`; each `@ts-expect-error` self-validates: if the
|
|
3
|
+
// inference regresses, the now-unused directive becomes a TS error the gate
|
|
4
|
+
// catches.
|
|
5
|
+
|
|
6
|
+
import type { ConstructManifest } from '@geekmidas/manifest';
|
|
7
|
+
import { App } from '../App';
|
|
8
|
+
import { fromManifest } from '../fromManifest';
|
|
9
|
+
|
|
10
|
+
const stack = new App({
|
|
11
|
+
name: 'a',
|
|
12
|
+
stage: 'dev',
|
|
13
|
+
domain: 'example.com',
|
|
14
|
+
hostedZoneId: 'Z',
|
|
15
|
+
region: 'us-east-1',
|
|
16
|
+
}).stack('shop');
|
|
17
|
+
|
|
18
|
+
declare const vpc: sst.aws.Vpc;
|
|
19
|
+
|
|
20
|
+
// The shape `gkm build` emits: `as const satisfies ConstructManifest`.
|
|
21
|
+
const constructs = {
|
|
22
|
+
Orders: {
|
|
23
|
+
kind: 'database',
|
|
24
|
+
id: 'Orders',
|
|
25
|
+
engine: 'postgres',
|
|
26
|
+
provides: ['ORDERS_URL'],
|
|
27
|
+
},
|
|
28
|
+
Billing: {
|
|
29
|
+
kind: 'database-schema',
|
|
30
|
+
id: 'Billing',
|
|
31
|
+
of: 'Orders',
|
|
32
|
+
schema: 'billing',
|
|
33
|
+
provides: ['BILLING_URL'],
|
|
34
|
+
},
|
|
35
|
+
Sessions: { kind: 'cache', id: 'Sessions', provides: ['SESSIONS_URL'] },
|
|
36
|
+
Rates: {
|
|
37
|
+
kind: 'cache',
|
|
38
|
+
id: 'Rates',
|
|
39
|
+
of: 'Orders',
|
|
40
|
+
provides: ['RATES_URL'],
|
|
41
|
+
},
|
|
42
|
+
Mail: { kind: 'email', id: 'Mail', provides: ['MAIL_URL'] },
|
|
43
|
+
Uploads: { kind: 'objects', id: 'Uploads', provides: ['UPLOADS_URL'] },
|
|
44
|
+
Api: {
|
|
45
|
+
kind: 'rest-api',
|
|
46
|
+
id: 'Api',
|
|
47
|
+
path: 'apps/api',
|
|
48
|
+
endpoints: [],
|
|
49
|
+
provides: ['API_URL'],
|
|
50
|
+
},
|
|
51
|
+
} as const satisfies ConstructManifest;
|
|
52
|
+
|
|
53
|
+
// Valid: the two the synth cannot guess, plus an optional bucket prop.
|
|
54
|
+
export const ok = fromManifest(stack, constructs, {
|
|
55
|
+
Orders: { vpc },
|
|
56
|
+
Mail: { from: 'no-reply@example.com' },
|
|
57
|
+
Uploads: { cors: false },
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// A database without its VPC.
|
|
61
|
+
// @ts-expect-error — `Orders` is a database: `vpc` is required.
|
|
62
|
+
fromManifest(stack, constructs, { Mail: { from: 'a@example.com' } });
|
|
63
|
+
|
|
64
|
+
// Mail without a sender.
|
|
65
|
+
fromManifest(stack, constructs, {
|
|
66
|
+
Orders: { vpc },
|
|
67
|
+
// @ts-expect-error — every provider rejects an unverified sender.
|
|
68
|
+
Mail: {},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// A misspelt id.
|
|
72
|
+
fromManifest(stack, constructs, {
|
|
73
|
+
Orders: { vpc },
|
|
74
|
+
Mail: { from: 'a@example.com' },
|
|
75
|
+
// @ts-expect-error — not a construct in this manifest.
|
|
76
|
+
Ordrs: { vpc },
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// A prop the kind's component never reads.
|
|
80
|
+
fromManifest(stack, constructs, {
|
|
81
|
+
Orders: { vpc },
|
|
82
|
+
Mail: { from: 'a@example.com' },
|
|
83
|
+
// @ts-expect-error — a surface takes API Gateway args, not bucket ones.
|
|
84
|
+
Api: { versioning: true },
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// What the declaration already decides is not overridable.
|
|
88
|
+
fromManifest(stack, constructs, {
|
|
89
|
+
// @ts-expect-error — the schema is the declaration's, not the deploy's.
|
|
90
|
+
Orders: { vpc, schema: 'other' },
|
|
91
|
+
Mail: { from: 'a@example.com' },
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// A kind with nothing to override takes no key.
|
|
95
|
+
fromManifest(stack, constructs, {
|
|
96
|
+
Orders: { vpc },
|
|
97
|
+
Mail: { from: 'a@example.com' },
|
|
98
|
+
// @ts-expect-error — a schema derives from its database.
|
|
99
|
+
Billing: {},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Nor does a cache in a database: same address, one more table.
|
|
103
|
+
fromManifest(stack, constructs, {
|
|
104
|
+
Orders: { vpc },
|
|
105
|
+
Mail: { from: 'a@example.com' },
|
|
106
|
+
// @ts-expect-error — `Rates` lives in `Orders`.
|
|
107
|
+
Rates: { region: 'eu-west-1' },
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// The backend decides too: ElastiCache needs a VPC to live in…
|
|
111
|
+
fromManifest(
|
|
112
|
+
stack,
|
|
113
|
+
constructs,
|
|
114
|
+
// @ts-expect-error — `Sessions` on ElastiCache needs `vpc`.
|
|
115
|
+
{ Orders: { vpc }, Mail: { from: 'a@example.com' } },
|
|
116
|
+
{ cache: 'elasticache' },
|
|
117
|
+
);
|
|
118
|
+
export const elasticache = fromManifest(
|
|
119
|
+
stack,
|
|
120
|
+
constructs,
|
|
121
|
+
{
|
|
122
|
+
Orders: { vpc },
|
|
123
|
+
Sessions: { vpc: { subnets: [], securityGroups: [] } },
|
|
124
|
+
Mail: { from: 'a@example.com' },
|
|
125
|
+
},
|
|
126
|
+
{ cache: 'elasticache' },
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// …and mail that is not SES is an account somebody created, with its URL.
|
|
130
|
+
fromManifest(
|
|
131
|
+
stack,
|
|
132
|
+
constructs,
|
|
133
|
+
{
|
|
134
|
+
Orders: { vpc },
|
|
135
|
+
// @ts-expect-error — Resend's URL is not something a deploy can mint.
|
|
136
|
+
Mail: { from: 'a@example.com' },
|
|
137
|
+
},
|
|
138
|
+
{ email: 'resend' },
|
|
139
|
+
);
|
|
140
|
+
export const resend = fromManifest(
|
|
141
|
+
stack,
|
|
142
|
+
constructs,
|
|
143
|
+
{
|
|
144
|
+
Orders: { vpc },
|
|
145
|
+
Mail: { from: 'a@example.com', url: 'smtp://resend' },
|
|
146
|
+
},
|
|
147
|
+
{ email: 'resend' },
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
// A manifest whose ids are not known statically keeps the untyped record.
|
|
151
|
+
declare const untyped: ConstructManifest;
|
|
152
|
+
export const loose = fromManifest(stack, untyped, { Anything: { at: 'all' } });
|
package/src/sst/fromManifest.ts
CHANGED
|
@@ -62,6 +62,7 @@ import {
|
|
|
62
62
|
UnresolvedDependency,
|
|
63
63
|
} from './errors';
|
|
64
64
|
import type { GkmLinkable } from './Linkable';
|
|
65
|
+
import type { ManifestBackends, ManifestOverrides } from './overrides';
|
|
65
66
|
import type { StackType } from './Stack';
|
|
66
67
|
|
|
67
68
|
/**
|
|
@@ -139,21 +140,6 @@ type Provisioner = (
|
|
|
139
140
|
context: ProvisionContext,
|
|
140
141
|
) => Provisioned;
|
|
141
142
|
|
|
142
|
-
/**
|
|
143
|
-
* Provider-specific props, per construct id.
|
|
144
|
-
*
|
|
145
|
-
* Neutral options — `versioned`, and later `cdn` — travel in the declaration,
|
|
146
|
-
* because the app legitimately has an opinion about them. Anything with S3 in
|
|
147
|
-
* its name does not belong in application code, so lifecycle rules, CORS, and
|
|
148
|
-
* canned ACLs are supplied here, in the deploy layer, keyed by the id they
|
|
149
|
-
* apply to and typed against the component that receives them.
|
|
150
|
-
*
|
|
151
|
-
* A third escape hatch needs no API at all: `fromManifest` returns the
|
|
152
|
-
* components, so `provisioned.Uploads.nodes.bucket` is reachable for anything
|
|
153
|
-
* neither route covers.
|
|
154
|
-
*/
|
|
155
|
-
export type ComponentOverrides = Record<string, Record<string, unknown>>;
|
|
156
|
-
|
|
157
143
|
/**
|
|
158
144
|
* Which component provisions which kind.
|
|
159
145
|
*
|
|
@@ -951,20 +937,34 @@ export function assertProvides(
|
|
|
951
937
|
* tenant, a read replica, and the surface over a bucket all resolve an address
|
|
952
938
|
* off something else, and a pass in map order would find it half the time.
|
|
953
939
|
*/
|
|
954
|
-
export function fromManifest
|
|
940
|
+
export function fromManifest<
|
|
941
|
+
const M extends ConstructManifest,
|
|
942
|
+
const B extends ManifestBackends = {},
|
|
943
|
+
>(
|
|
955
944
|
stack: StackType,
|
|
956
|
-
manifest:
|
|
957
|
-
|
|
945
|
+
manifest: M,
|
|
946
|
+
/**
|
|
947
|
+
* Provider-specific props, per construct id — typed from the manifest, so
|
|
948
|
+
* only its ids are keys, each takes what its kind's component reads, and
|
|
949
|
+
* what the synth cannot guess (a database's `vpc`, mail's `from`) is
|
|
950
|
+
* required. See {@link ManifestOverrides}.
|
|
951
|
+
*
|
|
952
|
+
* Neutral options — `versioned`, `fifo` — travel in the declaration,
|
|
953
|
+
* because the app legitimately has an opinion about them. Anything with S3
|
|
954
|
+
* in its name does not belong in application code, so it is supplied here.
|
|
955
|
+
* A third escape hatch needs no API: the components are returned, so
|
|
956
|
+
* `provisioned.Uploads.nodes.bucket` is reachable for anything else.
|
|
957
|
+
*/
|
|
958
|
+
overrides: ManifestOverrides<M, B>,
|
|
958
959
|
/**
|
|
959
960
|
* The backend choices that are config rather than declaration.
|
|
960
961
|
*
|
|
961
962
|
* Defaulted the same way the local target defaults them, so a stage deployed
|
|
962
|
-
* without saying gets the same backend a developer ran against.
|
|
963
|
+
* without saying gets the same backend a developer ran against. Typed
|
|
964
|
+
* literally, because the backend decides what is required above: an
|
|
965
|
+
* ElastiCache cache needs a `vpc`, Resend or SMTP mail a `url`.
|
|
963
966
|
*/
|
|
964
|
-
backends: {
|
|
965
|
-
cache?: 'upstash' | 'elasticache' | 'db';
|
|
966
|
-
email?: 'resend' | 'ses' | 'smtp';
|
|
967
|
-
} = {},
|
|
967
|
+
backends: B = {} as B,
|
|
968
968
|
): ProvisionedManifest {
|
|
969
969
|
const provisioned: ProvisionedManifest = {};
|
|
970
970
|
|
|
@@ -1006,7 +1006,7 @@ export function fromManifest(
|
|
|
1006
1006
|
const component = provisionerFor(declaration.kind)(
|
|
1007
1007
|
stack,
|
|
1008
1008
|
declaration,
|
|
1009
|
-
overrides[id] ?? {},
|
|
1009
|
+
(overrides as Record<string, Record<string, unknown>>)[id] ?? {},
|
|
1010
1010
|
context,
|
|
1011
1011
|
);
|
|
1012
1012
|
|
package/src/sst/index.ts
CHANGED
|
@@ -62,7 +62,6 @@ export {
|
|
|
62
62
|
} from './errors';
|
|
63
63
|
export {
|
|
64
64
|
assertProvides,
|
|
65
|
-
type ComponentOverrides,
|
|
66
65
|
describeRoutes,
|
|
67
66
|
fromManifest,
|
|
68
67
|
isServed,
|
|
@@ -76,4 +75,9 @@ export {
|
|
|
76
75
|
} from './fromManifest';
|
|
77
76
|
export { type GkmLinkable, ResourceType } from './Linkable';
|
|
78
77
|
export { kebab, prefixedName, regionOfArn } from './naming';
|
|
78
|
+
export type {
|
|
79
|
+
ManifestBackends,
|
|
80
|
+
ManifestOverrides,
|
|
81
|
+
OverrideFor,
|
|
82
|
+
} from './overrides';
|
|
79
83
|
export { Stack, type StackType } from './Stack';
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a deploy can say about each construct, inferred from the manifest.
|
|
3
|
+
*
|
|
4
|
+
* `fromManifest` used to take `Record<string, Record<string, unknown>>`, so a
|
|
5
|
+
* misspelt id, a prop the component never reads, and a missing VPC all passed
|
|
6
|
+
* the type checker — the first two silently, the last at synth, mid-deploy.
|
|
7
|
+
* The manifest is emitted `as const`, so every id's kind is known statically,
|
|
8
|
+
* and the kind decides what can be overridden:
|
|
9
|
+
*
|
|
10
|
+
* - only the manifest's own ids are keys;
|
|
11
|
+
* - each takes the props its kind's provisioner reads, minus those the
|
|
12
|
+
* declaration already decides (a site's `path`, a file server's `origin`);
|
|
13
|
+
* - what the synth refuses to guess is required — a database's `vpc`, mail's
|
|
14
|
+
* `from`, and, by backend, an ElastiCache `vpc` or a non-SES mail `url`;
|
|
15
|
+
* - a kind with nothing to override takes no key at all.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { ConstructManifest, DeclarationKind } from '@geekmidas/manifest';
|
|
19
|
+
import type { CacheProps } from './aws/Cache';
|
|
20
|
+
import type { CredentialProps } from './aws/Credential';
|
|
21
|
+
import type { DatabaseProps } from './aws/Database';
|
|
22
|
+
import type { EncryptionProps } from './aws/Encryption';
|
|
23
|
+
import type { ExternalApiProps } from './aws/ExternalApi';
|
|
24
|
+
import type { FileServerProps } from './aws/FileServer';
|
|
25
|
+
import type { ObjectStorageProps } from './aws/ObjectStorage';
|
|
26
|
+
import type { QueueProps } from './aws/Queue';
|
|
27
|
+
import type { SecretProps } from './aws/Secret';
|
|
28
|
+
import type { StaticSiteProps } from './aws/StaticSite';
|
|
29
|
+
import type { TopicProps } from './aws/Topic';
|
|
30
|
+
|
|
31
|
+
/** The backend choices that are config rather than declaration. */
|
|
32
|
+
export interface ManifestBackends {
|
|
33
|
+
cache?: 'upstash' | 'elasticache' | 'db';
|
|
34
|
+
email?: 'resend' | 'ses' | 'smtp';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The backend a choice resolves to — the provisioner's default when unset. */
|
|
38
|
+
type CacheBackendOf<B extends ManifestBackends> =
|
|
39
|
+
B['cache'] extends NonNullable<ManifestBackends['cache']>
|
|
40
|
+
? B['cache']
|
|
41
|
+
: 'upstash';
|
|
42
|
+
type EmailBackendOf<B extends ManifestBackends> =
|
|
43
|
+
B['email'] extends NonNullable<ManifestBackends['email']>
|
|
44
|
+
? B['email']
|
|
45
|
+
: 'ses';
|
|
46
|
+
|
|
47
|
+
type CacheOverride<Backend> = Backend extends 'elasticache'
|
|
48
|
+
? { vpc: NonNullable<CacheProps['vpc']> }
|
|
49
|
+
: Backend extends 'upstash'
|
|
50
|
+
? { url?: CacheProps['url']; region?: string }
|
|
51
|
+
: never;
|
|
52
|
+
|
|
53
|
+
type EmailOverride<Backend> = {
|
|
54
|
+
/** Every provider rejects an unverified sender, so there is no default. */
|
|
55
|
+
from: $util.Input<string>;
|
|
56
|
+
region?: $util.Input<string>;
|
|
57
|
+
} & (Backend extends 'ses'
|
|
58
|
+
? { url?: $util.Input<string> }
|
|
59
|
+
: // Resend and a relay are accounts somebody created: their URL is theirs.
|
|
60
|
+
{ url: $util.Input<string> });
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* What one declaration takes, by its kind and the backends; `never` for a kind
|
|
64
|
+
* with nothing to override, which drops its id from the keys.
|
|
65
|
+
*/
|
|
66
|
+
export type OverrideFor<D, B extends ManifestBackends = {}> = D extends {
|
|
67
|
+
kind: infer K extends DeclarationKind;
|
|
68
|
+
}
|
|
69
|
+
? K extends 'objects'
|
|
70
|
+
? Partial<ObjectStorageProps>
|
|
71
|
+
: K extends 'file-server'
|
|
72
|
+
? Partial<Omit<FileServerProps, 'origin'>>
|
|
73
|
+
: K extends 'site'
|
|
74
|
+
? Partial<Omit<StaticSiteProps, 'path' | 'variant' | 'environment'>>
|
|
75
|
+
: K extends 'queue'
|
|
76
|
+
? Partial<Omit<QueueProps, 'fifo'>>
|
|
77
|
+
: K extends 'topic'
|
|
78
|
+
? Partial<TopicProps>
|
|
79
|
+
: K extends 'database'
|
|
80
|
+
? Omit<DatabaseProps, 'schema' | 'version'>
|
|
81
|
+
: K extends 'cache'
|
|
82
|
+
? // In a database: same address, one more table, nothing to say.
|
|
83
|
+
D extends { of: string }
|
|
84
|
+
? never
|
|
85
|
+
: CacheOverride<CacheBackendOf<B>>
|
|
86
|
+
: K extends 'email'
|
|
87
|
+
? EmailOverride<EmailBackendOf<B>>
|
|
88
|
+
: K extends 'rest-api'
|
|
89
|
+
? Partial<sst.aws.ApiGatewayV2Args>
|
|
90
|
+
: K extends 'secret'
|
|
91
|
+
? SecretProps
|
|
92
|
+
: K extends 'credential'
|
|
93
|
+
? CredentialProps
|
|
94
|
+
: K extends 'encryption'
|
|
95
|
+
? Partial<EncryptionProps>
|
|
96
|
+
: K extends 'external-api'
|
|
97
|
+
? Omit<ExternalApiProps, 'url'>
|
|
98
|
+
: // Derived databases, compute, mobile apps, OIDC.
|
|
99
|
+
never
|
|
100
|
+
: never;
|
|
101
|
+
|
|
102
|
+
/** Whether `{}` satisfies an override — whether the id can be left out. */
|
|
103
|
+
type IsOptional<T> = {} extends T ? true : false;
|
|
104
|
+
|
|
105
|
+
type Overridable<M, B extends ManifestBackends> = {
|
|
106
|
+
[Id in keyof M]: [OverrideFor<M[Id], B>] extends [never] ? never : Id;
|
|
107
|
+
}[keyof M];
|
|
108
|
+
|
|
109
|
+
type RequiredIds<M, B extends ManifestBackends> = {
|
|
110
|
+
[Id in Overridable<M, B>]: IsOptional<OverrideFor<M[Id], B>> extends true
|
|
111
|
+
? never
|
|
112
|
+
: Id;
|
|
113
|
+
}[Overridable<M, B>];
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The overrides a manifest accepts, keyed by its own construct ids.
|
|
117
|
+
*
|
|
118
|
+
* A manifest typed only as `ConstructManifest` — ids unknown — falls back to
|
|
119
|
+
* the untyped record, and the provisioners' synth-time checks still apply.
|
|
120
|
+
*/
|
|
121
|
+
export type ManifestOverrides<
|
|
122
|
+
M extends ConstructManifest,
|
|
123
|
+
B extends ManifestBackends = {},
|
|
124
|
+
> = string extends keyof M
|
|
125
|
+
? Record<string, Record<string, unknown>>
|
|
126
|
+
: {
|
|
127
|
+
[Id in RequiredIds<M, B>]: OverrideFor<M[Id], B>;
|
|
128
|
+
} & {
|
|
129
|
+
[Id in Exclude<Overridable<M, B>, RequiredIds<M, B>>]?: OverrideFor<
|
|
130
|
+
M[Id],
|
|
131
|
+
B
|
|
132
|
+
>;
|
|
133
|
+
};
|