@geekmidas/cloud 10.0.0-alpha.50 → 10.0.0-alpha.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekmidas/cloud",
3
- "version": "10.0.0-alpha.50",
3
+ "version": "10.0.0-alpha.52",
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.50",
45
- "@geekmidas/manifest": "10.0.0-alpha.50"
44
+ "@geekmidas/envkit": "10.0.0-alpha.52",
45
+ "@geekmidas/manifest": "10.0.0-alpha.52"
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.50",
62
- "@geekmidas/storage": "^10.0.0-alpha.50",
63
- "@geekmidas/events": "^10.0.0-alpha.50",
64
- "@geekmidas/db": "^10.0.0-alpha.50"
61
+ "@geekmidas/manifest": "^10.0.0-alpha.52",
62
+ "@geekmidas/storage": "^10.0.0-alpha.52",
63
+ "@geekmidas/events": "^10.0.0-alpha.52",
64
+ "@geekmidas/db": "^10.0.0-alpha.52"
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' } });
@@ -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: ConstructManifest,
957
- overrides: ComponentOverrides = {},
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
+ };