@auth-craft/aws-cf-stack 1.9.0 → 1.10.0
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/.env.example +3 -0
- package/README.md +45 -4
- package/assets/admin/cli.mjs +83 -83
- package/assets/lambda/index.mjs +52 -55
- package/bin/deploy.sh +4 -0
- package/cdk/bin/app.ts +12 -0
- package/cdk/lib/dynamodb-stack.ts +59 -90
- package/cdk/lib/table-schema.json +192 -0
- package/lib/deploy-lambda.sh +4 -0
- package/lib/sync-table-gsis.sh +257 -0
- package/package.json +12 -8
package/bin/deploy.sh
CHANGED
|
@@ -17,6 +17,8 @@ export PKG_ROOT
|
|
|
17
17
|
source "$PKG_ROOT/lib/common.sh"
|
|
18
18
|
# shellcheck source=../lib/deploy-lambda.sh
|
|
19
19
|
source "$PKG_ROOT/lib/deploy-lambda.sh"
|
|
20
|
+
# shellcheck source=../lib/sync-table-gsis.sh
|
|
21
|
+
source "$PKG_ROOT/lib/sync-table-gsis.sh"
|
|
20
22
|
# shellcheck source=../lib/create-admin.sh
|
|
21
23
|
source "$PKG_ROOT/lib/create-admin.sh"
|
|
22
24
|
# shellcheck source=../lib/deploy-gateway.sh
|
|
@@ -31,6 +33,8 @@ Usage:
|
|
|
31
33
|
|
|
32
34
|
Commands:
|
|
33
35
|
lambda Deploy DynamoDB + Lambda (CDK). Prints FunctionUrl, ApiBasePath, table.
|
|
36
|
+
An existing table missing GSIs gets them first, one per update
|
|
37
|
+
(needs dynamodb:DescribeTable + cloudformation:GetTemplate).
|
|
34
38
|
Use --outputs-file to capture CDK outputs as JSON for your own steps.
|
|
35
39
|
gateway Deploy the 3 gateway workers (system/tenant/customer) via wrangler,
|
|
36
40
|
wiring BACKEND_URL/base-path/secrets from the live CDK outputs. Reads
|
package/cdk/bin/app.ts
CHANGED
|
@@ -49,6 +49,17 @@ const costTags = {
|
|
|
49
49
|
// Stack / resource naming helper: {app}-{env}-{layer}-{component}
|
|
50
50
|
const nameFor = (component: string) => `${resourceBase}-${component}`;
|
|
51
51
|
|
|
52
|
+
// GSI rollout: while adding missing GSIs to an existing table one per update, the deploy CLI
|
|
53
|
+
// passes the subset as CDK context (`-c dataGsis=A,B,...`) on that one command line. It is
|
|
54
|
+
// deliberately not an env var: an exported value (shell, --env-file) would leak into the
|
|
55
|
+
// full deploy and make CloudFormation drop every unlisted GSI. Absent or blank = every GSI.
|
|
56
|
+
const dataGsiContext: unknown = app.node.tryGetContext('dataGsis');
|
|
57
|
+
const dataGsiList = (typeof dataGsiContext === 'string' ? dataGsiContext : '')
|
|
58
|
+
.split(',')
|
|
59
|
+
.map((name) => name.trim())
|
|
60
|
+
.filter(Boolean);
|
|
61
|
+
const dataGsis = dataGsiList.length > 0 ? dataGsiList : undefined;
|
|
62
|
+
|
|
52
63
|
// DynamoDB Stack (data component)
|
|
53
64
|
const dynamodbStack = new DynamoDBStack(app, nameFor('data'), {
|
|
54
65
|
env,
|
|
@@ -57,6 +68,7 @@ const dynamodbStack = new DynamoDBStack(app, nameFor('data'), {
|
|
|
57
68
|
tags: costTags,
|
|
58
69
|
tableName: nameFor('data'),
|
|
59
70
|
stage,
|
|
71
|
+
...(dataGsis ? { gsiNames: dataGsis } : {}),
|
|
60
72
|
});
|
|
61
73
|
|
|
62
74
|
// Lambda Stack (api component)
|
|
@@ -1,10 +1,52 @@
|
|
|
1
1
|
import * as cdk from 'aws-cdk-lib';
|
|
2
2
|
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
|
|
3
3
|
import type { Construct } from 'constructs';
|
|
4
|
+
import tableSchema from './table-schema.json' with { type: 'json' };
|
|
4
5
|
|
|
5
6
|
interface DynamoDBStackProps extends cdk.StackProps {
|
|
6
7
|
tableName: string; // {app}-{env}-{layer}-data, e.g. "snapshot-commerce-prod-auth-data"
|
|
7
8
|
stage: string; // "dev" | "staging" | "prod" — passed from the app, single source of truth
|
|
9
|
+
/**
|
|
10
|
+
* Only declare these GSIs (schema order kept). Used by the deploy CLI to add missing
|
|
11
|
+
* GSIs to an existing table one per update — CloudFormation rejects more than one GSI
|
|
12
|
+
* creation per table update. Omit to declare every GSI in the schema.
|
|
13
|
+
*/
|
|
14
|
+
gsiNames?: readonly string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type KeyAttribute = { name: string; type: string };
|
|
18
|
+
type GsiDefinition = (typeof tableSchema.globalSecondaryIndexes)[number];
|
|
19
|
+
|
|
20
|
+
// The schema is generated from @auth-craft/database-plugin-dynamodb (TABLE_SCHEMA) by
|
|
21
|
+
// scripts/sync-table-schema.mjs — edit it there, never here.
|
|
22
|
+
export const TABLE_GSI_NAMES: readonly string[] = tableSchema.globalSecondaryIndexes.map((gsi) => gsi.indexName);
|
|
23
|
+
|
|
24
|
+
function toAttribute(key: KeyAttribute): dynamodb.Attribute {
|
|
25
|
+
const types: Record<string, dynamodb.AttributeType> = {
|
|
26
|
+
S: dynamodb.AttributeType.STRING,
|
|
27
|
+
N: dynamodb.AttributeType.NUMBER,
|
|
28
|
+
B: dynamodb.AttributeType.BINARY,
|
|
29
|
+
};
|
|
30
|
+
const type = types[key.type];
|
|
31
|
+
if (!type) throw new Error(`Unknown attribute type "${key.type}" for ${key.name}`);
|
|
32
|
+
return { name: key.name, type };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function toGsiProps(gsi: GsiDefinition): dynamodb.GlobalSecondaryIndexProps {
|
|
36
|
+
const sortKey = 'sortKey' in gsi && gsi.sortKey ? { sortKey: toAttribute(gsi.sortKey) } : {};
|
|
37
|
+
const projection = gsi.projection as { type: string; nonKeyAttributes?: string[] };
|
|
38
|
+
const projectionProps: Pick<dynamodb.GlobalSecondaryIndexProps, 'projectionType' | 'nonKeyAttributes'> =
|
|
39
|
+
projection.type === 'INCLUDE'
|
|
40
|
+
? { projectionType: dynamodb.ProjectionType.INCLUDE, nonKeyAttributes: projection.nonKeyAttributes ?? [] }
|
|
41
|
+
: projection.type === 'KEYS_ONLY'
|
|
42
|
+
? { projectionType: dynamodb.ProjectionType.KEYS_ONLY }
|
|
43
|
+
: { projectionType: dynamodb.ProjectionType.ALL };
|
|
44
|
+
return {
|
|
45
|
+
indexName: gsi.indexName,
|
|
46
|
+
partitionKey: toAttribute(gsi.partitionKey),
|
|
47
|
+
...sortKey,
|
|
48
|
+
...projectionProps,
|
|
49
|
+
};
|
|
8
50
|
}
|
|
9
51
|
|
|
10
52
|
export class DynamoDBStack extends cdk.Stack {
|
|
@@ -13,108 +55,35 @@ export class DynamoDBStack extends cdk.Stack {
|
|
|
13
55
|
constructor(scope: Construct, id: string, props: DynamoDBStackProps) {
|
|
14
56
|
super(scope, id, props);
|
|
15
57
|
|
|
16
|
-
const { tableName, stage } = props;
|
|
58
|
+
const { tableName, stage, gsiNames } = props;
|
|
17
59
|
|
|
18
60
|
// Table naming: {app}-{env}-{layer}-data
|
|
19
61
|
// Examples: snapshot-commerce-prod-auth-data, auth-craft-dev-auth-data
|
|
20
62
|
|
|
21
63
|
this.table = new dynamodb.Table(this, 'AuthCraftTable', {
|
|
22
64
|
tableName,
|
|
23
|
-
partitionKey:
|
|
24
|
-
sortKey:
|
|
65
|
+
partitionKey: toAttribute(tableSchema.partitionKey),
|
|
66
|
+
sortKey: toAttribute(tableSchema.sortKey),
|
|
25
67
|
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, // On-demand billing
|
|
26
|
-
timeToLiveAttribute:
|
|
68
|
+
timeToLiveAttribute: tableSchema.ttlAttribute, // Enable TTL for automatic expiration
|
|
27
69
|
pointInTimeRecoverySpecification: { pointInTimeRecoveryEnabled: stage === 'prod' }, // Enable backups in production
|
|
28
70
|
removalPolicy: stage === 'prod' ? cdk.RemovalPolicy.RETAIN : cdk.RemovalPolicy.DESTROY,
|
|
29
71
|
deletionProtection: stage === 'prod', // Protect production table from accidental deletion
|
|
30
72
|
});
|
|
31
73
|
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
// GSI_UserSessions: Query all sessions for a user
|
|
46
|
-
// PK: USR#{userId}, SK: {createdAtMillis}#{sessionId}
|
|
47
|
-
this.table.addGlobalSecondaryIndex({
|
|
48
|
-
indexName: 'GSI_UserSessions',
|
|
49
|
-
partitionKey: { name: 'userSessionPk', type: dynamodb.AttributeType.STRING },
|
|
50
|
-
sortKey: { name: 'userSessionSk', type: dynamodb.AttributeType.STRING },
|
|
51
|
-
projectionType: dynamodb.ProjectionType.KEYS_ONLY,
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
// GSI_UsersByStatus: List users by status (active/inactive)
|
|
55
|
-
// PK: USR#ACTIVE or USR#INACTIVE, SK: {userId}
|
|
56
|
-
// INCLUDE: displayName, createdAt
|
|
57
|
-
this.table.addGlobalSecondaryIndex({
|
|
58
|
-
indexName: 'GSI_UsersByStatus',
|
|
59
|
-
partitionKey: { name: 'userStatusPk', type: dynamodb.AttributeType.STRING },
|
|
60
|
-
sortKey: { name: 'userStatusSk', type: dynamodb.AttributeType.STRING },
|
|
61
|
-
projectionType: dynamodb.ProjectionType.INCLUDE,
|
|
62
|
-
nonKeyAttributes: ['displayName', 'createdAt'],
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
// GSI_UserProviders: Query OAuth providers for a user
|
|
66
|
-
// PK: USR#{userId}, SK: PRV#{provider}#{externalId}
|
|
67
|
-
this.table.addGlobalSecondaryIndex({
|
|
68
|
-
indexName: 'GSI_UserProviders',
|
|
69
|
-
partitionKey: { name: 'userProviderPk', type: dynamodb.AttributeType.STRING },
|
|
70
|
-
sortKey: { name: 'userProviderSk', type: dynamodb.AttributeType.STRING },
|
|
71
|
-
projectionType: dynamodb.ProjectionType.INCLUDE,
|
|
72
|
-
nonKeyAttributes: ['userId', 'externalEmail', 'externalName', 'createdAt'],
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
// GSI_UserIdentities: Query all identities for a user (SCHEMA_V4)
|
|
76
|
-
// PK: USR#{userId}, SK: IDT#{type}#{value}
|
|
77
|
-
this.table.addGlobalSecondaryIndex({
|
|
78
|
-
indexName: 'GSI_UserIdentities',
|
|
79
|
-
partitionKey: { name: 'identityUserPk', type: dynamodb.AttributeType.STRING },
|
|
80
|
-
sortKey: { name: 'identityUserSk', type: dynamodb.AttributeType.STRING },
|
|
81
|
-
projectionType: dynamodb.ProjectionType.INCLUDE,
|
|
82
|
-
nonKeyAttributes: ['loginEnabled', 'verifiedAt', 'createdAt'],
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
// GSI_UserCredentials: Query all credentials for a user (SCHEMA_V4)
|
|
86
|
-
// PK: USR#{userId}, SK: CRD#{type}#{credentialId}
|
|
87
|
-
// INCLUDE: revokedAt, createdAt
|
|
88
|
-
this.table.addGlobalSecondaryIndex({
|
|
89
|
-
indexName: 'GSI_UserCredentials',
|
|
90
|
-
partitionKey: { name: 'credentialUserPk', type: dynamodb.AttributeType.STRING },
|
|
91
|
-
sortKey: { name: 'credentialUserSk', type: dynamodb.AttributeType.STRING },
|
|
92
|
-
projectionType: dynamodb.ProjectionType.INCLUDE,
|
|
93
|
-
nonKeyAttributes: ['revokedAt', 'createdAt'],
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
// ============================================
|
|
97
|
-
// SCHEMA_V5+ Global Secondary Indexes
|
|
98
|
-
// ============================================
|
|
99
|
-
|
|
100
|
-
// GSI_UserTenants: Query all tenants for a user (SCHEMA_V7)
|
|
101
|
-
// PK: USR#{userId}, SK: {createdAtMillis}#{tenantId}
|
|
102
|
-
this.table.addGlobalSecondaryIndex({
|
|
103
|
-
indexName: 'GSI_UserTenants',
|
|
104
|
-
partitionKey: { name: 'userTenantPk', type: dynamodb.AttributeType.STRING },
|
|
105
|
-
sortKey: { name: 'userTenantSk', type: dynamodb.AttributeType.STRING },
|
|
106
|
-
projectionType: dynamodb.ProjectionType.INCLUDE,
|
|
107
|
-
nonKeyAttributes: ['userStatus', 'role', 'roles', 'perm', 'authPerm', 'isOwner', 'createdAt', 'updatedAt'],
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
// GSI_TenantInvites: Query invites by tenant (SCHEMA_V5)
|
|
111
|
-
// PK: TNT#{tenantId}, SK: {createdAtMillis}#{inviteId}
|
|
112
|
-
this.table.addGlobalSecondaryIndex({
|
|
113
|
-
indexName: 'GSI_TenantInvites',
|
|
114
|
-
partitionKey: { name: 'inviteTenantPk', type: dynamodb.AttributeType.STRING },
|
|
115
|
-
sortKey: { name: 'inviteTenantSk', type: dynamodb.AttributeType.STRING },
|
|
116
|
-
projectionType: dynamodb.ProjectionType.ALL,
|
|
117
|
-
});
|
|
74
|
+
// Global Secondary Indexes — see TABLE_SCHEMA in database-plugin-dynamodb for each
|
|
75
|
+
// index's key layout and access pattern.
|
|
76
|
+
if (gsiNames && gsiNames.length === 0) {
|
|
77
|
+
throw new Error('gsiNames is empty — omit it to declare every GSI (an empty list would delete them all)');
|
|
78
|
+
}
|
|
79
|
+
const unknown = (gsiNames ?? []).filter((name) => !TABLE_GSI_NAMES.includes(name));
|
|
80
|
+
if (unknown.length > 0) {
|
|
81
|
+
throw new Error(`Unknown GSI(s) requested: ${unknown.join(', ')} (schema has ${TABLE_GSI_NAMES.join(', ')})`);
|
|
82
|
+
}
|
|
83
|
+
for (const gsi of tableSchema.globalSecondaryIndexes) {
|
|
84
|
+
if (gsiNames && !gsiNames.includes(gsi.indexName)) continue;
|
|
85
|
+
this.table.addGlobalSecondaryIndex(toGsiProps(gsi));
|
|
86
|
+
}
|
|
118
87
|
|
|
119
88
|
// Outputs
|
|
120
89
|
new cdk.CfnOutput(this, 'TableName', {
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
{
|
|
2
|
+
"partitionKey": {
|
|
3
|
+
"name": "PK",
|
|
4
|
+
"type": "S"
|
|
5
|
+
},
|
|
6
|
+
"sortKey": {
|
|
7
|
+
"name": "SK",
|
|
8
|
+
"type": "S"
|
|
9
|
+
},
|
|
10
|
+
"ttlAttribute": "TTL",
|
|
11
|
+
"globalSecondaryIndexes": [
|
|
12
|
+
{
|
|
13
|
+
"indexName": "GSI_ActiveSessions",
|
|
14
|
+
"partitionKey": {
|
|
15
|
+
"name": "activeSessionPk",
|
|
16
|
+
"type": "S"
|
|
17
|
+
},
|
|
18
|
+
"sortKey": {
|
|
19
|
+
"name": "activeSessionSk",
|
|
20
|
+
"type": "S"
|
|
21
|
+
},
|
|
22
|
+
"projection": {
|
|
23
|
+
"type": "KEYS_ONLY"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"indexName": "GSI_UserSessions",
|
|
28
|
+
"partitionKey": {
|
|
29
|
+
"name": "userSessionPk",
|
|
30
|
+
"type": "S"
|
|
31
|
+
},
|
|
32
|
+
"sortKey": {
|
|
33
|
+
"name": "userSessionSk",
|
|
34
|
+
"type": "S"
|
|
35
|
+
},
|
|
36
|
+
"projection": {
|
|
37
|
+
"type": "KEYS_ONLY"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"indexName": "GSI_UsersByStatus",
|
|
42
|
+
"partitionKey": {
|
|
43
|
+
"name": "userStatusPk",
|
|
44
|
+
"type": "S"
|
|
45
|
+
},
|
|
46
|
+
"sortKey": {
|
|
47
|
+
"name": "userStatusSk",
|
|
48
|
+
"type": "S"
|
|
49
|
+
},
|
|
50
|
+
"projection": {
|
|
51
|
+
"type": "INCLUDE",
|
|
52
|
+
"nonKeyAttributes": [
|
|
53
|
+
"displayName",
|
|
54
|
+
"createdAt"
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"indexName": "GSI_UserProviders",
|
|
60
|
+
"partitionKey": {
|
|
61
|
+
"name": "userProviderPk",
|
|
62
|
+
"type": "S"
|
|
63
|
+
},
|
|
64
|
+
"sortKey": {
|
|
65
|
+
"name": "userProviderSk",
|
|
66
|
+
"type": "S"
|
|
67
|
+
},
|
|
68
|
+
"projection": {
|
|
69
|
+
"type": "INCLUDE",
|
|
70
|
+
"nonKeyAttributes": [
|
|
71
|
+
"userId",
|
|
72
|
+
"externalEmail",
|
|
73
|
+
"externalName",
|
|
74
|
+
"createdAt"
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"indexName": "GSI_UserIdentities",
|
|
80
|
+
"partitionKey": {
|
|
81
|
+
"name": "identityUserPk",
|
|
82
|
+
"type": "S"
|
|
83
|
+
},
|
|
84
|
+
"sortKey": {
|
|
85
|
+
"name": "identityUserSk",
|
|
86
|
+
"type": "S"
|
|
87
|
+
},
|
|
88
|
+
"projection": {
|
|
89
|
+
"type": "INCLUDE",
|
|
90
|
+
"nonKeyAttributes": [
|
|
91
|
+
"loginEnabled",
|
|
92
|
+
"verifiedAt",
|
|
93
|
+
"createdAt"
|
|
94
|
+
]
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"indexName": "GSI_UserCredentials",
|
|
99
|
+
"partitionKey": {
|
|
100
|
+
"name": "credentialUserPk",
|
|
101
|
+
"type": "S"
|
|
102
|
+
},
|
|
103
|
+
"sortKey": {
|
|
104
|
+
"name": "credentialUserSk",
|
|
105
|
+
"type": "S"
|
|
106
|
+
},
|
|
107
|
+
"projection": {
|
|
108
|
+
"type": "INCLUDE",
|
|
109
|
+
"nonKeyAttributes": [
|
|
110
|
+
"revokedAt",
|
|
111
|
+
"createdAt"
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"indexName": "GSI_UserTenants",
|
|
117
|
+
"partitionKey": {
|
|
118
|
+
"name": "userTenantPk",
|
|
119
|
+
"type": "S"
|
|
120
|
+
},
|
|
121
|
+
"sortKey": {
|
|
122
|
+
"name": "userTenantSk",
|
|
123
|
+
"type": "S"
|
|
124
|
+
},
|
|
125
|
+
"projection": {
|
|
126
|
+
"type": "INCLUDE",
|
|
127
|
+
"nonKeyAttributes": [
|
|
128
|
+
"userStatus",
|
|
129
|
+
"role",
|
|
130
|
+
"roles",
|
|
131
|
+
"perm",
|
|
132
|
+
"authPerm",
|
|
133
|
+
"isOwner",
|
|
134
|
+
"createdAt",
|
|
135
|
+
"updatedAt"
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"indexName": "GSI_TenantInvites",
|
|
141
|
+
"partitionKey": {
|
|
142
|
+
"name": "inviteTenantPk",
|
|
143
|
+
"type": "S"
|
|
144
|
+
},
|
|
145
|
+
"sortKey": {
|
|
146
|
+
"name": "inviteTenantSk",
|
|
147
|
+
"type": "S"
|
|
148
|
+
},
|
|
149
|
+
"projection": {
|
|
150
|
+
"type": "ALL"
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"indexName": "GSI_InviteToken",
|
|
155
|
+
"partitionKey": {
|
|
156
|
+
"name": "inviteTokenGsi",
|
|
157
|
+
"type": "S"
|
|
158
|
+
},
|
|
159
|
+
"projection": {
|
|
160
|
+
"type": "KEYS_ONLY"
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
"indexName": "GSI_InviteeEmail",
|
|
165
|
+
"partitionKey": {
|
|
166
|
+
"name": "inviteeEmailGsi",
|
|
167
|
+
"type": "S"
|
|
168
|
+
},
|
|
169
|
+
"sortKey": {
|
|
170
|
+
"name": "inviteeEmailTenantSk",
|
|
171
|
+
"type": "S"
|
|
172
|
+
},
|
|
173
|
+
"projection": {
|
|
174
|
+
"type": "ALL"
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"indexName": "GSI_InviteePhone",
|
|
179
|
+
"partitionKey": {
|
|
180
|
+
"name": "inviteePhoneGsi",
|
|
181
|
+
"type": "S"
|
|
182
|
+
},
|
|
183
|
+
"sortKey": {
|
|
184
|
+
"name": "inviteePhoneTenantSk",
|
|
185
|
+
"type": "S"
|
|
186
|
+
},
|
|
187
|
+
"projection": {
|
|
188
|
+
"type": "ALL"
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
]
|
|
192
|
+
}
|
package/lib/deploy-lambda.sh
CHANGED
|
@@ -25,6 +25,10 @@ deploy_lambda() {
|
|
|
25
25
|
OUTPUTS_FILE="${OUTPUTS_FILE:-$(mktemp -t auth-craft-cdk-outputs.XXXXXX.json)}"
|
|
26
26
|
export OUTPUTS_FILE
|
|
27
27
|
|
|
28
|
+
# Existing tables from older package versions may lack GSIs; CloudFormation adds at most
|
|
29
|
+
# one per update, so add them one at a time before the full deploy below.
|
|
30
|
+
sync_table_gsis
|
|
31
|
+
|
|
28
32
|
info "Deploying CDK stacks (${RESOURCE_NAME}-data, ${api_stack})…"
|
|
29
33
|
(
|
|
30
34
|
cd "$CDK_DIR"
|