wirejs-deploy-amplify-basic 0.0.78-table-resource → 0.0.80-table-resource
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/amplify-backend-assets/backend.ts +20 -15
- package/amplify-backend-assets/constructs/table-indexes/handler.ts +115 -0
- package/amplify-backend-assets/constructs/table-indexes/index.ts +36 -0
- package/amplify-hosting-assets/compute/default/package.json +1 -1
- package/build.js +1 -0
- package/package.json +2 -2
|
@@ -8,9 +8,11 @@ import { Table, AttributeType, BillingMode } from 'aws-cdk-lib/aws-dynamodb';
|
|
|
8
8
|
import { api } from './functions/api/resource';
|
|
9
9
|
import { auth } from './auth/resource';
|
|
10
10
|
import { TableDefinition, indexName } from 'wirejs-resources';
|
|
11
|
+
import { TableIndexes } from './constructs/table-indexes';
|
|
11
12
|
|
|
12
13
|
// @ts-ignore
|
|
13
14
|
import generated from './generated-resources';
|
|
15
|
+
import { Key } from 'aws-cdk-lib/aws-kms';
|
|
14
16
|
|
|
15
17
|
const APP_ID = process.env.AWS_APP_ID ?? process.env.PWD?.replace(/[^a-zA-Z0-9-_]/g, '_');
|
|
16
18
|
const BRANCH_ID = process.env.AWS_BRANCH ?? process.env.USER ?? 'anonymous';
|
|
@@ -72,8 +74,10 @@ const PKFieldTypes = {
|
|
|
72
74
|
for (const resource of generated) {
|
|
73
75
|
if (isDistributedTable(resource)) {
|
|
74
76
|
const sanitizedId = resource.options.absoluteId.replace(/[^a-zA-Z0-9-_]/g, '_');
|
|
77
|
+
const tableName = `${TABLE_NAME_PREFIX}${sanitizedId}`
|
|
75
78
|
|
|
76
79
|
const table = new Table(backend.stack, sanitizedId, {
|
|
80
|
+
tableName,
|
|
77
81
|
partitionKey: {
|
|
78
82
|
name: resource.options.partitionKey.field,
|
|
79
83
|
type: PKFieldTypes[resource.options.partitionKey.type],
|
|
@@ -83,25 +87,26 @@ for (const resource of generated) {
|
|
|
83
87
|
type: PKFieldTypes[resource.options.sortKey.type],
|
|
84
88
|
} : undefined,
|
|
85
89
|
removalPolicy: RemovalPolicy.RETAIN,
|
|
86
|
-
tableName: `${TABLE_NAME_PREFIX}${sanitizedId}`,
|
|
87
90
|
billingMode: BillingMode.PAY_PER_REQUEST,
|
|
88
91
|
pointInTimeRecovery: true,
|
|
89
92
|
});
|
|
90
93
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
94
|
+
new TableIndexes(backend.stack, `${tableName}Indexes`,
|
|
95
|
+
{
|
|
96
|
+
tableName,
|
|
97
|
+
indexes: (resource.options.indexes ?? []).map((index) => ({
|
|
98
|
+
indexName: indexName(index as any),
|
|
99
|
+
partitionKey: {
|
|
100
|
+
name: index.partition.field,
|
|
101
|
+
type: PKFieldTypes[index.partition.type as keyof typeof PKFieldTypes],
|
|
102
|
+
},
|
|
103
|
+
sortKey: index.sort ? {
|
|
104
|
+
name: index.sort.field,
|
|
105
|
+
type: PKFieldTypes[index.sort.type as keyof typeof PKFieldTypes],
|
|
106
|
+
} : undefined,
|
|
107
|
+
}))
|
|
108
|
+
}
|
|
109
|
+
);
|
|
105
110
|
|
|
106
111
|
table.grantReadWriteData(backend.api.resources.lambda);
|
|
107
112
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DynamoDBClient,
|
|
3
|
+
UpdateTableCommand,
|
|
4
|
+
DescribeTableCommand,
|
|
5
|
+
} from '@aws-sdk/client-dynamodb';
|
|
6
|
+
import { GlobalSecondaryIndexProps } from 'aws-cdk-lib/aws-dynamodb';
|
|
7
|
+
import { CloudFormationCustomResourceEvent } from 'aws-lambda';
|
|
8
|
+
|
|
9
|
+
const ddb = new DynamoDBClient({});
|
|
10
|
+
|
|
11
|
+
function sleep(ms: number) {
|
|
12
|
+
return new Promise((res) => setTimeout(res, ms));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Polls DDB until status matches the desired status.
|
|
17
|
+
*
|
|
18
|
+
* @param tableName
|
|
19
|
+
* @param indexName
|
|
20
|
+
* @param desiredStatus
|
|
21
|
+
*/
|
|
22
|
+
async function waitForGsiStatus(
|
|
23
|
+
tableName: string,
|
|
24
|
+
indexName: string,
|
|
25
|
+
desiredStatus: string
|
|
26
|
+
) {
|
|
27
|
+
while (true) {
|
|
28
|
+
const table = await ddb.send(new DescribeTableCommand({ TableName: tableName }));
|
|
29
|
+
const gsis = table.Table?.GlobalSecondaryIndexes ?? [];
|
|
30
|
+
const index = gsis.find((gsi) => gsi.IndexName === indexName);
|
|
31
|
+
if (!index && desiredStatus === 'DELETED') break;
|
|
32
|
+
if (index?.IndexStatus === desiredStatus) break;
|
|
33
|
+
await sleep(5000);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const handler = async (event: CloudFormationCustomResourceEvent) => {
|
|
38
|
+
console.log('Event:', JSON.stringify(event));
|
|
39
|
+
|
|
40
|
+
const tableName = event.ResourceProperties.TableName;
|
|
41
|
+
const desiredGsis: GlobalSecondaryIndexProps[] =
|
|
42
|
+
event.ResourceProperties.GlobalSecondaryIndexes || [];
|
|
43
|
+
const desiredNames = new Set(desiredGsis.map((gsi) => gsi.indexName));
|
|
44
|
+
|
|
45
|
+
const tableDesc = await ddb.send(new DescribeTableCommand({ TableName: tableName }));
|
|
46
|
+
const existingGsis = tableDesc.Table?.GlobalSecondaryIndexes ?? [];
|
|
47
|
+
const existingNames = new Set(existingGsis.map((gsi) => gsi.IndexName));
|
|
48
|
+
|
|
49
|
+
if (event.RequestType === 'Delete') {
|
|
50
|
+
// table-level delete will take care of deleting everything.
|
|
51
|
+
return { PhysicalResourceId: tableName };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Delete unwanted GSIs first.
|
|
55
|
+
for (const gsi of existingGsis) {
|
|
56
|
+
if (!gsi.IndexName) {
|
|
57
|
+
console.warn('"Existing" GSI without IndexName found (1)', gsi);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (!desiredNames.has(gsi.IndexName)) {
|
|
61
|
+
console.log(`Deleting GSI: ${gsi.IndexName}`);
|
|
62
|
+
await ddb.send(new UpdateTableCommand({
|
|
63
|
+
TableName: tableName,
|
|
64
|
+
GlobalSecondaryIndexUpdates: [
|
|
65
|
+
{ Delete: { IndexName: gsi.IndexName } },
|
|
66
|
+
],
|
|
67
|
+
}));
|
|
68
|
+
await waitForGsiStatus(tableName, gsi.IndexName, 'DELETED');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Add new GSIs
|
|
73
|
+
for (const gsi of desiredGsis) {
|
|
74
|
+
if (!existingNames.has(gsi.indexName)) {
|
|
75
|
+
console.log(`Creating GSI: ${gsi.indexName}`);
|
|
76
|
+
await ddb.send(new UpdateTableCommand({
|
|
77
|
+
TableName: tableName,
|
|
78
|
+
AttributeDefinitions: [
|
|
79
|
+
{
|
|
80
|
+
AttributeName: gsi.partitionKey.name,
|
|
81
|
+
AttributeType: gsi.partitionKey.type,
|
|
82
|
+
},
|
|
83
|
+
...(gsi.sortKey ? [{
|
|
84
|
+
AttributeName: gsi.sortKey.name,
|
|
85
|
+
AttributeType: gsi.sortKey.type,
|
|
86
|
+
}] : []),
|
|
87
|
+
],
|
|
88
|
+
GlobalSecondaryIndexUpdates: [{
|
|
89
|
+
Create: {
|
|
90
|
+
IndexName: gsi.indexName,
|
|
91
|
+
KeySchema: [
|
|
92
|
+
{
|
|
93
|
+
AttributeName: gsi.partitionKey.name,
|
|
94
|
+
KeyType: 'HASH'
|
|
95
|
+
},
|
|
96
|
+
...(gsi.sortKey ? [{
|
|
97
|
+
AttributeName: gsi.sortKey.name,
|
|
98
|
+
KeyType: 'RANGE' as const
|
|
99
|
+
}] : []),
|
|
100
|
+
],
|
|
101
|
+
Projection: {
|
|
102
|
+
ProjectionType: gsi.projectionType,
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}],
|
|
106
|
+
}));
|
|
107
|
+
await waitForGsiStatus(tableName, gsi.indexName, 'ACTIVE');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
PhysicalResourceId: tableName,
|
|
113
|
+
Data: { Message: 'GSIs synced successfully' },
|
|
114
|
+
};
|
|
115
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Duration, CustomResource } from 'aws-cdk-lib';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as lambda from 'aws-cdk-lib/aws-lambda';
|
|
4
|
+
import * as cr from 'aws-cdk-lib/custom-resources';
|
|
5
|
+
import { GlobalSecondaryIndexProps } from 'aws-cdk-lib/aws-dynamodb';
|
|
6
|
+
import { Construct } from 'constructs';
|
|
7
|
+
|
|
8
|
+
export interface TableIndexesProps {
|
|
9
|
+
tableName: string;
|
|
10
|
+
indexes: GlobalSecondaryIndexProps[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class TableIndexes extends Construct {
|
|
14
|
+
constructor(scope: Construct, id: string, props: TableIndexesProps) {
|
|
15
|
+
super(scope, id);
|
|
16
|
+
|
|
17
|
+
const onEvent = new lambda.Function(this, 'TableIndexesHandler', {
|
|
18
|
+
runtime: lambda.Runtime.NODEJS_22_X,
|
|
19
|
+
handler: 'index.handler',
|
|
20
|
+
code: lambda.Code.fromAsset(path.join(__dirname, 'handler')),
|
|
21
|
+
timeout: Duration.minutes(15),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const provider = new cr.Provider(this, 'TableIndexesProvider', {
|
|
25
|
+
onEventHandler: onEvent,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
new CustomResource(this, 'TableIndexesCustomResource', {
|
|
29
|
+
serviceToken: provider.serviceToken,
|
|
30
|
+
properties: {
|
|
31
|
+
TableName: props.tableName,
|
|
32
|
+
GlobalSecondaryIndexes: props.indexes,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
package/build.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wirejs-deploy-amplify-basic",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.80-table-resource",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"recursive-copy": "^2.0.14",
|
|
33
33
|
"rimraf": "^6.0.1",
|
|
34
34
|
"wirejs-dom": "^1.0.38",
|
|
35
|
-
"wirejs-resources": "^0.1.
|
|
35
|
+
"wirejs-resources": "^0.1.48-table-resource"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@aws-amplify/backend": "^1.14.0",
|