@xube/kit-aws-hooks-infrastructure 0.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/functions/add-webhook-endpoint.d.ts +3 -0
- package/dist/functions/add-webhook-endpoint.js +34 -0
- package/dist/functions/get-webhook-endpoint.d.ts +0 -0
- package/dist/functions/get-webhook-endpoint.js +1 -0
- package/dist/functions/remove-webhook-endpoint.d.ts +0 -0
- package/dist/functions/remove-webhook-endpoint.js +1 -0
- package/dist/webhook-management.d.ts +26 -0
- package/dist/webhook-management.js +84 -0
- package/package.json +32 -0
- package/src/functions/add-webhook-endpoint.ts +50 -0
- package/src/functions/get-webhook-endpoint.ts +0 -0
- package/src/functions/remove-webhook-endpoint.ts +0 -0
- package/src/webhook-management.ts +132 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handler = exports.webhookTableName = void 0;
|
|
4
|
+
const kit_aws_hooks_1 = require("@xube/kit-aws-hooks");
|
|
5
|
+
const kit_constants_1 = require("@xube/kit-constants");
|
|
6
|
+
const kit_log_1 = require("@xube/kit-log");
|
|
7
|
+
exports.webhookTableName = process.env.WEBHOOK_TABLE_NAME_ENV_VAR || "";
|
|
8
|
+
const handler = async (event) => {
|
|
9
|
+
if (!exports.webhookTableName) {
|
|
10
|
+
return {
|
|
11
|
+
body: `Could not find webhook table name in environment variables`,
|
|
12
|
+
statusCode: kit_constants_1.StatusCode.InternalError,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
if (!event.body) {
|
|
16
|
+
return {
|
|
17
|
+
body: `No body provided`,
|
|
18
|
+
statusCode: kit_constants_1.StatusCode.BadRequest,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const request = JSON.parse(event.body);
|
|
22
|
+
if (!(0, kit_aws_hooks_1.isCreateWebhookForAccountRequest)(request)) {
|
|
23
|
+
return {
|
|
24
|
+
body: `Request body is not a valid Webhook Endpoint creation request`,
|
|
25
|
+
statusCode: kit_constants_1.StatusCode.BadRequest,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const response = await (0, kit_aws_hooks_1.createWebhookForAccount)(request, exports.webhookTableName, kit_log_1.XubeLog.getInstance());
|
|
29
|
+
return {
|
|
30
|
+
body: JSON.stringify(response),
|
|
31
|
+
statusCode: response.statusCode,
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
exports.handler = handler;
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { CorsOptions, IAuthorizer, IDomainName, RestApi } from "aws-cdk-lib/aws-apigateway";
|
|
2
|
+
import { ICertificate } from "aws-cdk-lib/aws-certificatemanager";
|
|
3
|
+
import { ITable } from "aws-cdk-lib/aws-dynamodb";
|
|
4
|
+
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";
|
|
5
|
+
import { Construct } from "constructs";
|
|
6
|
+
export declare const ADD_WEBHOOK_ENDPOINT_FUNCTION_NAME = "add-webhook-endpoint";
|
|
7
|
+
export declare const REMOVE_WEBHOOK_ENDPOINT_FUNCTION_NAME = "remove-webhook-endpoint";
|
|
8
|
+
export declare const GET_WEBHOOK_ENDPOINTS_FUNCTION_NAME = "get-webhook-endpoints";
|
|
9
|
+
export interface WebhookManagementProps {
|
|
10
|
+
table?: ITable;
|
|
11
|
+
name?: string;
|
|
12
|
+
domainName: IDomainName;
|
|
13
|
+
certificate: ICertificate;
|
|
14
|
+
authorizer: IAuthorizer;
|
|
15
|
+
basePath?: string;
|
|
16
|
+
stage?: string;
|
|
17
|
+
corsOptions?: CorsOptions;
|
|
18
|
+
}
|
|
19
|
+
export declare class WebhookManagement extends Construct {
|
|
20
|
+
table: ITable;
|
|
21
|
+
addWebhookEndpoint: NodejsFunction;
|
|
22
|
+
removeWebhookEndpoint: NodejsFunction;
|
|
23
|
+
getWebhookEndpoints: NodejsFunction;
|
|
24
|
+
webhookAPI: RestApi;
|
|
25
|
+
constructor(scope: Construct, id: string, props: WebhookManagementProps);
|
|
26
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WebhookManagement = exports.GET_WEBHOOK_ENDPOINTS_FUNCTION_NAME = exports.REMOVE_WEBHOOK_ENDPOINT_FUNCTION_NAME = exports.ADD_WEBHOOK_ENDPOINT_FUNCTION_NAME = void 0;
|
|
4
|
+
const kit_aws_1 = require("@xube/kit-aws");
|
|
5
|
+
const kit_aws_2 = require("@xube/kit-aws");
|
|
6
|
+
const aws_apigateway_1 = require("aws-cdk-lib/aws-apigateway");
|
|
7
|
+
const aws_dynamodb_1 = require("aws-cdk-lib/aws-dynamodb");
|
|
8
|
+
const aws_lambda_1 = require("aws-cdk-lib/aws-lambda");
|
|
9
|
+
const aws_lambda_nodejs_1 = require("aws-cdk-lib/aws-lambda-nodejs");
|
|
10
|
+
const constructs_1 = require("constructs");
|
|
11
|
+
exports.ADD_WEBHOOK_ENDPOINT_FUNCTION_NAME = "add-webhook-endpoint";
|
|
12
|
+
exports.REMOVE_WEBHOOK_ENDPOINT_FUNCTION_NAME = "remove-webhook-endpoint";
|
|
13
|
+
exports.GET_WEBHOOK_ENDPOINTS_FUNCTION_NAME = "get-webhook-endpoints";
|
|
14
|
+
class WebhookManagement extends constructs_1.Construct {
|
|
15
|
+
table;
|
|
16
|
+
addWebhookEndpoint;
|
|
17
|
+
removeWebhookEndpoint;
|
|
18
|
+
getWebhookEndpoints;
|
|
19
|
+
webhookAPI;
|
|
20
|
+
constructor(scope, id, props) {
|
|
21
|
+
super(scope, id);
|
|
22
|
+
this.table =
|
|
23
|
+
props.table ??
|
|
24
|
+
new aws_dynamodb_1.Table(scope, id + "-table", {
|
|
25
|
+
tableName: (props.name ?? `webhook-management`) + "-table",
|
|
26
|
+
partitionKey: {
|
|
27
|
+
name: kit_aws_2.PARTITION_KEY,
|
|
28
|
+
type: aws_dynamodb_1.AttributeType.STRING,
|
|
29
|
+
},
|
|
30
|
+
sortKey: {
|
|
31
|
+
name: kit_aws_1.SORT_KEY,
|
|
32
|
+
type: aws_dynamodb_1.AttributeType.STRING,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
this.addWebhookEndpoint = new aws_lambda_nodejs_1.NodejsFunction(this, id + "-add-hook-function", {
|
|
36
|
+
entry: __dirname + "./functions/" + exports.ADD_WEBHOOK_ENDPOINT_FUNCTION_NAME,
|
|
37
|
+
functionName: (props.name ?? "webhook") + "-add-hook",
|
|
38
|
+
environment: {
|
|
39
|
+
WEBHOOK_TABLE_NAME_ENV_VAR: this.table.tableName,
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
this.removeWebhookEndpoint = new aws_lambda_nodejs_1.NodejsFunction(this, id + "-remove-hook-function", {
|
|
43
|
+
entry: __dirname + "./functions/" + exports.REMOVE_WEBHOOK_ENDPOINT_FUNCTION_NAME,
|
|
44
|
+
functionName: (props.name ?? "webhook") + "-remove-hook",
|
|
45
|
+
environment: {
|
|
46
|
+
WEBHOOK_TABLE_NAME_ENV_VAR: this.table.tableName,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
this.getWebhookEndpoints = new aws_lambda_nodejs_1.NodejsFunction(this, id + "-get-hooks-function", {
|
|
50
|
+
entry: __dirname + "./functions/" + exports.GET_WEBHOOK_ENDPOINTS_FUNCTION_NAME,
|
|
51
|
+
functionName: (props.name ?? "webhook") + "-get-hook",
|
|
52
|
+
environment: {
|
|
53
|
+
WEBHOOK_TABLE_NAME_ENV_VAR: this.table.tableName,
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
this.webhookAPI = new aws_apigateway_1.RestApi(this, id + "-api", {
|
|
57
|
+
defaultCorsPreflightOptions: props.corsOptions ?? {
|
|
58
|
+
allowOrigins: ["*"],
|
|
59
|
+
allowCredentials: true,
|
|
60
|
+
allowHeaders: ["*"],
|
|
61
|
+
allowMethods: ["*"],
|
|
62
|
+
},
|
|
63
|
+
domainName: {
|
|
64
|
+
domainName: props.domainName.domainName,
|
|
65
|
+
certificate: props.certificate,
|
|
66
|
+
basePath: props.basePath ?? "webhooks",
|
|
67
|
+
},
|
|
68
|
+
restApiName: props.name,
|
|
69
|
+
defaultMethodOptions: {
|
|
70
|
+
authorizer: props.authorizer,
|
|
71
|
+
},
|
|
72
|
+
deployOptions: {
|
|
73
|
+
stageName: props.stage ?? "production",
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
const addWebhookEndpointTarget = new aws_apigateway_1.LambdaIntegration(this.addWebhookEndpoint);
|
|
77
|
+
const removeWebhookEndpointTarget = new aws_apigateway_1.LambdaIntegration(this.removeWebhookEndpoint);
|
|
78
|
+
const getWebhookEndpointsTarget = new aws_apigateway_1.LambdaIntegration(this.getWebhookEndpoints);
|
|
79
|
+
this.webhookAPI.root.addMethod(aws_lambda_1.HttpMethod.GET, getWebhookEndpointsTarget);
|
|
80
|
+
this.webhookAPI.root.addMethod(aws_lambda_1.HttpMethod.POST, addWebhookEndpointTarget);
|
|
81
|
+
this.webhookAPI.root.addMethod(aws_lambda_1.HttpMethod.DELETE, removeWebhookEndpointTarget);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
exports.WebhookManagement = WebhookManagement;
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xube/kit-aws-hooks-infrastructure",
|
|
3
|
+
"version": "0.0.22",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+ssh://git@github.com/XubeLtd/dev-kit.git"
|
|
12
|
+
},
|
|
13
|
+
"author": "Xube Pty Ltd",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/XubeLtd/dev-kit/issues"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://github.com/XubeLtd/dev-kit#readme",
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@xube/kit-build": "^0.0.22"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@xube/kit-aws": "^0.0.22",
|
|
24
|
+
"@xube/kit-aws-hooks": "^0.0.22",
|
|
25
|
+
"@xube/kit-aws-infrastructure": "^0.0.22",
|
|
26
|
+
"@xube/kit-constants": "^0.0.22",
|
|
27
|
+
"@xube/kit-log": "^0.0.22",
|
|
28
|
+
"aws-cdk-lib": "^2.100.0",
|
|
29
|
+
"aws-lambda": "^1.0.7",
|
|
30
|
+
"constructs": "^10.3.0"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createWebhookForAccount,
|
|
3
|
+
isCreateWebhookForAccountRequest,
|
|
4
|
+
} from "@xube/kit-aws-hooks";
|
|
5
|
+
import { StatusCode } from "@xube/kit-constants";
|
|
6
|
+
import { XubeLog } from "@xube/kit-log";
|
|
7
|
+
import {
|
|
8
|
+
APIGatewayProxyResult,
|
|
9
|
+
APIGatewayProxyWithCognitoAuthorizerEvent,
|
|
10
|
+
} from "aws-lambda";
|
|
11
|
+
|
|
12
|
+
export const webhookTableName = process.env.WEBHOOK_TABLE_NAME_ENV_VAR || "";
|
|
13
|
+
|
|
14
|
+
export const handler = async (
|
|
15
|
+
event: APIGatewayProxyWithCognitoAuthorizerEvent
|
|
16
|
+
): Promise<APIGatewayProxyResult> => {
|
|
17
|
+
if (!webhookTableName) {
|
|
18
|
+
return {
|
|
19
|
+
body: `Could not find webhook table name in environment variables`,
|
|
20
|
+
statusCode: StatusCode.InternalError,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!event.body) {
|
|
25
|
+
return {
|
|
26
|
+
body: `No body provided`,
|
|
27
|
+
statusCode: StatusCode.BadRequest,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const request: unknown = JSON.parse(event.body);
|
|
32
|
+
|
|
33
|
+
if (!isCreateWebhookForAccountRequest(request)) {
|
|
34
|
+
return {
|
|
35
|
+
body: `Request body is not a valid Webhook Endpoint creation request`,
|
|
36
|
+
statusCode: StatusCode.BadRequest,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const response = await createWebhookForAccount(
|
|
41
|
+
request,
|
|
42
|
+
webhookTableName,
|
|
43
|
+
XubeLog.getInstance()
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
body: JSON.stringify(response),
|
|
48
|
+
statusCode: response.statusCode,
|
|
49
|
+
};
|
|
50
|
+
};
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { SORT_KEY } from "@xube/kit-aws";
|
|
2
|
+
import { PARTITION_KEY } from "@xube/kit-aws";
|
|
3
|
+
import { WEBHOOK_TABLE_NAME_ENV_VAR } from "@xube/kit-aws-hooks";
|
|
4
|
+
import {
|
|
5
|
+
CorsOptions,
|
|
6
|
+
IAuthorizer,
|
|
7
|
+
IDomainName,
|
|
8
|
+
LambdaIntegration,
|
|
9
|
+
RestApi,
|
|
10
|
+
} from "aws-cdk-lib/aws-apigateway";
|
|
11
|
+
import { ICertificate } from "aws-cdk-lib/aws-certificatemanager";
|
|
12
|
+
import { AttributeType, ITable, Table } from "aws-cdk-lib/aws-dynamodb";
|
|
13
|
+
import { HttpMethod } from "aws-cdk-lib/aws-lambda";
|
|
14
|
+
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";
|
|
15
|
+
import { Construct } from "constructs";
|
|
16
|
+
|
|
17
|
+
export const ADD_WEBHOOK_ENDPOINT_FUNCTION_NAME = "add-webhook-endpoint";
|
|
18
|
+
export const REMOVE_WEBHOOK_ENDPOINT_FUNCTION_NAME = "remove-webhook-endpoint";
|
|
19
|
+
export const GET_WEBHOOK_ENDPOINTS_FUNCTION_NAME = "get-webhook-endpoints";
|
|
20
|
+
|
|
21
|
+
export interface WebhookManagementProps {
|
|
22
|
+
table?: ITable;
|
|
23
|
+
name?: string;
|
|
24
|
+
domainName: IDomainName;
|
|
25
|
+
certificate: ICertificate;
|
|
26
|
+
authorizer: IAuthorizer;
|
|
27
|
+
basePath?: string;
|
|
28
|
+
stage?: string;
|
|
29
|
+
corsOptions?: CorsOptions;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class WebhookManagement extends Construct {
|
|
33
|
+
table: ITable;
|
|
34
|
+
addWebhookEndpoint: NodejsFunction;
|
|
35
|
+
removeWebhookEndpoint: NodejsFunction;
|
|
36
|
+
getWebhookEndpoints: NodejsFunction;
|
|
37
|
+
|
|
38
|
+
webhookAPI: RestApi;
|
|
39
|
+
|
|
40
|
+
constructor(scope: Construct, id: string, props: WebhookManagementProps) {
|
|
41
|
+
super(scope, id);
|
|
42
|
+
|
|
43
|
+
this.table =
|
|
44
|
+
props.table ??
|
|
45
|
+
new Table(scope, id + "-table", {
|
|
46
|
+
tableName: (props.name ?? `webhook-management`) + "-table",
|
|
47
|
+
partitionKey: {
|
|
48
|
+
name: PARTITION_KEY,
|
|
49
|
+
type: AttributeType.STRING,
|
|
50
|
+
},
|
|
51
|
+
sortKey: {
|
|
52
|
+
name: SORT_KEY,
|
|
53
|
+
type: AttributeType.STRING,
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
this.addWebhookEndpoint = new NodejsFunction(
|
|
58
|
+
this,
|
|
59
|
+
id + "-add-hook-function",
|
|
60
|
+
{
|
|
61
|
+
entry: __dirname + "./functions/" + ADD_WEBHOOK_ENDPOINT_FUNCTION_NAME,
|
|
62
|
+
functionName: (props.name ?? "webhook") + "-add-hook",
|
|
63
|
+
environment: {
|
|
64
|
+
WEBHOOK_TABLE_NAME_ENV_VAR: this.table.tableName,
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
this.removeWebhookEndpoint = new NodejsFunction(
|
|
69
|
+
this,
|
|
70
|
+
id + "-remove-hook-function",
|
|
71
|
+
{
|
|
72
|
+
entry:
|
|
73
|
+
__dirname + "./functions/" + REMOVE_WEBHOOK_ENDPOINT_FUNCTION_NAME,
|
|
74
|
+
functionName: (props.name ?? "webhook") + "-remove-hook",
|
|
75
|
+
environment: {
|
|
76
|
+
WEBHOOK_TABLE_NAME_ENV_VAR: this.table.tableName,
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
this.getWebhookEndpoints = new NodejsFunction(
|
|
81
|
+
this,
|
|
82
|
+
id + "-get-hooks-function",
|
|
83
|
+
{
|
|
84
|
+
entry: __dirname + "./functions/" + GET_WEBHOOK_ENDPOINTS_FUNCTION_NAME,
|
|
85
|
+
functionName: (props.name ?? "webhook") + "-get-hook",
|
|
86
|
+
environment: {
|
|
87
|
+
WEBHOOK_TABLE_NAME_ENV_VAR: this.table.tableName,
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
this.webhookAPI = new RestApi(this, id + "-api", {
|
|
93
|
+
defaultCorsPreflightOptions: props.corsOptions ?? {
|
|
94
|
+
allowOrigins: ["*"],
|
|
95
|
+
allowCredentials: true,
|
|
96
|
+
allowHeaders: ["*"],
|
|
97
|
+
allowMethods: ["*"],
|
|
98
|
+
},
|
|
99
|
+
domainName: {
|
|
100
|
+
domainName: props.domainName.domainName,
|
|
101
|
+
certificate: props.certificate,
|
|
102
|
+
basePath: props.basePath ?? "webhooks",
|
|
103
|
+
},
|
|
104
|
+
restApiName: props.name,
|
|
105
|
+
defaultMethodOptions: {
|
|
106
|
+
authorizer: props.authorizer,
|
|
107
|
+
},
|
|
108
|
+
deployOptions: {
|
|
109
|
+
stageName: props.stage ?? "production",
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const addWebhookEndpointTarget = new LambdaIntegration(
|
|
114
|
+
this.addWebhookEndpoint
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const removeWebhookEndpointTarget = new LambdaIntegration(
|
|
118
|
+
this.removeWebhookEndpoint
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const getWebhookEndpointsTarget = new LambdaIntegration(
|
|
122
|
+
this.getWebhookEndpoints
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
this.webhookAPI.root.addMethod(HttpMethod.GET, getWebhookEndpointsTarget);
|
|
126
|
+
this.webhookAPI.root.addMethod(HttpMethod.POST, addWebhookEndpointTarget);
|
|
127
|
+
this.webhookAPI.root.addMethod(
|
|
128
|
+
HttpMethod.DELETE,
|
|
129
|
+
removeWebhookEndpointTarget
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../kit-build/tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": "./src",
|
|
5
|
+
"outDir": "./dist"
|
|
6
|
+
},
|
|
7
|
+
"exclude": ["**/*.test.ts", "**/*.mock.ts", "dist"],
|
|
8
|
+
"references": [
|
|
9
|
+
{
|
|
10
|
+
"path": "../kit-constants"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"path": "../kit-log"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"path": "../kit-aws"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"path": "../kit-aws-hooks"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"path": "../kit-aws-infrastructure"
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
|