@awsless/awsless 0.0.250 → 0.0.251

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.
Files changed (2) hide show
  1. package/dist/bin.js +928 -928
  2. package/package.json +9 -9
package/dist/bin.js CHANGED
@@ -1686,7 +1686,7 @@ var bootstrap = (program2) => {
1686
1686
  // src/app.ts
1687
1687
  import { App, Stack } from "@awsless/formation";
1688
1688
 
1689
- // src/feature/cache/index.ts
1689
+ // src/feature/auth/index.ts
1690
1690
  import { constantCase as constantCase2 } from "change-case";
1691
1691
 
1692
1692
  // src/feature.ts
@@ -1819,76 +1819,11 @@ var formatLocalResourceName = (appName, stackName, ns, id, seperator = "--") =>
1819
1819
  ].map((v) => paramCase3(v)).join(seperator);
1820
1820
  };
1821
1821
 
1822
- // src/feature/cache/index.ts
1823
- import { Node, aws } from "@awsless/formation";
1824
- var typeGenCode = `
1825
- import { Cluster, CommandOptions } from '@awsless/redis'
1826
-
1827
- type Callback<T> = (redis: Cluster) => T
1828
-
1829
- type Command = {
1830
- readonly host: string
1831
- readonly port: number
1832
- <T>(callback: Callback<T>): T
1833
- <T>(options:Omit<CommandOptions, 'cluster'>, callback: Callback<T>): T
1834
- }`;
1835
- var cacheFeature = defineFeature({
1836
- name: "cache",
1837
- async onTypeGen(ctx) {
1838
- const gen = new TypeFile("@awsless/awsless");
1839
- const resources = new TypeObject(1);
1840
- for (const stack of ctx.stackConfigs) {
1841
- const resource2 = new TypeObject(2);
1842
- for (const name of Object.keys(stack.caches || {})) {
1843
- resource2.addType(name, `Command`);
1844
- }
1845
- resources.addType(stack.name, resource2);
1846
- }
1847
- gen.addCode(typeGenCode);
1848
- gen.addInterface("CacheResources", resources);
1849
- await ctx.write("cache.d.ts", gen, true);
1850
- },
1851
- onStack(ctx) {
1852
- for (const [id, props] of Object.entries(ctx.stackConfig.caches ?? {})) {
1853
- const group = new Node(ctx.stack, this.name, id);
1854
- const name = formatLocalResourceName(ctx.appConfig.name, ctx.stack.name, this.name, id, "-");
1855
- const subnetGroup = new aws.memorydb.SubnetGroup(group, "subnets", {
1856
- name,
1857
- subnetIds: [
1858
- //
1859
- ctx.shared.get("vpc-private-subnet-id-1"),
1860
- ctx.shared.get("vpc-private-subnet-id-2")
1861
- ]
1862
- });
1863
- const securityGroup = new aws.ec2.SecurityGroup(group, "security", {
1864
- name,
1865
- vpcId: ctx.shared.get(`vpc-id`),
1866
- description: name
1867
- });
1868
- const port = aws.ec2.Port.tcp(props.port);
1869
- securityGroup.addIngressRule({ port, peer: aws.ec2.Peer.anyIpv4() });
1870
- securityGroup.addIngressRule({ port, peer: aws.ec2.Peer.anyIpv6() });
1871
- const cluster = new aws.memorydb.Cluster(group, "cluster", {
1872
- name,
1873
- aclName: "open-access",
1874
- securityGroupIds: [securityGroup.id],
1875
- subnetGroupName: subnetGroup.name,
1876
- ...props
1877
- });
1878
- ctx.onFunction(({ lambda }) => {
1879
- const prefix = `CACHE_${constantCase2(ctx.stack.name)}_${constantCase2(id)}`;
1880
- lambda.addEnvironment(`${prefix}_HOST`, cluster.address);
1881
- lambda.addEnvironment(`${prefix}_PORT`, props.port.toString());
1882
- });
1883
- }
1884
- }
1885
- });
1886
-
1887
- // src/feature/cron/index.ts
1888
- import { Node as Node3, aws as aws3 } from "@awsless/formation";
1822
+ // src/feature/auth/index.ts
1823
+ import { Node as Node2, aws as aws2 } from "@awsless/formation";
1889
1824
 
1890
1825
  // src/feature/function/util.ts
1891
- import { Asset, aws as aws2 } from "@awsless/formation";
1826
+ import { Asset, aws } from "@awsless/formation";
1892
1827
  import deepmerge from "deepmerge";
1893
1828
  import { basename as basename4, dirname as dirname6, extname as extname3 } from "path";
1894
1829
  import { exec } from "promisify-child-process";
@@ -2163,7 +2098,7 @@ var createLambdaFunction = (group, ctx, ns, id, local2) => {
2163
2098
  };
2164
2099
  });
2165
2100
  });
2166
- code = new aws2.s3.BucketObject(group, "code", {
2101
+ code = new aws.s3.BucketObject(group, "code", {
2167
2102
  bucket: ctx.shared.get("function-bucket-name"),
2168
2103
  key: `/lambda/${name}.zip`,
2169
2104
  body: Asset.fromFile(getBuildPath("function", name, "bundle.zip"))
@@ -2184,7 +2119,7 @@ var createLambdaFunction = (group, ctx, ns, id, local2) => {
2184
2119
  );
2185
2120
  });
2186
2121
  });
2187
- const image = new aws2.ecr.Image(group, "image", {
2122
+ const image = new aws.ecr.Image(group, "image", {
2188
2123
  repository: ctx.shared.get("function-repository-name"),
2189
2124
  name,
2190
2125
  tag: name
@@ -2195,12 +2130,12 @@ var createLambdaFunction = (group, ctx, ns, id, local2) => {
2195
2130
  } else {
2196
2131
  throw new Error("Unknown Lambda Function type.");
2197
2132
  }
2198
- const role = new aws2.iam.Role(group, "role", {
2133
+ const role = new aws.iam.Role(group, "role", {
2199
2134
  name,
2200
2135
  assumedBy: "lambda.amazonaws.com"
2201
2136
  // policies: inlinePolicies,
2202
2137
  });
2203
- const policy = new aws2.iam.RolePolicy(group, "policy", {
2138
+ const policy = new aws.iam.RolePolicy(group, "policy", {
2204
2139
  role: role.name,
2205
2140
  name: "lambda-policy",
2206
2141
  version: "2012-10-17",
@@ -2212,7 +2147,7 @@ var createLambdaFunction = (group, ctx, ns, id, local2) => {
2212
2147
  }
2213
2148
  ]
2214
2149
  });
2215
- const lambda = new aws2.lambda.Function(group, `function`, {
2150
+ const lambda = new aws.lambda.Function(group, `function`, {
2216
2151
  ...props,
2217
2152
  name,
2218
2153
  role: role.arn,
@@ -2227,7 +2162,7 @@ var createLambdaFunction = (group, ctx, ns, id, local2) => {
2227
2162
  lambda.addEnvironment("STACK", ctx.stackConfig.name);
2228
2163
  }
2229
2164
  if (props.log.retention.value > 0n) {
2230
- const logGroup = new aws2.cloudWatch.LogGroup(group, "log", {
2165
+ const logGroup = new aws.cloudWatch.LogGroup(group, "log", {
2231
2166
  name: lambda.name.apply((name2) => `/aws/lambda/${name2}`),
2232
2167
  retention: props.log.retention
2233
2168
  });
@@ -2249,7 +2184,7 @@ var createLambdaFunction = (group, ctx, ns, id, local2) => {
2249
2184
  policy.addStatement(...local2.permissions);
2250
2185
  }
2251
2186
  if (props.warm) {
2252
- const rule = new aws2.events.Rule(group, "warm", {
2187
+ const rule = new aws.events.Rule(group, "warm", {
2253
2188
  name: `${name}--warm`,
2254
2189
  schedule: "rate(5 minutes)",
2255
2190
  enabled: true,
@@ -2264,7 +2199,7 @@ var createLambdaFunction = (group, ctx, ns, id, local2) => {
2264
2199
  }
2265
2200
  ]
2266
2201
  });
2267
- new aws2.lambda.Permission(group, `warm`, {
2202
+ new aws.lambda.Permission(group, `warm`, {
2268
2203
  action: "lambda:InvokeFunction",
2269
2204
  principal: "events.amazonaws.com",
2270
2205
  functionArn: lambda.arn,
@@ -2279,7 +2214,7 @@ var createLambdaFunction = (group, ctx, ns, id, local2) => {
2279
2214
  ctx.shared.get(`vpc-public-subnet-id-2`)
2280
2215
  ]
2281
2216
  });
2282
- const vpcPolicy = new aws2.iam.RolePolicy(group, "vpc-policy", {
2217
+ const vpcPolicy = new aws.iam.RolePolicy(group, "vpc-policy", {
2283
2218
  role: role.name,
2284
2219
  name: "lambda-vpc-policy",
2285
2220
  version: "2012-10-17",
@@ -2308,7 +2243,7 @@ var createAsyncLambdaFunction = (group, ctx, ns, id, local2) => {
2308
2243
  const result = createLambdaFunction(group, ctx, ns, id, local2);
2309
2244
  const props = deepmerge(ctx.appConfig.defaults.function, local2);
2310
2245
  result.lambda.addEnvironment("LOG_VIEWABLE_ERROR", "1");
2311
- const invokeConfig = new aws2.lambda.EventInvokeConfig(group, "async", {
2246
+ const invokeConfig = new aws.lambda.EventInvokeConfig(group, "async", {
2312
2247
  functionArn: result.lambda.arn,
2313
2248
  retryAttempts: props.retryAttempts,
2314
2249
  onFailure: getGlobalOnFailure(ctx)
@@ -2323,139 +2258,180 @@ var createAsyncLambdaFunction = (group, ctx, ns, id, local2) => {
2323
2258
  return result;
2324
2259
  };
2325
2260
 
2326
- // src/feature/cron/index.ts
2327
- var cronFeature = defineFeature({
2328
- name: "cron",
2329
- onStack(ctx) {
2330
- for (const [id, props] of Object.entries(ctx.stackConfig.crons ?? {})) {
2331
- const group = new Node3(ctx.stack, "cron", id);
2332
- const { lambda } = createAsyncLambdaFunction(group, ctx, "cron", id, props.consumer);
2333
- const rule = new aws3.events.Rule(group, "rule", {
2334
- name: formatLocalResourceName(ctx.app.name, ctx.stack.name, this.name, id),
2335
- schedule: props.schedule,
2336
- enabled: props.enabled,
2337
- targets: [
2338
- {
2339
- id: "default",
2340
- arn: lambda.arn,
2341
- input: props.payload
2342
- }
2343
- ]
2344
- });
2345
- new aws3.lambda.Permission(group, "permission", {
2346
- action: "lambda:InvokeFunction",
2347
- principal: "events.amazonaws.com",
2348
- functionArn: lambda.arn,
2349
- sourceArn: rule.arn
2350
- });
2351
- }
2352
- }
2353
- });
2354
-
2355
- // src/feature/domain/index.ts
2356
- import { Node as Node4, aws as aws4 } from "@awsless/formation";
2357
- import { minutes as minutes3 } from "@awsless/duration";
2358
- var domainFeature = defineFeature({
2359
- name: "domain",
2360
- onApp(ctx) {
2361
- const domains = Object.entries(ctx.appConfig.defaults.domains ?? {});
2362
- if (domains.length === 0) {
2363
- return;
2261
+ // src/feature/auth/index.ts
2262
+ var authFeature = defineFeature({
2263
+ name: "auth",
2264
+ async onTypeGen(ctx) {
2265
+ const gen = new TypeFile("@awsless/awsless");
2266
+ const resources = new TypeObject(1);
2267
+ for (const name of Object.keys(ctx.appConfig.defaults.auth)) {
2268
+ const authName = formatGlobalResourceName(ctx.appConfig.name, "auth", name);
2269
+ resources.addType(
2270
+ name,
2271
+ `{ readonly name: '${authName}', readonly userPoolId: string, readonly clientId: string }`
2272
+ );
2364
2273
  }
2365
- const group = new Node4(ctx.base, "domain", "mail");
2366
- const configurationSet = new aws4.ses.ConfigurationSet(group, "config", {
2367
- name: ctx.app.name,
2368
- engagementMetrics: true,
2369
- reputationMetrics: true
2370
- });
2371
- ctx.shared.set(`mail-configuration-set`, configurationSet.name);
2372
- for (const [id, props] of domains) {
2373
- const group2 = new Node4(ctx.base, "domain", id);
2374
- const hostedZone = new aws4.route53.HostedZone(group2, "zone", {
2375
- name: props.domain
2376
- });
2377
- ctx.shared.set(`hosted-zone-${id}-id`, hostedZone.id);
2378
- const certificate = new aws4.acm.Certificate(group2, "local", {
2379
- domainName: props.domain,
2380
- alternativeNames: [`*.${props.domain}`]
2381
- });
2382
- hostedZone.addRecord("local-cert-1", certificate.validationRecord(0));
2383
- hostedZone.addRecord("local-cert-2", certificate.validationRecord(1));
2384
- const validation = new aws4.acm.CertificateValidation(group2, "local", {
2385
- certificateArn: certificate.arn
2274
+ gen.addInterface("AuthResources", resources);
2275
+ await ctx.write("auth.d.ts", gen, true);
2276
+ },
2277
+ onStack(ctx) {
2278
+ for (const [id, props] of Object.entries(ctx.stackConfig.auth ?? {})) {
2279
+ const group = new Node2(ctx.stack, "auth", id);
2280
+ const userPoolId = ctx.shared.get(`auth-${id}-user-pool-id`);
2281
+ const userPoolArn = ctx.shared.get(`auth-${id}-user-pool-arn`);
2282
+ const clientId = ctx.shared.get(`auth-${id}-client-id`);
2283
+ const triggers = {};
2284
+ const list4 = {};
2285
+ for (const [trigger, triggerProps] of Object.entries(props.triggers ?? {})) {
2286
+ const triggerGroup = new Node2(group, "trigger", trigger);
2287
+ const { lambda, policy } = createAsyncLambdaFunction(
2288
+ triggerGroup,
2289
+ ctx,
2290
+ "auth",
2291
+ `${id}-${trigger}`,
2292
+ triggerProps
2293
+ );
2294
+ triggers[trigger] = lambda.arn;
2295
+ list4[trigger] = {
2296
+ trigger,
2297
+ group: triggerGroup,
2298
+ lambda,
2299
+ policy
2300
+ };
2301
+ }
2302
+ new aws2.cognito.LambdaTriggers(group, "lambda-triggers", {
2303
+ userPoolId,
2304
+ triggers
2386
2305
  });
2387
- ctx.shared.set(`local-certificate-${id}-arn`, validation.arn);
2388
- if (ctx.appConfig.region !== "us-east-1") {
2389
- const globalCertificate = new aws4.acm.Certificate(group2, "global", {
2390
- domainName: props.domain,
2391
- alternativeNames: [`*.${props.domain}`],
2392
- region: "us-east-1"
2306
+ for (const item of Object.values(list4)) {
2307
+ new aws2.lambda.Permission(item.group, `permission`, {
2308
+ action: "lambda:InvokeFunction",
2309
+ principal: "cognito-idp.amazonaws.com",
2310
+ functionArn: item.lambda.arn,
2311
+ sourceArn: userPoolArn
2393
2312
  });
2394
- hostedZone.addRecord("global-cert-1", globalCertificate.validationRecord(0));
2395
- hostedZone.addRecord("global-cert-2", globalCertificate.validationRecord(1));
2396
- const globalValidation = new aws4.acm.CertificateValidation(group2, "global", {
2397
- certificateArn: globalCertificate.arn,
2398
- region: "us-east-1"
2313
+ item.lambda.addEnvironment(`AUTH_${constantCase2(id)}_USER_POOL_ID`, userPoolId);
2314
+ item.lambda.addEnvironment(`AUTH_${constantCase2(id)}_CLIENT_ID`, clientId);
2315
+ item.policy.addStatement({
2316
+ actions: ["cognito:*"],
2317
+ resources: [
2318
+ // Not yet known if this is correct way to grant access to all resources
2319
+ userPoolArn
2320
+ // userPoolId.apply<aws.ARN>(
2321
+ // id => `arn:aws:cognito-idp:${ctx.appConfig.region}:${ctx.accountId}:userpool/${id}`
2322
+ // ),
2323
+ // userPoolId.apply<aws.ARN>(
2324
+ // id => `arn:aws:cognito-idp:${ctx.appConfig.region}:${ctx.accountId}:userpool/${id}*`
2325
+ // ),
2326
+ ]
2399
2327
  });
2400
- ctx.shared.set(`global-certificate-${id}-arn`, globalValidation.arn);
2401
- } else {
2402
- ctx.shared.set(`global-certificate-${id}-arn`, validation.arn);
2403
2328
  }
2404
- const emailIdentity = new aws4.ses.EmailIdentity(group2, "mail", {
2405
- emailIdentity: props.domain,
2406
- mailFromDomain: `mail.${props.domain}`,
2407
- configurationSetName: configurationSet.name,
2408
- feedback: true,
2409
- rejectOnMxFailure: true
2410
- });
2411
- let i = 0;
2412
- for (const record of emailIdentity.dkimRecords) {
2413
- new aws4.route53.RecordSet(group2, `dkim-${++i}`, {
2414
- hostedZoneId: hostedZone.id,
2415
- ...record
2416
- });
2329
+ }
2330
+ },
2331
+ onApp(ctx) {
2332
+ for (const [id, props] of Object.entries(ctx.appConfig.defaults.auth ?? {})) {
2333
+ const group = new Node2(ctx.base, "auth", id);
2334
+ let emailConfig;
2335
+ if (props.messaging) {
2336
+ const [_, domainName] = props.messaging.fromEmail.split("@");
2337
+ emailConfig = {
2338
+ type: "developer",
2339
+ replyTo: props.messaging.replyTo,
2340
+ sourceArn: ctx.shared.get(`mail-${domainName}-arn`),
2341
+ configurationSet: ctx.shared.get("mail-configuration-set"),
2342
+ from: props.messaging.fromName ? `${props.messaging.fromName} <${props.messaging.fromEmail}>` : props.messaging.fromEmail
2343
+ };
2417
2344
  }
2418
- new aws4.route53.RecordSet(group2, `MX`, {
2419
- hostedZoneId: hostedZone.id,
2420
- name: `mail.${props.domain}`,
2421
- type: "MX",
2422
- ttl: minutes3(5),
2423
- records: [`10 feedback-smtp.${ctx.appConfig.region}.amazonses.com`]
2424
- });
2425
- new aws4.route53.RecordSet(group2, `SPF`, {
2426
- hostedZoneId: hostedZone.id,
2427
- name: `mail.${props.domain}`,
2428
- type: "TXT",
2429
- ttl: minutes3(5),
2430
- records: ['"v=spf1 include:amazonses.com -all"']
2431
- });
2432
- new aws4.route53.RecordSet(group2, `DMARC`, {
2433
- hostedZoneId: hostedZone.id,
2434
- name: `_dmarc.${props.domain}`,
2435
- type: "TXT",
2436
- ttl: minutes3(5),
2437
- records: ['"v=DMARC1; p=none;"']
2345
+ const name = formatGlobalResourceName(ctx.appConfig.name, "auth", id);
2346
+ const userPool = new aws2.cognito.UserPool(group, "user-pool", {
2347
+ name,
2348
+ // deletionProtection: true,
2349
+ allowUserRegistration: props.allowUserRegistration,
2350
+ username: props.username,
2351
+ password: props.password,
2352
+ email: emailConfig
2438
2353
  });
2439
- const mailIdentityArn = emailIdentity.output(() => {
2440
- return `arn:aws:ses:${ctx.appConfig.region}:${ctx.accountId}:identity/${props.domain}`;
2354
+ const client = new aws2.cognito.UserPoolClient(group, "client", {
2355
+ userPoolId: userPool.id,
2356
+ name,
2357
+ validity: props.validity,
2358
+ supportedIdentityProviders: ["cognito"],
2359
+ authFlows: {
2360
+ userSrp: true
2361
+ }
2362
+ });
2363
+ ctx.bindEnv(`AUTH_${constantCase2(id)}_USER_POOL_ID`, userPool.id);
2364
+ ctx.bindEnv(`AUTH_${constantCase2(id)}_CLIENT_ID`, client.id);
2365
+ ctx.shared.set(`auth-${id}-user-pool-arn`, userPool.arn);
2366
+ ctx.shared.set(`auth-${id}-user-pool-id`, userPool.id);
2367
+ ctx.shared.set(`auth-${id}-client-id`, client.id);
2368
+ }
2369
+ }
2370
+ });
2371
+
2372
+ // src/feature/cache/index.ts
2373
+ import { constantCase as constantCase3 } from "change-case";
2374
+ import { Node as Node3, aws as aws3 } from "@awsless/formation";
2375
+ var typeGenCode = `
2376
+ import { Cluster, CommandOptions } from '@awsless/redis'
2377
+
2378
+ type Callback<T> = (redis: Cluster) => T
2379
+
2380
+ type Command = {
2381
+ readonly host: string
2382
+ readonly port: number
2383
+ <T>(callback: Callback<T>): T
2384
+ <T>(options:Omit<CommandOptions, 'cluster'>, callback: Callback<T>): T
2385
+ }`;
2386
+ var cacheFeature = defineFeature({
2387
+ name: "cache",
2388
+ async onTypeGen(ctx) {
2389
+ const gen = new TypeFile("@awsless/awsless");
2390
+ const resources = new TypeObject(1);
2391
+ for (const stack of ctx.stackConfigs) {
2392
+ const resource2 = new TypeObject(2);
2393
+ for (const name of Object.keys(stack.caches || {})) {
2394
+ resource2.addType(name, `Command`);
2395
+ }
2396
+ resources.addType(stack.name, resource2);
2397
+ }
2398
+ gen.addCode(typeGenCode);
2399
+ gen.addInterface("CacheResources", resources);
2400
+ await ctx.write("cache.d.ts", gen, true);
2401
+ },
2402
+ onStack(ctx) {
2403
+ for (const [id, props] of Object.entries(ctx.stackConfig.caches ?? {})) {
2404
+ const group = new Node3(ctx.stack, this.name, id);
2405
+ const name = formatLocalResourceName(ctx.appConfig.name, ctx.stack.name, this.name, id, "-");
2406
+ const subnetGroup = new aws3.memorydb.SubnetGroup(group, "subnets", {
2407
+ name,
2408
+ subnetIds: [
2409
+ //
2410
+ ctx.shared.get("vpc-private-subnet-id-1"),
2411
+ ctx.shared.get("vpc-private-subnet-id-2")
2412
+ ]
2413
+ });
2414
+ const securityGroup = new aws3.ec2.SecurityGroup(group, "security", {
2415
+ name,
2416
+ vpcId: ctx.shared.get(`vpc-id`),
2417
+ description: name
2418
+ });
2419
+ const port = aws3.ec2.Port.tcp(props.port);
2420
+ securityGroup.addIngressRule({ port, peer: aws3.ec2.Peer.anyIpv4() });
2421
+ securityGroup.addIngressRule({ port, peer: aws3.ec2.Peer.anyIpv6() });
2422
+ const cluster = new aws3.memorydb.Cluster(group, "cluster", {
2423
+ name,
2424
+ aclName: "open-access",
2425
+ securityGroupIds: [securityGroup.id],
2426
+ subnetGroupName: subnetGroup.name,
2427
+ ...props
2428
+ });
2429
+ ctx.onFunction(({ lambda }) => {
2430
+ const prefix = `CACHE_${constantCase3(ctx.stack.name)}_${constantCase3(id)}`;
2431
+ lambda.addEnvironment(`${prefix}_HOST`, cluster.address);
2432
+ lambda.addEnvironment(`${prefix}_PORT`, props.port.toString());
2441
2433
  });
2442
- ctx.shared.set(`mail-${id}-arn`, mailIdentityArn);
2443
- ctx.shared.set(`mail-${props.domain}-arn`, mailIdentityArn);
2444
- for (const record of props.dns ?? []) {
2445
- const name = record.name ?? props.domain;
2446
- new aws4.route53.RecordSet(group2, `${name}-${record.type}`, {
2447
- hostedZoneId: hostedZone.id,
2448
- name,
2449
- ...record
2450
- });
2451
- }
2452
2434
  }
2453
- ctx.onFunction(
2454
- ({ policy }) => policy.addStatement({
2455
- actions: ["ses:*"],
2456
- resources: [`arn:aws:ses:${ctx.appConfig.region}:${ctx.accountId}:identity/*`]
2457
- })
2458
- );
2459
2435
  }
2460
2436
  });
2461
2437
 
@@ -2601,8 +2577,145 @@ var configFeature = defineFeature({
2601
2577
  }
2602
2578
  });
2603
2579
 
2580
+ // src/feature/cron/index.ts
2581
+ import { Node as Node4, aws as aws4 } from "@awsless/formation";
2582
+ var cronFeature = defineFeature({
2583
+ name: "cron",
2584
+ onStack(ctx) {
2585
+ for (const [id, props] of Object.entries(ctx.stackConfig.crons ?? {})) {
2586
+ const group = new Node4(ctx.stack, "cron", id);
2587
+ const { lambda } = createAsyncLambdaFunction(group, ctx, "cron", id, props.consumer);
2588
+ const rule = new aws4.events.Rule(group, "rule", {
2589
+ name: formatLocalResourceName(ctx.app.name, ctx.stack.name, this.name, id),
2590
+ schedule: props.schedule,
2591
+ enabled: props.enabled,
2592
+ targets: [
2593
+ {
2594
+ id: "default",
2595
+ arn: lambda.arn,
2596
+ input: props.payload
2597
+ }
2598
+ ]
2599
+ });
2600
+ new aws4.lambda.Permission(group, "permission", {
2601
+ action: "lambda:InvokeFunction",
2602
+ principal: "events.amazonaws.com",
2603
+ functionArn: lambda.arn,
2604
+ sourceArn: rule.arn
2605
+ });
2606
+ }
2607
+ }
2608
+ });
2609
+
2610
+ // src/feature/domain/index.ts
2611
+ import { Node as Node5, aws as aws5 } from "@awsless/formation";
2612
+ import { minutes as minutes3 } from "@awsless/duration";
2613
+ var domainFeature = defineFeature({
2614
+ name: "domain",
2615
+ onApp(ctx) {
2616
+ const domains = Object.entries(ctx.appConfig.defaults.domains ?? {});
2617
+ if (domains.length === 0) {
2618
+ return;
2619
+ }
2620
+ const group = new Node5(ctx.base, "domain", "mail");
2621
+ const configurationSet = new aws5.ses.ConfigurationSet(group, "config", {
2622
+ name: ctx.app.name,
2623
+ engagementMetrics: true,
2624
+ reputationMetrics: true
2625
+ });
2626
+ ctx.shared.set(`mail-configuration-set`, configurationSet.name);
2627
+ for (const [id, props] of domains) {
2628
+ const group2 = new Node5(ctx.base, "domain", id);
2629
+ const hostedZone = new aws5.route53.HostedZone(group2, "zone", {
2630
+ name: props.domain
2631
+ });
2632
+ ctx.shared.set(`hosted-zone-${id}-id`, hostedZone.id);
2633
+ const certificate = new aws5.acm.Certificate(group2, "local", {
2634
+ domainName: props.domain,
2635
+ alternativeNames: [`*.${props.domain}`]
2636
+ });
2637
+ hostedZone.addRecord("local-cert-1", certificate.validationRecord(0));
2638
+ hostedZone.addRecord("local-cert-2", certificate.validationRecord(1));
2639
+ const validation = new aws5.acm.CertificateValidation(group2, "local", {
2640
+ certificateArn: certificate.arn
2641
+ });
2642
+ ctx.shared.set(`local-certificate-${id}-arn`, validation.arn);
2643
+ if (ctx.appConfig.region !== "us-east-1") {
2644
+ const globalCertificate = new aws5.acm.Certificate(group2, "global", {
2645
+ domainName: props.domain,
2646
+ alternativeNames: [`*.${props.domain}`],
2647
+ region: "us-east-1"
2648
+ });
2649
+ hostedZone.addRecord("global-cert-1", globalCertificate.validationRecord(0));
2650
+ hostedZone.addRecord("global-cert-2", globalCertificate.validationRecord(1));
2651
+ const globalValidation = new aws5.acm.CertificateValidation(group2, "global", {
2652
+ certificateArn: globalCertificate.arn,
2653
+ region: "us-east-1"
2654
+ });
2655
+ ctx.shared.set(`global-certificate-${id}-arn`, globalValidation.arn);
2656
+ } else {
2657
+ ctx.shared.set(`global-certificate-${id}-arn`, validation.arn);
2658
+ }
2659
+ const emailIdentity = new aws5.ses.EmailIdentity(group2, "mail", {
2660
+ emailIdentity: props.domain,
2661
+ mailFromDomain: `mail.${props.domain}`,
2662
+ configurationSetName: configurationSet.name,
2663
+ feedback: true,
2664
+ rejectOnMxFailure: true
2665
+ });
2666
+ let i = 0;
2667
+ for (const record of emailIdentity.dkimRecords) {
2668
+ new aws5.route53.RecordSet(group2, `dkim-${++i}`, {
2669
+ hostedZoneId: hostedZone.id,
2670
+ ...record
2671
+ });
2672
+ }
2673
+ new aws5.route53.RecordSet(group2, `MX`, {
2674
+ hostedZoneId: hostedZone.id,
2675
+ name: `mail.${props.domain}`,
2676
+ type: "MX",
2677
+ ttl: minutes3(5),
2678
+ records: [`10 feedback-smtp.${ctx.appConfig.region}.amazonses.com`]
2679
+ });
2680
+ new aws5.route53.RecordSet(group2, `SPF`, {
2681
+ hostedZoneId: hostedZone.id,
2682
+ name: `mail.${props.domain}`,
2683
+ type: "TXT",
2684
+ ttl: minutes3(5),
2685
+ records: ['"v=spf1 include:amazonses.com -all"']
2686
+ });
2687
+ new aws5.route53.RecordSet(group2, `DMARC`, {
2688
+ hostedZoneId: hostedZone.id,
2689
+ name: `_dmarc.${props.domain}`,
2690
+ type: "TXT",
2691
+ ttl: minutes3(5),
2692
+ records: ['"v=DMARC1; p=none;"']
2693
+ });
2694
+ const mailIdentityArn = emailIdentity.output(() => {
2695
+ return `arn:aws:ses:${ctx.appConfig.region}:${ctx.accountId}:identity/${props.domain}`;
2696
+ });
2697
+ ctx.shared.set(`mail-${id}-arn`, mailIdentityArn);
2698
+ ctx.shared.set(`mail-${props.domain}-arn`, mailIdentityArn);
2699
+ for (const record of props.dns ?? []) {
2700
+ const name = record.name ?? props.domain;
2701
+ new aws5.route53.RecordSet(group2, `${name}-${record.type}`, {
2702
+ hostedZoneId: hostedZone.id,
2703
+ name,
2704
+ ...record
2705
+ });
2706
+ }
2707
+ }
2708
+ ctx.onFunction(
2709
+ ({ policy }) => policy.addStatement({
2710
+ actions: ["ses:*"],
2711
+ resources: [`arn:aws:ses:${ctx.appConfig.region}:${ctx.accountId}:identity/*`]
2712
+ })
2713
+ );
2714
+ }
2715
+ });
2716
+
2604
2717
  // src/feature/function/index.ts
2605
- import { aws as aws5, Node as Node5 } from "@awsless/formation";
2718
+ import { aws as aws6, Node as Node6 } from "@awsless/formation";
2606
2719
  import { camelCase as camelCase3 } from "change-case";
2607
2720
  import { relative } from "path";
2608
2721
  var typeGenCode2 = `
@@ -2655,14 +2768,14 @@ var functionFeature = defineFeature({
2655
2768
  await ctx.write("function.d.ts", types2, true);
2656
2769
  },
2657
2770
  onApp(ctx) {
2658
- const group = new Node5(ctx.base, "function", "asset");
2659
- const bucket = new aws5.s3.Bucket(group, "bucket", {
2771
+ const group = new Node6(ctx.base, "function", "asset");
2772
+ const bucket = new aws6.s3.Bucket(group, "bucket", {
2660
2773
  name: formatGlobalResourceName(ctx.appConfig.name, "function", "assets"),
2661
2774
  versioning: true,
2662
2775
  forceDelete: true
2663
2776
  });
2664
2777
  ctx.shared.set("function-bucket-name", bucket.name);
2665
- const repository = new aws5.ecr.Repository(group, "repository", {
2778
+ const repository = new aws6.ecr.Repository(group, "repository", {
2666
2779
  name: formatGlobalResourceName(ctx.appConfig.name, "function", "repository", "-")
2667
2780
  });
2668
2781
  ctx.shared.set("function-repository-name", repository.name);
@@ -2670,19 +2783,19 @@ var functionFeature = defineFeature({
2670
2783
  },
2671
2784
  onStack(ctx) {
2672
2785
  for (const [id, props] of Object.entries(ctx.stackConfig.functions || {})) {
2673
- const group = new Node5(ctx.stack, "function", id);
2786
+ const group = new Node6(ctx.stack, "function", id);
2674
2787
  createLambdaFunction(group, ctx, "function", id, props);
2675
2788
  }
2676
2789
  }
2677
2790
  });
2678
2791
 
2679
2792
  // src/feature/graphql/index.ts
2680
- import { constantCase as constantCase3, paramCase as paramCase5 } from "change-case";
2793
+ import { constantCase as constantCase4, paramCase as paramCase5 } from "change-case";
2681
2794
  import { mergeTypeDefs } from "@graphql-tools/merge";
2682
2795
  import { generate } from "@awsless/graphql";
2683
2796
  import { buildSchema, print } from "graphql";
2684
2797
  import { readFile as readFile5 } from "fs/promises";
2685
- import { Asset as Asset2, Node as Node6, aws as aws6 } from "@awsless/formation";
2798
+ import { Asset as Asset2, Node as Node7, aws as aws7 } from "@awsless/formation";
2686
2799
 
2687
2800
  // src/feature/domain/util.ts
2688
2801
  var getDomainNameById = (config2, id) => {
@@ -2922,14 +3035,14 @@ var graphqlFeature = defineFeature({
2922
3035
  },
2923
3036
  onApp(ctx) {
2924
3037
  for (const [id, props] of Object.entries(ctx.appConfig.defaults.graphql ?? {})) {
2925
- const group = new Node6(ctx.base, "graphql", id);
3038
+ const group = new Node7(ctx.base, "graphql", id);
2926
3039
  let authorizer;
2927
3040
  if (typeof props.auth === "object") {
2928
3041
  const { lambda } = createLambdaFunction(group, ctx, "graphql-auth", id, props.auth.authorizer);
2929
3042
  authorizer = lambda;
2930
3043
  }
2931
3044
  const name = formatGlobalResourceName(ctx.app.name, "graphql", id);
2932
- const api = new aws6.appsync.GraphQLApi(group, "api", {
3045
+ const api = new aws7.appsync.GraphQLApi(group, "api", {
2933
3046
  name,
2934
3047
  type: "graphql",
2935
3048
  auth: {
@@ -2947,7 +3060,7 @@ var graphqlFeature = defineFeature({
2947
3060
  }
2948
3061
  });
2949
3062
  if (typeof props.auth === "object") {
2950
- new aws6.lambda.Permission(group, "authorizer", {
3063
+ new aws7.lambda.Permission(group, "authorizer", {
2951
3064
  functionArn: authorizer.arn,
2952
3065
  principal: "appsync.amazonaws.com",
2953
3066
  action: "lambda:InvokeFunction"
@@ -2976,7 +3089,7 @@ var graphqlFeature = defineFeature({
2976
3089
  };
2977
3090
  });
2978
3091
  });
2979
- new aws6.appsync.GraphQLSchema(group, "schema", {
3092
+ new aws7.appsync.GraphQLSchema(group, "schema", {
2980
3093
  apiId: api.id,
2981
3094
  definition: Asset2.fromFile(getBuildPath("graphql-schema", name, "schema.gql"))
2982
3095
  });
@@ -2998,16 +3111,16 @@ var graphqlFeature = defineFeature({
2998
3111
  }
2999
3112
  if (props.domain) {
3000
3113
  const domainName = formatFullDomainName(ctx.appConfig, props.domain, props.subDomain);
3001
- const domainGroup = new Node6(group, "domain", domainName);
3002
- const domain = new aws6.appsync.DomainName(domainGroup, "domain", {
3114
+ const domainGroup = new Node7(group, "domain", domainName);
3115
+ const domain = new aws7.appsync.DomainName(domainGroup, "domain", {
3003
3116
  domainName,
3004
3117
  certificateArn: ctx.shared.get(`global-certificate-${props.domain}-arn`)
3005
3118
  });
3006
- new aws6.appsync.DomainNameApiAssociation(domainGroup, "association", {
3119
+ new aws7.appsync.DomainNameApiAssociation(domainGroup, "association", {
3007
3120
  apiId: api.id,
3008
3121
  domainName: domain.domainName
3009
3122
  });
3010
- new aws6.route53.RecordSet(domainGroup, "record", {
3123
+ new aws7.route53.RecordSet(domainGroup, "record", {
3011
3124
  hostedZoneId: ctx.shared.get(`hosted-zone-${props.domain}-id`),
3012
3125
  type: "A",
3013
3126
  name: domainName,
@@ -3017,7 +3130,7 @@ var graphqlFeature = defineFeature({
3017
3130
  evaluateTargetHealth: false
3018
3131
  }
3019
3132
  });
3020
- ctx.bindEnv(`GRAPHQL_${constantCase3(id)}_ENDPOINT`, domainName);
3133
+ ctx.bindEnv(`GRAPHQL_${constantCase4(id)}_ENDPOINT`, domainName);
3021
3134
  }
3022
3135
  }
3023
3136
  },
@@ -3030,18 +3143,18 @@ var graphqlFeature = defineFeature({
3030
3143
  `GraphQL definition is not defined on app level for "${id}"`
3031
3144
  );
3032
3145
  }
3033
- const group = new Node6(ctx.stack, "graphql", id);
3146
+ const group = new Node7(ctx.stack, "graphql", id);
3034
3147
  const apiId = ctx.shared.get(`graphql-${id}-id`);
3035
3148
  for (const [typeName, fields] of Object.entries(props.resolvers ?? {})) {
3036
3149
  for (const [fieldName, props2] of Object.entries(fields ?? {})) {
3037
3150
  const name = `${typeName}__${fieldName}`;
3038
- const resolverGroup = new Node6(group, "resolver", name);
3151
+ const resolverGroup = new Node7(group, "resolver", name);
3039
3152
  const entryId = paramCase5(`${id}-${shortId(`${typeName}-${fieldName}`)}`);
3040
3153
  const { lambda } = createLambdaFunction(resolverGroup, ctx, `graphql`, entryId, {
3041
3154
  ...props2.consumer,
3042
3155
  description: `${id} ${typeName}.${fieldName}`
3043
3156
  });
3044
- const role = new aws6.iam.Role(resolverGroup, "source-role", {
3157
+ const role = new aws7.iam.Role(resolverGroup, "source-role", {
3045
3158
  assumedBy: "appsync.amazonaws.com",
3046
3159
  policies: [
3047
3160
  {
@@ -3055,7 +3168,7 @@ var graphqlFeature = defineFeature({
3055
3168
  }
3056
3169
  ]
3057
3170
  });
3058
- const source = new aws6.appsync.DataSource(resolverGroup, "source", {
3171
+ const source = new aws7.appsync.DataSource(resolverGroup, "source", {
3059
3172
  apiId,
3060
3173
  name,
3061
3174
  role: role.arn,
@@ -3068,13 +3181,13 @@ var graphqlFeature = defineFeature({
3068
3181
  } else if (defaultProps.resolver) {
3069
3182
  code = Asset2.fromFile(getBuildPath("graphql-resolver", id, "resolver.js"));
3070
3183
  }
3071
- const config2 = new aws6.appsync.FunctionConfiguration(resolverGroup, "config", {
3184
+ const config2 = new aws7.appsync.FunctionConfiguration(resolverGroup, "config", {
3072
3185
  apiId,
3073
3186
  name,
3074
3187
  code,
3075
3188
  dataSourceName: source.name
3076
3189
  });
3077
- new aws6.appsync.Resolver(resolverGroup, "resolver", {
3190
+ new aws7.appsync.Resolver(resolverGroup, "resolver", {
3078
3191
  apiId,
3079
3192
  typeName,
3080
3193
  fieldName,
@@ -3087,32 +3200,200 @@ var graphqlFeature = defineFeature({
3087
3200
  }
3088
3201
  });
3089
3202
 
3090
- // src/feature/on-failure/index.ts
3091
- import { Node as Node7, aws as aws7 } from "@awsless/formation";
3092
- var onFailureFeature = defineFeature({
3093
- name: "on-failure",
3094
- onApp(ctx) {
3095
- if (!hasOnFailure(ctx.stackConfigs)) {
3096
- return;
3203
+ // src/feature/http/index.ts
3204
+ import { Node as Node8, aws as aws8 } from "@awsless/formation";
3205
+ import { camelCase as camelCase4, constantCase as constantCase5 } from "change-case";
3206
+ import { relative as relative2 } from "path";
3207
+ var parseRoute = (route) => {
3208
+ const [method, ...paths] = route.split(" ");
3209
+ const path = paths.join(" ");
3210
+ return { method, path };
3211
+ };
3212
+ var strToInt = (str) => {
3213
+ return parseInt(Buffer.from(str, "utf8").toString("hex"), 16);
3214
+ };
3215
+ var generatePriority = (stackName, route) => {
3216
+ const start = strToInt(stackName) % 500 + 1;
3217
+ const end = strToInt(route) % 100;
3218
+ const priority = start + "" + end;
3219
+ return parseInt(priority, 10);
3220
+ };
3221
+ var httpFeature = defineFeature({
3222
+ name: "http",
3223
+ async onTypeGen(ctx) {
3224
+ const types2 = new TypeFile("@awsless/awsless");
3225
+ const resources = new TypeObject(1);
3226
+ const api = {};
3227
+ for (const stack of ctx.stackConfigs) {
3228
+ for (const [id, routes] of Object.entries(stack.http ?? {})) {
3229
+ if (!(id in api))
3230
+ api[id] = {};
3231
+ for (const [route, props] of Object.entries(routes)) {
3232
+ const { path, method } = parseRoute(route);
3233
+ const file = typeof props === "string" ? props : props.file;
3234
+ if (!(method in api[id])) {
3235
+ api[id][method] = {};
3236
+ }
3237
+ api[id][method][path] = file;
3238
+ }
3239
+ }
3097
3240
  }
3098
- const count = ctx.stackConfigs.filter((s) => s.onFailure).length;
3099
- if (count > 1) {
3100
- throw new TypeError("Only 1 onFailure configuration is allowed in your app.");
3241
+ for (const [id, routes] of Object.entries(api)) {
3242
+ const idType = new TypeObject(2);
3243
+ for (const [method, paths] of Object.entries(routes)) {
3244
+ const methodType = new TypeObject(3);
3245
+ for (const [path, file] of Object.entries(paths)) {
3246
+ const paramType = new TypeObject(4);
3247
+ for (const param of path.matchAll(/{([a-z0-9]+)}/g)) {
3248
+ paramType.addType(param[0], "string | number");
3249
+ }
3250
+ const varName = camelCase4(`${id}-${path}-${method}`);
3251
+ const relFile = relative2(directories.types, file);
3252
+ types2.addImport(varName, relFile);
3253
+ methodType.add(`'${path}'`, `Route<typeof ${varName}, ${paramType.toString() || "never"}>`);
3254
+ }
3255
+ idType.addConst(method, methodType);
3256
+ }
3257
+ resources.addType(id, idType);
3101
3258
  }
3102
- const queue2 = new aws7.sqs.Queue(ctx.base, "on-failure", {
3103
- name: formatGlobalResourceName(ctx.appConfig.name, "on-failure", "failure")
3104
- });
3105
- ctx.shared.set("on-failure-queue-arn", queue2.arn);
3259
+ const code = [
3260
+ `import { InvokeResponse } from '@awsless/lambda'`,
3261
+ `type Function = (...args: any) => any`,
3262
+ `type Event<F extends Function> = Parameters<F>[0]`,
3263
+ `type RequestWithQuery = { request: { queryStringParameters: any } }`,
3264
+ `type RequestWithBody = { request: { body: any } }`,
3265
+ `type ResponseWithBody = { statusCode: number, body: any }`,
3266
+ `type Query<F extends Function> = Event<F> extends RequestWithQuery ? Event<F>['request']['queryStringParameters'] : never`,
3267
+ `type Body<F extends Function> = Event<F> extends RequestWithBody ? Exclude<Event<F>['request']['body'], string> : never`,
3268
+ `type Response<F extends Function> = Awaited<InvokeResponse<F>> extends ResponseWithBody ? Promise<Awaited<InvokeResponse<F>>['body']> : Promise<never>`,
3269
+ `type Route<F extends Function, P> = { param: P; query: Query<F>; body: Body<F>; response: Response<F> }`
3270
+ ];
3271
+ code.map((code2) => types2.addCode(code2));
3272
+ types2.addInterface("HTTP", resources);
3273
+ await ctx.write("http.d.ts", types2, true);
3106
3274
  },
3107
- onStack(ctx) {
3108
- const onFailure = ctx.stackConfig.onFailure;
3275
+ onApp(ctx) {
3276
+ if (Object.keys(ctx.appConfig.defaults?.http ?? {}).length === 0) {
3277
+ return;
3278
+ }
3279
+ const group = new Node8(ctx.base, "http", "main");
3280
+ const securityGroup = new aws8.ec2.SecurityGroup(group, "http", {
3281
+ vpcId: ctx.shared.get(`vpc-id`),
3282
+ name: formatGlobalResourceName(ctx.app.name, "http", "http"),
3283
+ description: `Global security group for HTTP api.`
3284
+ });
3285
+ const port = aws8.ec2.Port.tcp(443);
3286
+ securityGroup.addIngressRule({ port, peer: aws8.ec2.Peer.anyIpv4() });
3287
+ securityGroup.addIngressRule({ port, peer: aws8.ec2.Peer.anyIpv6() });
3288
+ for (const [id, props] of Object.entries(ctx.appConfig.defaults?.http ?? {})) {
3289
+ const group2 = new Node8(ctx.base, "http", id);
3290
+ const loadBalancer = new aws8.elb.LoadBalancer(group2, "balancer", {
3291
+ name: formatGlobalResourceName(ctx.app.name, "http", id),
3292
+ type: "application",
3293
+ securityGroups: [securityGroup.id],
3294
+ subnets: [
3295
+ //
3296
+ ctx.shared.get(`vpc-public-subnet-id-1`),
3297
+ ctx.shared.get(`vpc-public-subnet-id-2`)
3298
+ ]
3299
+ });
3300
+ const listener = new aws8.elb.Listener(group2, "listener", {
3301
+ loadBalancerArn: loadBalancer.arn,
3302
+ port: 443,
3303
+ protocol: "https",
3304
+ certificates: [ctx.shared.get(`local-certificate-${props.domain}-arn`)],
3305
+ defaultActions: [
3306
+ aws8.elb.ListenerAction.fixedResponse({
3307
+ statusCode: 404,
3308
+ contentType: "application/json",
3309
+ messageBody: JSON.stringify({
3310
+ message: "Route not found"
3311
+ })
3312
+ })
3313
+ ]
3314
+ });
3315
+ ctx.shared.set(`http-${id}-listener-arn`, listener.arn);
3316
+ const domainName = formatFullDomainName(ctx.appConfig, props.domain, props.subDomain);
3317
+ new aws8.route53.RecordSet(group2, domainName, {
3318
+ hostedZoneId: ctx.shared.get(`hosted-zone-${props.domain}-id`),
3319
+ name: domainName,
3320
+ type: "A",
3321
+ alias: {
3322
+ evaluateTargetHealth: false,
3323
+ hostedZoneId: loadBalancer.hostedZoneId,
3324
+ dnsName: loadBalancer.dnsName
3325
+ }
3326
+ });
3327
+ ctx.bindEnv(`HTTP_${constantCase5(id)}_ENDPOINT`, domainName);
3328
+ }
3329
+ },
3330
+ onStack(ctx) {
3331
+ for (const [id, routes] of Object.entries(ctx.stackConfig.http ?? {})) {
3332
+ const props = ctx.appConfig.defaults.http?.[id];
3333
+ if (!props) {
3334
+ throw new Error(`Http definition is not defined on app level for "${id}"`);
3335
+ }
3336
+ const group = new Node8(ctx.stack, "http", id);
3337
+ for (const [routeKey, routeProps] of Object.entries(routes)) {
3338
+ const routeGroup = new Node8(group, "route", routeKey);
3339
+ const { method, path } = parseRoute(routeKey);
3340
+ const routeId = shortId(routeKey);
3341
+ const { lambda } = createLambdaFunction(routeGroup, ctx, "http", `${id}-${routeId}`, {
3342
+ ...routeProps,
3343
+ description: routeKey
3344
+ });
3345
+ const name = formatLocalResourceName(ctx.app.name, ctx.stack.name, "http", routeId);
3346
+ const permission = new aws8.lambda.Permission(routeGroup, id, {
3347
+ action: "lambda:InvokeFunction",
3348
+ principal: "elasticloadbalancing.amazonaws.com",
3349
+ functionArn: lambda.arn
3350
+ // sourceArn: `arn:aws:elasticloadbalancing:${ctx.appConfig.region}:*:targetgroup/${name}/*`,
3351
+ });
3352
+ const target = new aws8.elb.TargetGroup(routeGroup, id, {
3353
+ name,
3354
+ type: "lambda",
3355
+ targets: [lambda.arn]
3356
+ }).dependsOn(permission);
3357
+ new aws8.elb.ListenerRule(routeGroup, id, {
3358
+ listenerArn: ctx.shared.get(`http-${id}-listener-arn`),
3359
+ priority: generatePriority(ctx.stackConfig.name, routeKey),
3360
+ conditions: [
3361
+ aws8.elb.ListenerCondition.httpRequestMethods([method]),
3362
+ aws8.elb.ListenerCondition.pathPatterns([path])
3363
+ ],
3364
+ actions: [aws8.elb.ListenerAction.forward([target.arn])]
3365
+ }).dependsOn(target);
3366
+ }
3367
+ }
3368
+ }
3369
+ });
3370
+
3371
+ // src/feature/on-failure/index.ts
3372
+ import { Node as Node9, aws as aws9 } from "@awsless/formation";
3373
+ var onFailureFeature = defineFeature({
3374
+ name: "on-failure",
3375
+ onApp(ctx) {
3376
+ if (!hasOnFailure(ctx.stackConfigs)) {
3377
+ return;
3378
+ }
3379
+ const count = ctx.stackConfigs.filter((s) => s.onFailure).length;
3380
+ if (count > 1) {
3381
+ throw new TypeError("Only 1 onFailure configuration is allowed in your app.");
3382
+ }
3383
+ const queue2 = new aws9.sqs.Queue(ctx.base, "on-failure", {
3384
+ name: formatGlobalResourceName(ctx.appConfig.name, "on-failure", "failure")
3385
+ });
3386
+ ctx.shared.set("on-failure-queue-arn", queue2.arn);
3387
+ },
3388
+ onStack(ctx) {
3389
+ const onFailure = ctx.stackConfig.onFailure;
3109
3390
  if (!onFailure) {
3110
3391
  return;
3111
3392
  }
3112
3393
  const queueArn = ctx.shared.get("on-failure-queue-arn");
3113
- const group = new Node7(ctx.stack, "on-failure", "failure");
3394
+ const group = new Node9(ctx.stack, "on-failure", "failure");
3114
3395
  const { lambda, policy } = createLambdaFunction(group, ctx, "on-failure", "failure", onFailure);
3115
- const source = new aws7.lambda.EventSourceMapping(group, "on-failure", {
3396
+ const source = new aws9.lambda.EventSourceMapping(group, "on-failure", {
3116
3397
  functionArn: lambda.arn,
3117
3398
  sourceArn: queueArn,
3118
3399
  batchSize: 10
@@ -3132,7 +3413,7 @@ var onFailureFeature = defineFeature({
3132
3413
  });
3133
3414
 
3134
3415
  // src/feature/pubsub/index.ts
3135
- import { Node as Node8, aws as aws8 } from "@awsless/formation";
3416
+ import { Node as Node10, aws as aws10 } from "@awsless/formation";
3136
3417
  var pubsubFeature = defineFeature({
3137
3418
  name: "pubsub",
3138
3419
  onApp(ctx) {
@@ -3145,16 +3426,16 @@ var pubsubFeature = defineFeature({
3145
3426
  },
3146
3427
  onStack(ctx) {
3147
3428
  for (const [id, props] of Object.entries(ctx.stackConfig.pubsub ?? {})) {
3148
- const group = new Node8(ctx.stack, "pubsub", id);
3429
+ const group = new Node10(ctx.stack, "pubsub", id);
3149
3430
  const { lambda } = createAsyncLambdaFunction(group, ctx, `pubsub`, id, props.consumer);
3150
3431
  const name = formatLocalResourceName(ctx.app.name, ctx.stack.name, "pubsub", id);
3151
- const topic = new aws8.iot.TopicRule(group, "rule", {
3432
+ const topic = new aws10.iot.TopicRule(group, "rule", {
3152
3433
  name: name.replaceAll("-", "_"),
3153
3434
  sql: props.sql,
3154
3435
  sqlVersion: props.sqlVersion,
3155
3436
  actions: [{ lambda: { functionArn: lambda.arn } }]
3156
3437
  });
3157
- new aws8.lambda.Permission(group, "permission", {
3438
+ new aws10.lambda.Permission(group, "permission", {
3158
3439
  action: "lambda:InvokeFunction",
3159
3440
  principal: "iot.amazonaws.com",
3160
3441
  functionArn: lambda.arn,
@@ -3165,10 +3446,10 @@ var pubsubFeature = defineFeature({
3165
3446
  });
3166
3447
 
3167
3448
  // src/feature/queue/index.ts
3168
- import { camelCase as camelCase4, constantCase as constantCase4 } from "change-case";
3169
- import { relative as relative2 } from "path";
3449
+ import { camelCase as camelCase5, constantCase as constantCase6 } from "change-case";
3450
+ import { relative as relative3 } from "path";
3170
3451
  import deepmerge2 from "deepmerge";
3171
- import { Node as Node9, aws as aws9 } from "@awsless/formation";
3452
+ import { Node as Node11, aws as aws11 } from "@awsless/formation";
3172
3453
  var typeGenCode3 = `
3173
3454
  import { SendMessageOptions, SendMessageBatchOptions, BatchItem } from '@awsless/sqs'
3174
3455
  import type { Mock } from 'vitest'
@@ -3198,10 +3479,10 @@ var queueFeature = defineFeature({
3198
3479
  const mock = new TypeObject(2);
3199
3480
  const mockResponse = new TypeObject(2);
3200
3481
  for (const [name, fileOrProps] of Object.entries(stack.queues || {})) {
3201
- const varName = camelCase4(`${stack.name}-${name}`);
3482
+ const varName = camelCase5(`${stack.name}-${name}`);
3202
3483
  const queueName = formatLocalResourceName(ctx.appConfig.name, stack.name, "queue", name);
3203
3484
  const file = typeof fileOrProps === "string" ? fileOrProps : typeof fileOrProps.consumer === "string" ? fileOrProps.consumer : fileOrProps.consumer.file;
3204
- const relFile = relative2(directories.types, file);
3485
+ const relFile = relative3(directories.types, file);
3205
3486
  gen.addImport(varName, relFile);
3206
3487
  mock.addType(name, `MockBuilder<typeof ${varName}>`);
3207
3488
  resource2.addType(name, `Send<'${queueName}', typeof ${varName}>`);
@@ -3220,15 +3501,15 @@ var queueFeature = defineFeature({
3220
3501
  onStack(ctx) {
3221
3502
  for (const [id, local2] of Object.entries(ctx.stackConfig.queues || {})) {
3222
3503
  const props = deepmerge2(ctx.appConfig.defaults.queue, local2);
3223
- const group = new Node9(ctx.stack, "queue", id);
3224
- const queue2 = new aws9.sqs.Queue(group, "queue", {
3504
+ const group = new Node11(ctx.stack, "queue", id);
3505
+ const queue2 = new aws11.sqs.Queue(group, "queue", {
3225
3506
  name: formatLocalResourceName(ctx.appConfig.name, ctx.stack.name, "queue", id),
3226
3507
  deadLetterArn: getGlobalOnFailure(ctx),
3227
3508
  ...props
3228
3509
  });
3229
3510
  const { lambda, policy } = createLambdaFunction(group, ctx, `queue`, id, props.consumer);
3230
3511
  lambda.addEnvironment("LOG_VIEWABLE_ERROR", "1");
3231
- new aws9.lambda.EventSourceMapping(group, "event", {
3512
+ new aws11.lambda.EventSourceMapping(group, "event", {
3232
3513
  functionArn: lambda.arn,
3233
3514
  sourceArn: queue2.arn,
3234
3515
  batchSize: props.batchSize,
@@ -3241,612 +3522,129 @@ var queueFeature = defineFeature({
3241
3522
  });
3242
3523
  ctx.onFunction(({ lambda: lambda2, policy: policy2 }) => {
3243
3524
  policy2.addStatement(queue2.permissions);
3244
- lambda2.addEnvironment(`QUEUE_${constantCase4(ctx.stack.name)}_${constantCase4(id)}_URL`, queue2.url);
3525
+ lambda2.addEnvironment(`QUEUE_${constantCase6(ctx.stack.name)}_${constantCase6(id)}_URL`, queue2.url);
3245
3526
  });
3246
3527
  }
3247
3528
  }
3248
3529
  });
3249
3530
 
3250
- // src/feature/store/index.ts
3251
- import { aws as aws10, Node as Node10 } from "@awsless/formation";
3252
- var typeGenCode4 = `
3253
- import { Body, PutObjectProps, BodyStream, createPresignedPost } from '@awsless/s3'
3254
- import { Size } from '@awsless/size'
3255
- import { Duration } from '@awsless/duration'
3256
- import { PresignedPost } from '@aws-sdk/s3-presigned-post'
3257
-
3258
- type Store<Name extends string> = {
3259
- readonly name: Name
3260
- readonly put: (key: string, body: Body, options?: Pick<PutObjectProps, 'metadata' | 'storageClass'>) => Promise<void>
3261
- readonly get: (key: string) => Promise<BodyStream | undefined>
3262
- readonly delete: (key: string) => Promise<void>
3263
- readonly createPresignedPost: (key: string, contentLengthRange: [Size, Size], expires?: Duration, fields?: Record<string, string>) => Promise<PresignedPost>
3264
- }
3265
- `;
3266
- var storeFeature = defineFeature({
3267
- name: "store",
3268
- async onTypeGen(ctx) {
3269
- const gen = new TypeFile("@awsless/awsless");
3270
- const resources = new TypeObject(1);
3271
- for (const stack of ctx.stackConfigs) {
3272
- const list4 = new TypeObject(2);
3273
- for (const id of Object.keys(stack.stores ?? {})) {
3274
- const storeName = formatLocalResourceName(ctx.appConfig.name, stack.name, "store", id);
3275
- list4.addType(id, `Store<'${storeName}'>`);
3531
+ // src/feature/rest/index.ts
3532
+ import { Node as Node12, aws as aws12 } from "@awsless/formation";
3533
+ import { constantCase as constantCase7 } from "change-case";
3534
+ var restFeature = defineFeature({
3535
+ name: "rest",
3536
+ onApp(ctx) {
3537
+ for (const [id, props] of Object.entries(ctx.appConfig.defaults?.rest ?? {})) {
3538
+ const group = new Node12(ctx.base, "rest", id);
3539
+ const api = new aws12.apiGatewayV2.Api(group, "api", {
3540
+ name: formatGlobalResourceName(ctx.app.name, "rest", id),
3541
+ protocolType: "HTTP"
3542
+ });
3543
+ const stage = new aws12.apiGatewayV2.Stage(group, "stage", {
3544
+ name: "v1",
3545
+ apiId: api.id
3546
+ });
3547
+ ctx.shared.set(`rest-${id}-id`, api.id);
3548
+ if (props.domain) {
3549
+ const domainName = formatFullDomainName(ctx.appConfig, props.domain, props.subDomain);
3550
+ const hostedZoneId = ctx.shared.get(`hosted-zone-${props.domain}-id`);
3551
+ const certificateArn = ctx.shared.get(`certificate-${props.domain}-arn`);
3552
+ const domain = new aws12.apiGatewayV2.DomainName(group, "domain", {
3553
+ name: domainName,
3554
+ certificates: [
3555
+ {
3556
+ certificateArn
3557
+ }
3558
+ ]
3559
+ });
3560
+ const mapping = new aws12.apiGatewayV2.ApiMapping(group, "mapping", {
3561
+ apiId: api.id,
3562
+ domainName: domain.name,
3563
+ stage: stage.name
3564
+ });
3565
+ const record = new aws12.route53.RecordSet(group, "record", {
3566
+ hostedZoneId,
3567
+ type: "A",
3568
+ name: domainName,
3569
+ alias: {
3570
+ dnsName: domain.regionalDomainName,
3571
+ hostedZoneId: domain.regionalHostedZoneId,
3572
+ evaluateTargetHealth: false
3573
+ }
3574
+ });
3575
+ record.dependsOn(domain, mapping);
3576
+ ctx.bindEnv(`REST_${constantCase7(id)}_ENDPOINT`, domainName);
3276
3577
  }
3277
- resources.addType(stack.name, list4);
3278
3578
  }
3279
- gen.addCode(typeGenCode4);
3280
- gen.addInterface("StoreResources", resources);
3281
- await ctx.write("store.d.ts", gen, true);
3282
3579
  },
3283
3580
  onStack(ctx) {
3284
- for (const [id, props] of Object.entries(ctx.stackConfig.stores ?? {})) {
3285
- const group = new Node10(ctx.stack, "store", id);
3286
- const bucketName = formatLocalResourceName(ctx.appConfig.name, ctx.stack.name, "store", id);
3287
- const lambdaConfigs = [];
3288
- const eventMap = {
3289
- "created:*": "s3:ObjectCreated:*",
3290
- "created:put": "s3:ObjectCreated:Put",
3291
- "created:post": "s3:ObjectCreated:Post",
3292
- "created:copy": "s3:ObjectCreated:Copy",
3293
- "created:upload": "s3:ObjectCreated:CompleteMultipartUpload",
3294
- "removed:*": "s3:ObjectRemoved:*",
3295
- "removed:delete": "s3:ObjectRemoved:Delete",
3296
- "removed:marker": "s3:ObjectRemoved:DeleteMarkerCreated"
3297
- };
3298
- for (const [event, funcProps] of Object.entries(props.events ?? {})) {
3299
- const eventGroup = new Node10(group, "event", event);
3300
- const { lambda } = createAsyncLambdaFunction(eventGroup, ctx, `store`, id, funcProps);
3301
- new aws10.lambda.Permission(eventGroup, "permission", {
3581
+ for (const [id, routes] of Object.entries(ctx.stackConfig.rest ?? {})) {
3582
+ const restGroup = new Node12(ctx.stack, "rest", id);
3583
+ for (const [routeKey, props] of Object.entries(routes)) {
3584
+ const group = new Node12(restGroup, "route", routeKey);
3585
+ const apiId = ctx.shared.get(`rest-${id}-id`);
3586
+ const routeId = shortId(routeKey);
3587
+ const { lambda } = createLambdaFunction(group, ctx, "rest", `${id}-${routeId}`, {
3588
+ ...props,
3589
+ description: `${id} ${routeKey}`
3590
+ });
3591
+ const permission = new aws12.lambda.Permission(group, "permission", {
3302
3592
  action: "lambda:InvokeFunction",
3303
- principal: "s3.amazonaws.com",
3304
- functionArn: lambda.arn,
3305
- sourceArn: `arn:aws:s3:::${bucketName}`
3593
+ principal: "apigateway.amazonaws.com",
3594
+ functionArn: lambda.arn
3306
3595
  });
3307
- lambdaConfigs.push({
3308
- event: eventMap[event],
3309
- function: lambda.arn
3596
+ const integration = new aws12.apiGatewayV2.Integration(group, "integration", {
3597
+ apiId,
3598
+ description: `${id} ${routeKey}`,
3599
+ method: "POST",
3600
+ payloadFormatVersion: "2.0",
3601
+ type: "AWS_PROXY",
3602
+ uri: lambda.arn.apply((arn) => {
3603
+ return `arn:aws:apigateway:${ctx.appConfig.region}:lambda:path/2015-03-31/functions/${arn}/invocations`;
3604
+ })
3310
3605
  });
3606
+ const route = new aws12.apiGatewayV2.Route(group, "route", {
3607
+ apiId,
3608
+ routeKey,
3609
+ target: integration.id.apply((id2) => `integrations/${id2}`)
3610
+ });
3611
+ route.dependsOn(lambda, permission);
3311
3612
  }
3312
- const bucket = new aws10.s3.Bucket(group, "store", {
3313
- name: bucketName,
3314
- versioning: props.versioning,
3315
- lambdaConfigs,
3316
- cors: [
3317
- // ---------------------------------------------
3318
- // Support for presigned post requests
3319
- // ---------------------------------------------
3320
- {
3321
- origins: ["*"],
3322
- methods: ["POST"]
3323
- }
3324
- // ---------------------------------------------
3325
- ]
3326
- });
3327
- ctx.onFunction(({ policy }) => {
3328
- policy.addStatement(bucket.permissions);
3329
- });
3330
3613
  }
3331
3614
  }
3332
3615
  });
3333
3616
 
3334
- // src/feature/table/index.ts
3335
- import { Node as Node11, aws as aws11 } from "@awsless/formation";
3336
- var tableFeature = defineFeature({
3337
- name: "table",
3617
+ // src/feature/search/index.ts
3618
+ import { Node as Node13, aws as aws13 } from "@awsless/formation";
3619
+ import { constantCase as constantCase8 } from "change-case";
3620
+ var typeGenCode4 = `
3621
+ import { AnyStruct, Table } from '@awsless/open-search'
3622
+
3623
+ type Search = {
3624
+ readonly domain: string
3625
+ readonly defineTable: <N extends stringm S extends AnyStruct>(tableName: N, schema: S) => Table<N, S>
3626
+ }
3627
+ `;
3628
+ var searchFeature = defineFeature({
3629
+ name: "search",
3338
3630
  async onTypeGen(ctx) {
3339
3631
  const gen = new TypeFile("@awsless/awsless");
3340
3632
  const resources = new TypeObject(1);
3341
3633
  for (const stack of ctx.stackConfigs) {
3342
3634
  const list4 = new TypeObject(2);
3343
- for (const name of Object.keys(stack.tables || {})) {
3344
- const tableName = formatLocalResourceName(ctx.appConfig.name, stack.name, "table", name);
3345
- list4.addType(name, `'${tableName}'`);
3635
+ for (const id of Object.keys(stack.searchs ?? {})) {
3636
+ list4.addType(id, `Search`);
3346
3637
  }
3347
3638
  resources.addType(stack.name, list4);
3348
3639
  }
3349
- gen.addInterface("TableResources", resources);
3350
- await ctx.write("table.d.ts", gen, true);
3351
- },
3352
- onStack(ctx) {
3353
- for (const [id, props] of Object.entries(ctx.stackConfig.tables ?? {})) {
3354
- const group = new Node11(ctx.stack, "table", id);
3355
- const table2 = new aws11.dynamodb.Table(group, "table", {
3356
- ...props,
3357
- name: formatLocalResourceName(ctx.appConfig.name, ctx.stackConfig.name, "table", id),
3358
- stream: props.stream?.type
3359
- });
3360
- if (props.stream) {
3361
- const { lambda, policy } = createLambdaFunction(group, ctx, "table", id, props.stream.consumer);
3362
- lambda.addEnvironment("LOG_VIEWABLE_ERROR", "1");
3363
- const onFailure = getGlobalOnFailure(ctx);
3364
- const source = new aws11.lambda.EventSourceMapping(group, id, {
3365
- functionArn: lambda.arn,
3366
- sourceArn: table2.streamArn,
3367
- batchSize: 100,
3368
- bisectBatchOnError: true,
3369
- // retryAttempts: props.stream.consumer.retryAttempts ?? -1,
3370
- parallelizationFactor: 1,
3371
- startingPosition: "latest",
3372
- onFailure
3373
- });
3374
- policy.addStatement(table2.streamPermissions);
3375
- source.dependsOn(policy);
3376
- if (onFailure) {
3377
- policy.addStatement({
3378
- actions: ["sqs:SendMessage", "sqs:GetQueueUrl"],
3379
- resources: [onFailure]
3380
- });
3381
- }
3382
- }
3383
- ctx.onFunction(({ policy }) => {
3384
- policy.addStatement(...table2.permissions);
3385
- });
3386
- }
3387
- }
3388
- });
3389
-
3390
- // src/feature/test/index.ts
3391
- var testFeature = defineFeature({
3392
- name: "test",
3393
- onStack(ctx) {
3394
- if (ctx.stackConfig.tests) {
3395
- ctx.registerTest(ctx.stackConfig.name, ctx.stackConfig.tests);
3396
- }
3397
- }
3398
- });
3399
-
3400
- // src/feature/topic/index.ts
3401
- import { Node as Node12, aws as aws12 } from "@awsless/formation";
3402
- var typeGenCode5 = `
3403
- import type { PublishOptions } from '@awsless/sns'
3404
- import type { Mock } from 'vitest'
3405
-
3406
- type Publish<Name extends string> = {
3407
- readonly name: Name
3408
- (payload: unknown, options?: Omit<PublishOptions, 'topic' | 'payload'>): Promise<void>
3409
- }
3410
-
3411
- type MockHandle = (payload: unknown) => void
3412
- type MockBuilder = (handle?: MockHandle) => void
3413
- `;
3414
- var topicFeature = defineFeature({
3415
- name: "topic",
3416
- async onTypeGen(ctx) {
3417
- const gen = new TypeFile("@awsless/awsless");
3418
- const resources = new TypeObject(1);
3419
- const mocks = new TypeObject(1);
3420
- const mockResponses = new TypeObject(1);
3421
- for (const stack of ctx.stackConfigs) {
3422
- for (const topic of stack.topics || []) {
3423
- const name = formatGlobalResourceName(ctx.appConfig.name, "topic", topic);
3424
- mockResponses.addType(topic, "Mock");
3425
- resources.addType(topic, `Publish<'${name}'>`);
3426
- mocks.addType(topic, `MockBuilder`);
3427
- }
3428
- }
3429
- gen.addCode(typeGenCode5);
3430
- gen.addInterface("TopicResources", resources);
3431
- gen.addInterface("TopicMock", mocks);
3432
- gen.addInterface("TopicMockResponse", mockResponses);
3433
- await ctx.write("topic.d.ts", gen, true);
3434
- },
3435
- onApp(ctx) {
3436
- for (const stack of ctx.stackConfigs) {
3437
- for (const id of stack.topics ?? []) {
3438
- const group = new Node12(ctx.base, "topic", id);
3439
- const topic = new aws12.sns.Topic(group, "topic", {
3440
- name: formatGlobalResourceName(ctx.appConfig.name, "topic", id)
3441
- });
3442
- ctx.shared.set(`topic-${id}-arn`, topic.arn);
3443
- }
3444
- }
3445
- },
3446
- onStack(ctx) {
3447
- for (const id of ctx.stackConfig.topics ?? []) {
3448
- ctx.onFunction(({ policy }) => {
3449
- policy.addStatement({
3450
- actions: ["sns:Publish"],
3451
- resources: [ctx.shared.get(`topic-${id}-arn`)]
3452
- });
3453
- });
3454
- }
3455
- for (const [id, props] of Object.entries(ctx.stackConfig.subscribers ?? {})) {
3456
- const group = new Node12(ctx.stack, "topic", id);
3457
- const topicArn = ctx.shared.get(`topic-${id}-arn`);
3458
- if (typeof props === "string" && isEmail(props)) {
3459
- new aws12.sns.Subscription(group, id, {
3460
- topicArn,
3461
- protocol: "email",
3462
- endpoint: props
3463
- });
3464
- } else if (typeof props === "object") {
3465
- const { lambda } = createAsyncLambdaFunction(group, ctx, `topic`, id, props);
3466
- new aws12.sns.Subscription(group, id, {
3467
- topicArn,
3468
- protocol: "lambda",
3469
- endpoint: lambda.arn
3470
- });
3471
- new aws12.lambda.Permission(group, id, {
3472
- action: "lambda:InvokeFunction",
3473
- principal: "sns.amazonaws.com",
3474
- functionArn: lambda.arn,
3475
- sourceArn: topicArn
3476
- });
3477
- }
3478
- }
3479
- }
3480
- });
3481
-
3482
- // src/feature/vpc/index.ts
3483
- import { Node as Node13, all, aws as aws13 } from "@awsless/formation";
3484
- var vpcFeature = defineFeature({
3485
- name: "vpc",
3486
- onApp(ctx) {
3487
- const group = new Node13(ctx.base, "vpc", "main");
3488
- const vpc = new aws13.ec2.Vpc(group, "vpc", {
3489
- name: ctx.app.name,
3490
- cidrBlock: aws13.ec2.Peer.ipv4("10.0.0.0/16")
3491
- });
3492
- const privateRouteTable = new aws13.ec2.RouteTable(group, "private", {
3493
- vpcId: vpc.id,
3494
- name: "private"
3495
- });
3496
- const publicRouteTable = new aws13.ec2.RouteTable(group, "public", {
3497
- vpcId: vpc.id,
3498
- name: "public"
3499
- });
3500
- const gateway = new aws13.ec2.InternetGateway(group, "gateway");
3501
- const attachment = new aws13.ec2.VPCGatewayAttachment(group, "attachment", {
3502
- vpcId: vpc.id,
3503
- internetGatewayId: gateway.id
3504
- });
3505
- new aws13.ec2.Route(group, "route", {
3506
- gatewayId: gateway.id,
3507
- routeTableId: publicRouteTable.id,
3508
- destination: aws13.ec2.Peer.anyIpv4()
3509
- });
3510
- ctx.shared.set(
3511
- "vpc-id",
3512
- // Some resources require the internet gateway to be attached.
3513
- all([vpc.id, attachment.internetGatewayId]).apply(([id]) => id)
3514
- );
3515
- ctx.shared.set("vpc-security-group-id", vpc.defaultSecurityGroup);
3516
- const zones = ["a", "b"];
3517
- const tables = [privateRouteTable, publicRouteTable];
3518
- let block = 0;
3519
- for (const table2 of tables) {
3520
- for (const i in zones) {
3521
- const index = Number(i) + 1;
3522
- const id = `${table2.identifier}-${index}`;
3523
- const subnet = new aws13.ec2.Subnet(group, id, {
3524
- vpcId: vpc.id,
3525
- cidrBlock: aws13.ec2.Peer.ipv4(`10.0.${block++}.0/24`),
3526
- availabilityZone: ctx.appConfig.region + zones[i]
3527
- });
3528
- new aws13.ec2.SubnetRouteTableAssociation(group, id, {
3529
- routeTableId: table2.id,
3530
- subnetId: subnet.id
3531
- });
3532
- ctx.shared.set(`vpc-${table2.identifier}-subnet-id-${index}`, subnet.id);
3533
- }
3534
- }
3535
- }
3536
- });
3537
-
3538
- // src/feature/auth/index.ts
3539
- import { constantCase as constantCase5 } from "change-case";
3540
- import { Node as Node14, aws as aws14 } from "@awsless/formation";
3541
- var authFeature = defineFeature({
3542
- name: "auth",
3543
- async onTypeGen(ctx) {
3544
- const gen = new TypeFile("@awsless/awsless");
3545
- const resources = new TypeObject(1);
3546
- for (const name of Object.keys(ctx.appConfig.defaults.auth)) {
3547
- const authName = formatGlobalResourceName(ctx.appConfig.name, "auth", name);
3548
- resources.addType(
3549
- name,
3550
- `{ readonly name: '${authName}', readonly userPoolId: string, readonly clientId: string }`
3551
- );
3552
- }
3553
- gen.addInterface("AuthResources", resources);
3554
- await ctx.write("auth.d.ts", gen, true);
3555
- },
3556
- onStack(ctx) {
3557
- for (const [id, props] of Object.entries(ctx.stackConfig.auth ?? {})) {
3558
- const group = new Node14(ctx.stack, "auth", id);
3559
- const userPoolId = ctx.shared.get(`auth-${id}-user-pool-id`);
3560
- const userPoolArn = ctx.shared.get(`auth-${id}-user-pool-arn`);
3561
- const clientId = ctx.shared.get(`auth-${id}-client-id`);
3562
- const triggers = {};
3563
- const list4 = {};
3564
- for (const [trigger, triggerProps] of Object.entries(props.triggers ?? {})) {
3565
- const triggerGroup = new Node14(group, "trigger", trigger);
3566
- const { lambda, policy } = createAsyncLambdaFunction(
3567
- triggerGroup,
3568
- ctx,
3569
- "auth",
3570
- `${id}-${trigger}`,
3571
- triggerProps
3572
- );
3573
- triggers[trigger] = lambda.arn;
3574
- list4[trigger] = {
3575
- trigger,
3576
- group: triggerGroup,
3577
- lambda,
3578
- policy
3579
- };
3580
- }
3581
- new aws14.cognito.LambdaTriggers(group, "lambda-triggers", {
3582
- userPoolId,
3583
- triggers
3584
- });
3585
- for (const item of Object.values(list4)) {
3586
- new aws14.lambda.Permission(item.group, `permission`, {
3587
- action: "lambda:InvokeFunction",
3588
- principal: "cognito-idp.amazonaws.com",
3589
- functionArn: item.lambda.arn,
3590
- sourceArn: userPoolArn
3591
- });
3592
- item.lambda.addEnvironment(`AUTH_${constantCase5(id)}_USER_POOL_ID`, userPoolId);
3593
- item.lambda.addEnvironment(`AUTH_${constantCase5(id)}_CLIENT_ID`, clientId);
3594
- item.policy.addStatement({
3595
- actions: ["cognito:*"],
3596
- resources: [
3597
- // Not yet known if this is correct way to grant access to all resources
3598
- userPoolArn
3599
- // userPoolId.apply<aws.ARN>(
3600
- // id => `arn:aws:cognito-idp:${ctx.appConfig.region}:${ctx.accountId}:userpool/${id}`
3601
- // ),
3602
- // userPoolId.apply<aws.ARN>(
3603
- // id => `arn:aws:cognito-idp:${ctx.appConfig.region}:${ctx.accountId}:userpool/${id}*`
3604
- // ),
3605
- ]
3606
- });
3607
- }
3608
- }
3609
- },
3610
- onApp(ctx) {
3611
- for (const [id, props] of Object.entries(ctx.appConfig.defaults.auth ?? {})) {
3612
- const group = new Node14(ctx.base, "auth", id);
3613
- let emailConfig;
3614
- if (props.messaging) {
3615
- const [_, domainName] = props.messaging.fromEmail.split("@");
3616
- emailConfig = {
3617
- type: "developer",
3618
- replyTo: props.messaging.replyTo,
3619
- sourceArn: ctx.shared.get(`mail-${domainName}-arn`),
3620
- configurationSet: ctx.shared.get("mail-configuration-set"),
3621
- from: props.messaging.fromName ? `${props.messaging.fromName} <${props.messaging.fromEmail}>` : props.messaging.fromEmail
3622
- };
3623
- }
3624
- const name = formatGlobalResourceName(ctx.appConfig.name, "auth", id);
3625
- const userPool = new aws14.cognito.UserPool(group, "user-pool", {
3626
- name,
3627
- // deletionProtection: true,
3628
- allowUserRegistration: props.allowUserRegistration,
3629
- username: props.username,
3630
- password: props.password,
3631
- email: emailConfig
3632
- });
3633
- const client = new aws14.cognito.UserPoolClient(group, "client", {
3634
- userPoolId: userPool.id,
3635
- name,
3636
- validity: props.validity,
3637
- supportedIdentityProviders: ["cognito"],
3638
- authFlows: {
3639
- userSrp: true
3640
- }
3641
- });
3642
- ctx.bindEnv(`AUTH_${constantCase5(id)}_USER_POOL_ID`, userPool.id);
3643
- ctx.bindEnv(`AUTH_${constantCase5(id)}_CLIENT_ID`, client.id);
3644
- ctx.shared.set(`auth-${id}-user-pool-arn`, userPool.arn);
3645
- ctx.shared.set(`auth-${id}-user-pool-id`, userPool.id);
3646
- ctx.shared.set(`auth-${id}-client-id`, client.id);
3647
- }
3648
- }
3649
- });
3650
-
3651
- // src/feature/http/index.ts
3652
- import { Node as Node15, aws as aws15 } from "@awsless/formation";
3653
- import { camelCase as camelCase5, constantCase as constantCase6 } from "change-case";
3654
- import { relative as relative3 } from "path";
3655
- var parseRoute = (route) => {
3656
- const [method, ...paths] = route.split(" ");
3657
- const path = paths.join(" ");
3658
- return { method, path };
3659
- };
3660
- var strToInt = (str) => {
3661
- return parseInt(Buffer.from(str, "utf8").toString("hex"), 16);
3662
- };
3663
- var generatePriority = (stackName, route) => {
3664
- const start = strToInt(stackName) % 500 + 1;
3665
- const end = strToInt(route) % 100;
3666
- const priority = start + "" + end;
3667
- return parseInt(priority, 10);
3668
- };
3669
- var httpFeature = defineFeature({
3670
- name: "http",
3671
- async onTypeGen(ctx) {
3672
- const types2 = new TypeFile("@awsless/awsless");
3673
- const resources = new TypeObject(1);
3674
- const api = {};
3675
- for (const stack of ctx.stackConfigs) {
3676
- for (const [id, routes] of Object.entries(stack.http ?? {})) {
3677
- if (!(id in api))
3678
- api[id] = {};
3679
- for (const [route, props] of Object.entries(routes)) {
3680
- const { path, method } = parseRoute(route);
3681
- const file = typeof props === "string" ? props : props.file;
3682
- if (!(method in api[id])) {
3683
- api[id][method] = {};
3684
- }
3685
- api[id][method][path] = file;
3686
- }
3687
- }
3688
- }
3689
- for (const [id, routes] of Object.entries(api)) {
3690
- const idType = new TypeObject(2);
3691
- for (const [method, paths] of Object.entries(routes)) {
3692
- const methodType = new TypeObject(3);
3693
- for (const [path, file] of Object.entries(paths)) {
3694
- const paramType = new TypeObject(4);
3695
- for (const param of path.matchAll(/{([a-z0-9]+)}/g)) {
3696
- paramType.addType(param[0], "string | number");
3697
- }
3698
- const varName = camelCase5(`${id}-${path}-${method}`);
3699
- const relFile = relative3(directories.types, file);
3700
- types2.addImport(varName, relFile);
3701
- methodType.add(`'${path}'`, `Route<typeof ${varName}, ${paramType.toString() || "never"}>`);
3702
- }
3703
- idType.addConst(method, methodType);
3704
- }
3705
- resources.addType(id, idType);
3706
- }
3707
- const code = [
3708
- `import { InvokeResponse } from '@awsless/lambda'`,
3709
- `type Function = (...args: any) => any`,
3710
- `type Event<F extends Function> = Parameters<F>[0]`,
3711
- `type RequestWithQuery = { request: { queryStringParameters: any } }`,
3712
- `type RequestWithBody = { request: { body: any } }`,
3713
- `type ResponseWithBody = { statusCode: number, body: any }`,
3714
- `type Query<F extends Function> = Event<F> extends RequestWithQuery ? Event<F>['request']['queryStringParameters'] : never`,
3715
- `type Body<F extends Function> = Event<F> extends RequestWithBody ? Exclude<Event<F>['request']['body'], string> : never`,
3716
- `type Response<F extends Function> = Awaited<InvokeResponse<F>> extends ResponseWithBody ? Promise<Awaited<InvokeResponse<F>>['body']> : Promise<never>`,
3717
- `type Route<F extends Function, P> = { param: P; query: Query<F>; body: Body<F>; response: Response<F> }`
3718
- ];
3719
- code.map((code2) => types2.addCode(code2));
3720
- types2.addInterface("HTTP", resources);
3721
- await ctx.write("http.d.ts", types2, true);
3722
- },
3723
- onApp(ctx) {
3724
- if (Object.keys(ctx.appConfig.defaults?.http ?? {}).length === 0) {
3725
- return;
3726
- }
3727
- const group = new Node15(ctx.base, "http", "main");
3728
- const securityGroup = new aws15.ec2.SecurityGroup(group, "http", {
3729
- vpcId: ctx.shared.get(`vpc-id`),
3730
- name: formatGlobalResourceName(ctx.app.name, "http", "http"),
3731
- description: `Global security group for HTTP api.`
3732
- });
3733
- const port = aws15.ec2.Port.tcp(443);
3734
- securityGroup.addIngressRule({ port, peer: aws15.ec2.Peer.anyIpv4() });
3735
- securityGroup.addIngressRule({ port, peer: aws15.ec2.Peer.anyIpv6() });
3736
- for (const [id, props] of Object.entries(ctx.appConfig.defaults?.http ?? {})) {
3737
- const group2 = new Node15(ctx.base, "http", id);
3738
- const loadBalancer = new aws15.elb.LoadBalancer(group2, "balancer", {
3739
- name: formatGlobalResourceName(ctx.app.name, "http", id),
3740
- type: "application",
3741
- securityGroups: [securityGroup.id],
3742
- subnets: [
3743
- //
3744
- ctx.shared.get(`vpc-public-subnet-id-1`),
3745
- ctx.shared.get(`vpc-public-subnet-id-2`)
3746
- ]
3747
- });
3748
- const listener = new aws15.elb.Listener(group2, "listener", {
3749
- loadBalancerArn: loadBalancer.arn,
3750
- port: 443,
3751
- protocol: "https",
3752
- certificates: [ctx.shared.get(`local-certificate-${props.domain}-arn`)],
3753
- defaultActions: [
3754
- aws15.elb.ListenerAction.fixedResponse({
3755
- statusCode: 404,
3756
- contentType: "application/json",
3757
- messageBody: JSON.stringify({
3758
- message: "Route not found"
3759
- })
3760
- })
3761
- ]
3762
- });
3763
- ctx.shared.set(`http-${id}-listener-arn`, listener.arn);
3764
- const domainName = formatFullDomainName(ctx.appConfig, props.domain, props.subDomain);
3765
- new aws15.route53.RecordSet(group2, domainName, {
3766
- hostedZoneId: ctx.shared.get(`hosted-zone-${props.domain}-id`),
3767
- name: domainName,
3768
- type: "A",
3769
- alias: {
3770
- evaluateTargetHealth: false,
3771
- hostedZoneId: loadBalancer.hostedZoneId,
3772
- dnsName: loadBalancer.dnsName
3773
- }
3774
- });
3775
- ctx.bindEnv(`HTTP_${constantCase6(id)}_ENDPOINT`, domainName);
3776
- }
3777
- },
3778
- onStack(ctx) {
3779
- for (const [id, routes] of Object.entries(ctx.stackConfig.http ?? {})) {
3780
- const props = ctx.appConfig.defaults.http?.[id];
3781
- if (!props) {
3782
- throw new Error(`Http definition is not defined on app level for "${id}"`);
3783
- }
3784
- const group = new Node15(ctx.stack, "http", id);
3785
- for (const [routeKey, routeProps] of Object.entries(routes)) {
3786
- const routeGroup = new Node15(group, "route", routeKey);
3787
- const { method, path } = parseRoute(routeKey);
3788
- const routeId = shortId(routeKey);
3789
- const { lambda } = createLambdaFunction(routeGroup, ctx, "http", `${id}-${routeId}`, {
3790
- ...routeProps,
3791
- description: routeKey
3792
- });
3793
- const name = formatLocalResourceName(ctx.app.name, ctx.stack.name, "http", routeId);
3794
- const permission = new aws15.lambda.Permission(routeGroup, id, {
3795
- action: "lambda:InvokeFunction",
3796
- principal: "elasticloadbalancing.amazonaws.com",
3797
- functionArn: lambda.arn
3798
- // sourceArn: `arn:aws:elasticloadbalancing:${ctx.appConfig.region}:*:targetgroup/${name}/*`,
3799
- });
3800
- const target = new aws15.elb.TargetGroup(routeGroup, id, {
3801
- name,
3802
- type: "lambda",
3803
- targets: [lambda.arn]
3804
- }).dependsOn(permission);
3805
- new aws15.elb.ListenerRule(routeGroup, id, {
3806
- listenerArn: ctx.shared.get(`http-${id}-listener-arn`),
3807
- priority: generatePriority(ctx.stackConfig.name, routeKey),
3808
- conditions: [
3809
- aws15.elb.ListenerCondition.httpRequestMethods([method]),
3810
- aws15.elb.ListenerCondition.pathPatterns([path])
3811
- ],
3812
- actions: [aws15.elb.ListenerAction.forward([target.arn])]
3813
- }).dependsOn(target);
3814
- }
3815
- }
3816
- }
3817
- });
3818
-
3819
- // src/feature/search/index.ts
3820
- import { Node as Node16, aws as aws16 } from "@awsless/formation";
3821
- import { constantCase as constantCase7 } from "change-case";
3822
- var typeGenCode6 = `
3823
- import { AnyStruct, Table } from '@awsless/open-search'
3824
-
3825
- type Search = {
3826
- readonly domain: string
3827
- readonly defineTable: <N extends stringm S extends AnyStruct>(tableName: N, schema: S) => Table<N, S>
3828
- }
3829
- `;
3830
- var searchFeature = defineFeature({
3831
- name: "search",
3832
- async onTypeGen(ctx) {
3833
- const gen = new TypeFile("@awsless/awsless");
3834
- const resources = new TypeObject(1);
3835
- for (const stack of ctx.stackConfigs) {
3836
- const list4 = new TypeObject(2);
3837
- for (const id of Object.keys(stack.searchs ?? {})) {
3838
- list4.addType(id, `Search`);
3839
- }
3840
- resources.addType(stack.name, list4);
3841
- }
3842
- gen.addCode(typeGenCode6);
3843
- gen.addInterface("SearchResources", resources);
3844
- await ctx.write("search.d.ts", gen, true);
3640
+ gen.addCode(typeGenCode4);
3641
+ gen.addInterface("SearchResources", resources);
3642
+ await ctx.write("search.d.ts", gen, true);
3845
3643
  },
3846
3644
  onStack(ctx) {
3847
3645
  for (const [id, props] of Object.entries(ctx.stackConfig.searchs ?? {})) {
3848
- const group = new Node16(ctx.stack, "search", id);
3849
- const openSearch = new aws16.openSearch.Domain(group, "domain", {
3646
+ const group = new Node13(ctx.stack, "search", id);
3647
+ const openSearch = new aws13.openSearch.Domain(group, "domain", {
3850
3648
  // name: formatLocalResourceName(ctx.app.name, ctx.stack.name, this.name, id),
3851
3649
  version: props.version,
3852
3650
  storageSize: props.storage,
@@ -3874,7 +3672,7 @@ var searchFeature = defineFeature({
3874
3672
  }
3875
3673
  ctx.onFunction(({ lambda, policy }) => {
3876
3674
  lambda.addEnvironment(
3877
- `SEARCH_${constantCase7(ctx.stack.name)}_${constantCase7(id)}_DOMAIN`,
3675
+ `SEARCH_${constantCase8(ctx.stack.name)}_${constantCase8(id)}_DOMAIN`,
3878
3676
  openSearch.domainEndpoint
3879
3677
  );
3880
3678
  policy.addStatement({
@@ -3887,7 +3685,7 @@ var searchFeature = defineFeature({
3887
3685
  });
3888
3686
 
3889
3687
  // src/feature/site/index.ts
3890
- import { Asset as Asset3, Node as Node17, aws as aws17 } from "@awsless/formation";
3688
+ import { Asset as Asset3, Node as Node14, aws as aws14 } from "@awsless/formation";
3891
3689
  import { days as days3, seconds as seconds3 } from "@awsless/duration";
3892
3690
  import { glob as glob2 } from "glob";
3893
3691
  import { join as join8 } from "path";
@@ -3917,7 +3715,7 @@ var siteFeature = defineFeature({
3917
3715
  name: "site",
3918
3716
  onStack(ctx) {
3919
3717
  for (const [id, props] of Object.entries(ctx.stackConfig.sites ?? {})) {
3920
- const group = new Node17(ctx.stack, "site", id);
3718
+ const group = new Node14(ctx.stack, "site", id);
3921
3719
  const name = formatLocalResourceName(ctx.app.name, ctx.stack.name, "site", id);
3922
3720
  const origins = [];
3923
3721
  const originGroups = [];
@@ -3927,7 +3725,7 @@ var siteFeature = defineFeature({
3927
3725
  const { lambda, code } = createLambdaFunction(group, ctx, `site`, id, props.ssr);
3928
3726
  versions.push(code.version);
3929
3727
  ctx.registerSiteFunction(lambda);
3930
- new aws17.lambda.Permission(group, "permission", {
3728
+ new aws14.lambda.Permission(group, "permission", {
3931
3729
  principal: "*",
3932
3730
  // principal: 'cloudfront.amazonaws.com',
3933
3731
  action: "lambda:InvokeFunctionUrl",
@@ -3936,7 +3734,7 @@ var siteFeature = defineFeature({
3936
3734
  // urlAuthType: 'aws-iam',
3937
3735
  // sourceArn: distribution.arn,
3938
3736
  });
3939
- const url = new aws17.lambda.Url(group, "url", {
3737
+ const url = new aws14.lambda.Url(group, "url", {
3940
3738
  targetArn: lambda.arn,
3941
3739
  authType: "none"
3942
3740
  // authType: 'aws-iam',
@@ -3948,7 +3746,7 @@ var siteFeature = defineFeature({
3948
3746
  });
3949
3747
  }
3950
3748
  if (props.static) {
3951
- bucket = new aws17.s3.Bucket(group, "bucket", {
3749
+ bucket = new aws14.s3.Bucket(group, "bucket", {
3952
3750
  name,
3953
3751
  forceDelete: true,
3954
3752
  website: {
@@ -3965,7 +3763,7 @@ var siteFeature = defineFeature({
3965
3763
  ]
3966
3764
  });
3967
3765
  bucket.deletionPolicy = "after-deployment";
3968
- const accessControl = new aws17.cloudFront.OriginAccessControl(group, `access`, {
3766
+ const accessControl = new aws14.cloudFront.OriginAccessControl(group, `access`, {
3969
3767
  name,
3970
3768
  type: "s3",
3971
3769
  behavior: "always",
@@ -3977,7 +3775,7 @@ var siteFeature = defineFeature({
3977
3775
  nodir: true
3978
3776
  });
3979
3777
  for (const file of files) {
3980
- const object = new aws17.s3.BucketObject(group, file, {
3778
+ const object = new aws14.s3.BucketObject(group, file, {
3981
3779
  bucket: bucket.name,
3982
3780
  key: file,
3983
3781
  body: Asset3.fromFile(join8(props.static, file)),
@@ -4000,14 +3798,14 @@ var siteFeature = defineFeature({
4000
3798
  statusCodes: [403, 404]
4001
3799
  });
4002
3800
  }
4003
- const cache = new aws17.cloudFront.CachePolicy(group, "cache", {
3801
+ const cache = new aws14.cloudFront.CachePolicy(group, "cache", {
4004
3802
  name,
4005
3803
  minTtl: seconds3(1),
4006
3804
  maxTtl: days3(365),
4007
3805
  defaultTtl: days3(1),
4008
3806
  ...props.cache
4009
3807
  });
4010
- const originRequest = new aws17.cloudFront.OriginRequestPolicy(group, "request", {
3808
+ const originRequest = new aws14.cloudFront.OriginRequestPolicy(group, "request", {
4011
3809
  name,
4012
3810
  header: {
4013
3811
  behavior: "all-except",
@@ -4015,7 +3813,7 @@ var siteFeature = defineFeature({
4015
3813
  }
4016
3814
  });
4017
3815
  const domainName = formatFullDomainName(ctx.appConfig, props.domain, props.subDomain);
4018
- const responseHeaders = new aws17.cloudFront.ResponseHeadersPolicy(group, "response", {
3816
+ const responseHeaders = new aws14.cloudFront.ResponseHeadersPolicy(group, "response", {
4019
3817
  name,
4020
3818
  cors: props.cors,
4021
3819
  remove: ["server"]
@@ -4023,7 +3821,7 @@ var siteFeature = defineFeature({
4023
3821
  // override: true,
4024
3822
  // },
4025
3823
  });
4026
- const distribution = new aws17.cloudFront.Distribution(group, "distribution", {
3824
+ const distribution = new aws14.cloudFront.Distribution(group, "distribution", {
4027
3825
  name,
4028
3826
  certificateArn: ctx.shared.get(`global-certificate-${props.domain}-arn`),
4029
3827
  compress: true,
@@ -4051,13 +3849,13 @@ var siteFeature = defineFeature({
4051
3849
  };
4052
3850
  })
4053
3851
  });
4054
- new aws17.cloudFront.InvalidateCache(group, "invalidate", {
3852
+ new aws14.cloudFront.InvalidateCache(group, "invalidate", {
4055
3853
  distributionId: distribution.id,
4056
3854
  paths: ["/*"],
4057
3855
  versions
4058
3856
  });
4059
3857
  if (props.static) {
4060
- new aws17.s3.BucketPolicy(group, `policy`, {
3858
+ new aws14.s3.BucketPolicy(group, `policy`, {
4061
3859
  bucketName: bucket.name,
4062
3860
  statements: [
4063
3861
  {
@@ -4083,7 +3881,7 @@ var siteFeature = defineFeature({
4083
3881
  ]
4084
3882
  });
4085
3883
  }
4086
- new aws17.route53.RecordSet(group, `record`, {
3884
+ new aws14.route53.RecordSet(group, `record`, {
4087
3885
  hostedZoneId: ctx.shared.get(`hosted-zone-${props.domain}-id`),
4088
3886
  type: "A",
4089
3887
  name: domainName,
@@ -4097,88 +3895,142 @@ var siteFeature = defineFeature({
4097
3895
  }
4098
3896
  });
4099
3897
 
4100
- // src/feature/rest/index.ts
4101
- import { Node as Node18, aws as aws18 } from "@awsless/formation";
4102
- import { constantCase as constantCase8 } from "change-case";
4103
- var restFeature = defineFeature({
4104
- name: "rest",
4105
- onApp(ctx) {
4106
- for (const [id, props] of Object.entries(ctx.appConfig.defaults?.rest ?? {})) {
4107
- const group = new Node18(ctx.base, "rest", id);
4108
- const api = new aws18.apiGatewayV2.Api(group, "api", {
4109
- name: formatGlobalResourceName(ctx.app.name, "rest", id),
4110
- protocolType: "HTTP"
4111
- });
4112
- const stage = new aws18.apiGatewayV2.Stage(group, "stage", {
4113
- name: "v1",
4114
- apiId: api.id
4115
- });
4116
- ctx.shared.set(`rest-${id}-id`, api.id);
4117
- if (props.domain) {
4118
- const domainName = formatFullDomainName(ctx.appConfig, props.domain, props.subDomain);
4119
- const hostedZoneId = ctx.shared.get(`hosted-zone-${props.domain}-id`);
4120
- const certificateArn = ctx.shared.get(`certificate-${props.domain}-arn`);
4121
- const domain = new aws18.apiGatewayV2.DomainName(group, "domain", {
4122
- name: domainName,
4123
- certificates: [
4124
- {
4125
- certificateArn
4126
- }
4127
- ]
4128
- });
4129
- const mapping = new aws18.apiGatewayV2.ApiMapping(group, "mapping", {
4130
- apiId: api.id,
4131
- domainName: domain.name,
4132
- stage: stage.name
4133
- });
4134
- const record = new aws18.route53.RecordSet(group, "record", {
4135
- hostedZoneId,
4136
- type: "A",
4137
- name: domainName,
4138
- alias: {
4139
- dnsName: domain.regionalDomainName,
4140
- hostedZoneId: domain.regionalHostedZoneId,
4141
- evaluateTargetHealth: false
4142
- }
4143
- });
4144
- record.dependsOn(domain, mapping);
4145
- ctx.bindEnv(`REST_${constantCase8(id)}_ENDPOINT`, domainName);
3898
+ // src/feature/store/index.ts
3899
+ import { aws as aws15, Node as Node15 } from "@awsless/formation";
3900
+ var typeGenCode5 = `
3901
+ import { Body, PutObjectProps, BodyStream, createPresignedPost } from '@awsless/s3'
3902
+ import { Size } from '@awsless/size'
3903
+ import { Duration } from '@awsless/duration'
3904
+ import { PresignedPost } from '@aws-sdk/s3-presigned-post'
3905
+
3906
+ type Store<Name extends string> = {
3907
+ readonly name: Name
3908
+ readonly put: (key: string, body: Body, options?: Pick<PutObjectProps, 'metadata' | 'storageClass'>) => Promise<void>
3909
+ readonly get: (key: string) => Promise<BodyStream | undefined>
3910
+ readonly delete: (key: string) => Promise<void>
3911
+ readonly createPresignedPost: (key: string, contentLengthRange: [Size, Size], expires?: Duration, fields?: Record<string, string>) => Promise<PresignedPost>
3912
+ }
3913
+ `;
3914
+ var storeFeature = defineFeature({
3915
+ name: "store",
3916
+ async onTypeGen(ctx) {
3917
+ const gen = new TypeFile("@awsless/awsless");
3918
+ const resources = new TypeObject(1);
3919
+ for (const stack of ctx.stackConfigs) {
3920
+ const list4 = new TypeObject(2);
3921
+ for (const id of Object.keys(stack.stores ?? {})) {
3922
+ const storeName = formatLocalResourceName(ctx.appConfig.name, stack.name, "store", id);
3923
+ list4.addType(id, `Store<'${storeName}'>`);
3924
+ }
3925
+ resources.addType(stack.name, list4);
3926
+ }
3927
+ gen.addCode(typeGenCode5);
3928
+ gen.addInterface("StoreResources", resources);
3929
+ await ctx.write("store.d.ts", gen, true);
3930
+ },
3931
+ onStack(ctx) {
3932
+ for (const [id, props] of Object.entries(ctx.stackConfig.stores ?? {})) {
3933
+ const group = new Node15(ctx.stack, "store", id);
3934
+ const bucketName = formatLocalResourceName(ctx.appConfig.name, ctx.stack.name, "store", id);
3935
+ const lambdaConfigs = [];
3936
+ const eventMap = {
3937
+ "created:*": "s3:ObjectCreated:*",
3938
+ "created:put": "s3:ObjectCreated:Put",
3939
+ "created:post": "s3:ObjectCreated:Post",
3940
+ "created:copy": "s3:ObjectCreated:Copy",
3941
+ "created:upload": "s3:ObjectCreated:CompleteMultipartUpload",
3942
+ "removed:*": "s3:ObjectRemoved:*",
3943
+ "removed:delete": "s3:ObjectRemoved:Delete",
3944
+ "removed:marker": "s3:ObjectRemoved:DeleteMarkerCreated"
3945
+ };
3946
+ for (const [event, funcProps] of Object.entries(props.events ?? {})) {
3947
+ const eventGroup = new Node15(group, "event", event);
3948
+ const { lambda } = createAsyncLambdaFunction(eventGroup, ctx, `store`, id, funcProps);
3949
+ new aws15.lambda.Permission(eventGroup, "permission", {
3950
+ action: "lambda:InvokeFunction",
3951
+ principal: "s3.amazonaws.com",
3952
+ functionArn: lambda.arn,
3953
+ sourceArn: `arn:aws:s3:::${bucketName}`
3954
+ });
3955
+ lambdaConfigs.push({
3956
+ event: eventMap[event],
3957
+ function: lambda.arn
3958
+ });
3959
+ }
3960
+ const bucket = new aws15.s3.Bucket(group, "store", {
3961
+ name: bucketName,
3962
+ versioning: props.versioning,
3963
+ lambdaConfigs,
3964
+ cors: [
3965
+ // ---------------------------------------------
3966
+ // Support for presigned post requests
3967
+ // ---------------------------------------------
3968
+ {
3969
+ origins: ["*"],
3970
+ methods: ["POST"]
3971
+ }
3972
+ // ---------------------------------------------
3973
+ ]
3974
+ });
3975
+ ctx.onFunction(({ policy }) => {
3976
+ policy.addStatement(bucket.permissions);
3977
+ });
3978
+ }
3979
+ }
3980
+ });
3981
+
3982
+ // src/feature/table/index.ts
3983
+ import { Node as Node16, aws as aws16 } from "@awsless/formation";
3984
+ var tableFeature = defineFeature({
3985
+ name: "table",
3986
+ async onTypeGen(ctx) {
3987
+ const gen = new TypeFile("@awsless/awsless");
3988
+ const resources = new TypeObject(1);
3989
+ for (const stack of ctx.stackConfigs) {
3990
+ const list4 = new TypeObject(2);
3991
+ for (const name of Object.keys(stack.tables || {})) {
3992
+ const tableName = formatLocalResourceName(ctx.appConfig.name, stack.name, "table", name);
3993
+ list4.addType(name, `'${tableName}'`);
4146
3994
  }
3995
+ resources.addType(stack.name, list4);
4147
3996
  }
3997
+ gen.addInterface("TableResources", resources);
3998
+ await ctx.write("table.d.ts", gen, true);
4148
3999
  },
4149
4000
  onStack(ctx) {
4150
- for (const [id, routes] of Object.entries(ctx.stackConfig.rest ?? {})) {
4151
- const restGroup = new Node18(ctx.stack, "rest", id);
4152
- for (const [routeKey, props] of Object.entries(routes)) {
4153
- const group = new Node18(restGroup, "route", routeKey);
4154
- const apiId = ctx.shared.get(`rest-${id}-id`);
4155
- const routeId = shortId(routeKey);
4156
- const { lambda } = createLambdaFunction(group, ctx, "rest", `${id}-${routeId}`, {
4157
- ...props,
4158
- description: `${id} ${routeKey}`
4159
- });
4160
- const permission = new aws18.lambda.Permission(group, "permission", {
4161
- action: "lambda:InvokeFunction",
4162
- principal: "apigateway.amazonaws.com",
4163
- functionArn: lambda.arn
4164
- });
4165
- const integration = new aws18.apiGatewayV2.Integration(group, "integration", {
4166
- apiId,
4167
- description: `${id} ${routeKey}`,
4168
- method: "POST",
4169
- payloadFormatVersion: "2.0",
4170
- type: "AWS_PROXY",
4171
- uri: lambda.arn.apply((arn) => {
4172
- return `arn:aws:apigateway:${ctx.appConfig.region}:lambda:path/2015-03-31/functions/${arn}/invocations`;
4173
- })
4174
- });
4175
- const route = new aws18.apiGatewayV2.Route(group, "route", {
4176
- apiId,
4177
- routeKey,
4178
- target: integration.id.apply((id2) => `integrations/${id2}`)
4001
+ for (const [id, props] of Object.entries(ctx.stackConfig.tables ?? {})) {
4002
+ const group = new Node16(ctx.stack, "table", id);
4003
+ const table2 = new aws16.dynamodb.Table(group, "table", {
4004
+ ...props,
4005
+ name: formatLocalResourceName(ctx.appConfig.name, ctx.stackConfig.name, "table", id),
4006
+ stream: props.stream?.type
4007
+ });
4008
+ if (props.stream) {
4009
+ const { lambda, policy } = createLambdaFunction(group, ctx, "table", id, props.stream.consumer);
4010
+ lambda.addEnvironment("LOG_VIEWABLE_ERROR", "1");
4011
+ const onFailure = getGlobalOnFailure(ctx);
4012
+ const source = new aws16.lambda.EventSourceMapping(group, id, {
4013
+ functionArn: lambda.arn,
4014
+ sourceArn: table2.streamArn,
4015
+ batchSize: 100,
4016
+ bisectBatchOnError: true,
4017
+ // retryAttempts: props.stream.consumer.retryAttempts ?? -1,
4018
+ parallelizationFactor: 1,
4019
+ startingPosition: "latest",
4020
+ onFailure
4179
4021
  });
4180
- route.dependsOn(lambda, permission);
4022
+ policy.addStatement(table2.streamPermissions);
4023
+ source.dependsOn(policy);
4024
+ if (onFailure) {
4025
+ policy.addStatement({
4026
+ actions: ["sqs:SendMessage", "sqs:GetQueueUrl"],
4027
+ resources: [onFailure]
4028
+ });
4029
+ }
4181
4030
  }
4031
+ ctx.onFunction(({ policy }) => {
4032
+ policy.addStatement(...table2.permissions);
4033
+ });
4182
4034
  }
4183
4035
  }
4184
4036
  });
@@ -4186,8 +4038,8 @@ var restFeature = defineFeature({
4186
4038
  // src/feature/task/index.ts
4187
4039
  import { camelCase as camelCase6 } from "change-case";
4188
4040
  import { relative as relative4 } from "path";
4189
- import { Node as Node19 } from "@awsless/formation";
4190
- var typeGenCode7 = `
4041
+ import { Node as Node17 } from "@awsless/formation";
4042
+ var typeGenCode6 = `
4191
4043
  import { InvokeOptions } from '@awsless/lambda'
4192
4044
  import type { Mock } from 'vitest'
4193
4045
 
@@ -4226,7 +4078,7 @@ var taskFeature = defineFeature({
4226
4078
  resources.addType(stack.name, resource2);
4227
4079
  mockResponses.addType(stack.name, mockResponse);
4228
4080
  }
4229
- types2.addCode(typeGenCode7);
4081
+ types2.addCode(typeGenCode6);
4230
4082
  types2.addInterface("TaskResources", resources);
4231
4083
  types2.addInterface("TaskMock", mocks);
4232
4084
  types2.addInterface("TaskMockResponse", mockResponses);
@@ -4234,12 +4086,160 @@ var taskFeature = defineFeature({
4234
4086
  },
4235
4087
  onStack(ctx) {
4236
4088
  for (const [id, props] of Object.entries(ctx.stackConfig.tasks ?? {})) {
4237
- const group = new Node19(ctx.stack, "task", id);
4089
+ const group = new Node17(ctx.stack, "task", id);
4238
4090
  createAsyncLambdaFunction(group, ctx, "task", id, props.consumer);
4239
4091
  }
4240
4092
  }
4241
4093
  });
4242
4094
 
4095
+ // src/feature/test/index.ts
4096
+ var testFeature = defineFeature({
4097
+ name: "test",
4098
+ onStack(ctx) {
4099
+ if (ctx.stackConfig.tests) {
4100
+ ctx.registerTest(ctx.stackConfig.name, ctx.stackConfig.tests);
4101
+ }
4102
+ }
4103
+ });
4104
+
4105
+ // src/feature/topic/index.ts
4106
+ import { Node as Node18, aws as aws17 } from "@awsless/formation";
4107
+ var typeGenCode7 = `
4108
+ import type { PublishOptions } from '@awsless/sns'
4109
+ import type { Mock } from 'vitest'
4110
+
4111
+ type Publish<Name extends string> = {
4112
+ readonly name: Name
4113
+ (payload: unknown, options?: Omit<PublishOptions, 'topic' | 'payload'>): Promise<void>
4114
+ }
4115
+
4116
+ type MockHandle = (payload: unknown) => void
4117
+ type MockBuilder = (handle?: MockHandle) => void
4118
+ `;
4119
+ var topicFeature = defineFeature({
4120
+ name: "topic",
4121
+ async onTypeGen(ctx) {
4122
+ const gen = new TypeFile("@awsless/awsless");
4123
+ const resources = new TypeObject(1);
4124
+ const mocks = new TypeObject(1);
4125
+ const mockResponses = new TypeObject(1);
4126
+ for (const stack of ctx.stackConfigs) {
4127
+ for (const topic of stack.topics || []) {
4128
+ const name = formatGlobalResourceName(ctx.appConfig.name, "topic", topic);
4129
+ mockResponses.addType(topic, "Mock");
4130
+ resources.addType(topic, `Publish<'${name}'>`);
4131
+ mocks.addType(topic, `MockBuilder`);
4132
+ }
4133
+ }
4134
+ gen.addCode(typeGenCode7);
4135
+ gen.addInterface("TopicResources", resources);
4136
+ gen.addInterface("TopicMock", mocks);
4137
+ gen.addInterface("TopicMockResponse", mockResponses);
4138
+ await ctx.write("topic.d.ts", gen, true);
4139
+ },
4140
+ onApp(ctx) {
4141
+ for (const stack of ctx.stackConfigs) {
4142
+ for (const id of stack.topics ?? []) {
4143
+ const group = new Node18(ctx.base, "topic", id);
4144
+ const topic = new aws17.sns.Topic(group, "topic", {
4145
+ name: formatGlobalResourceName(ctx.appConfig.name, "topic", id)
4146
+ });
4147
+ ctx.shared.set(`topic-${id}-arn`, topic.arn);
4148
+ }
4149
+ }
4150
+ },
4151
+ onStack(ctx) {
4152
+ for (const id of ctx.stackConfig.topics ?? []) {
4153
+ ctx.onFunction(({ policy }) => {
4154
+ policy.addStatement({
4155
+ actions: ["sns:Publish"],
4156
+ resources: [ctx.shared.get(`topic-${id}-arn`)]
4157
+ });
4158
+ });
4159
+ }
4160
+ for (const [id, props] of Object.entries(ctx.stackConfig.subscribers ?? {})) {
4161
+ const group = new Node18(ctx.stack, "topic", id);
4162
+ const topicArn = ctx.shared.get(`topic-${id}-arn`);
4163
+ if (typeof props === "string" && isEmail(props)) {
4164
+ new aws17.sns.Subscription(group, id, {
4165
+ topicArn,
4166
+ protocol: "email",
4167
+ endpoint: props
4168
+ });
4169
+ } else if (typeof props === "object") {
4170
+ const { lambda } = createAsyncLambdaFunction(group, ctx, `topic`, id, props);
4171
+ new aws17.sns.Subscription(group, id, {
4172
+ topicArn,
4173
+ protocol: "lambda",
4174
+ endpoint: lambda.arn
4175
+ });
4176
+ new aws17.lambda.Permission(group, id, {
4177
+ action: "lambda:InvokeFunction",
4178
+ principal: "sns.amazonaws.com",
4179
+ functionArn: lambda.arn,
4180
+ sourceArn: topicArn
4181
+ });
4182
+ }
4183
+ }
4184
+ }
4185
+ });
4186
+
4187
+ // src/feature/vpc/index.ts
4188
+ import { Node as Node19, all, aws as aws18 } from "@awsless/formation";
4189
+ var vpcFeature = defineFeature({
4190
+ name: "vpc",
4191
+ onApp(ctx) {
4192
+ const group = new Node19(ctx.base, "vpc", "main");
4193
+ const vpc = new aws18.ec2.Vpc(group, "vpc", {
4194
+ name: ctx.app.name,
4195
+ cidrBlock: aws18.ec2.Peer.ipv4("10.0.0.0/16")
4196
+ });
4197
+ const privateRouteTable = new aws18.ec2.RouteTable(group, "private", {
4198
+ vpcId: vpc.id,
4199
+ name: "private"
4200
+ });
4201
+ const publicRouteTable = new aws18.ec2.RouteTable(group, "public", {
4202
+ vpcId: vpc.id,
4203
+ name: "public"
4204
+ });
4205
+ const gateway = new aws18.ec2.InternetGateway(group, "gateway");
4206
+ const attachment = new aws18.ec2.VPCGatewayAttachment(group, "attachment", {
4207
+ vpcId: vpc.id,
4208
+ internetGatewayId: gateway.id
4209
+ });
4210
+ new aws18.ec2.Route(group, "route", {
4211
+ gatewayId: gateway.id,
4212
+ routeTableId: publicRouteTable.id,
4213
+ destination: aws18.ec2.Peer.anyIpv4()
4214
+ });
4215
+ ctx.shared.set(
4216
+ "vpc-id",
4217
+ // Some resources require the internet gateway to be attached.
4218
+ all([vpc.id, attachment.internetGatewayId]).apply(([id]) => id)
4219
+ );
4220
+ ctx.shared.set("vpc-security-group-id", vpc.defaultSecurityGroup);
4221
+ const zones = ["a", "b"];
4222
+ const tables = [privateRouteTable, publicRouteTable];
4223
+ let block = 0;
4224
+ for (const table2 of tables) {
4225
+ for (const i in zones) {
4226
+ const index = Number(i) + 1;
4227
+ const id = `${table2.identifier}-${index}`;
4228
+ const subnet = new aws18.ec2.Subnet(group, id, {
4229
+ vpcId: vpc.id,
4230
+ cidrBlock: aws18.ec2.Peer.ipv4(`10.0.${block++}.0/24`),
4231
+ availabilityZone: ctx.appConfig.region + zones[i]
4232
+ });
4233
+ new aws18.ec2.SubnetRouteTableAssociation(group, id, {
4234
+ routeTableId: table2.id,
4235
+ subnetId: subnet.id
4236
+ });
4237
+ ctx.shared.set(`vpc-${table2.identifier}-subnet-id-${index}`, subnet.id);
4238
+ }
4239
+ }
4240
+ }
4241
+ });
4242
+
4243
4243
  // src/feature/index.ts
4244
4244
  var features = [
4245
4245
  // 1