source/packages/services/greengrass2-provisioning/src/templates/templates.dao.ts (421 lines of code) (raw):
/*********************************************************************************************************************
* Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. *
* *
* Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance *
* with the License. A copy of the License is located at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES *
* OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions *
* and limitations under the License. *
*********************************************************************************************************************/
import { inject, injectable } from 'inversify';
import clone from 'just-clone';
import ow from 'ow';
import {
BatchWriteCommandInput,
DynamoDBDocumentClient,
PutCommand,
PutCommandInput,
QueryCommand,
QueryCommandInput,
} from '@aws-sdk/lib-dynamodb';
import { logger } from '@awssolutions/simple-cdf-logger';
import {
DynamoDbPaginationKey,
GSI1_INDEX_NAME,
GSI2_INDEX_NAME,
GSI3_INDEX_NAME,
} from '../common/common.models';
import { TYPES } from '../di/types';
import { DocumentDbClientItem, DynamoDbUtils } from '../utils/dynamoDb.util';
import {
PkType,
createDelimitedAttribute,
createDelimitedAttributePrefix,
expandDelimitedAttribute,
} from '../utils/pkUtils.util';
import { Component, TemplateItem } from './templates.models';
@injectable()
export class TemplatesDao {
private dbc: DynamoDBDocumentClient;
public constructor(
@inject(TYPES.DynamoDbUtils) private dynamoDbUtils: DynamoDbUtils,
@inject(TYPES.DynamoDBDocumentFactory) ddcFactory: () => DynamoDBDocumentClient
) {
this.dbc = ddcFactory();
}
public async get(name: string, version: number | string): Promise<TemplateItem> {
logger.debug(`templates.dao get: in: name:${name}, version:${version}`);
const params: QueryCommandInput = {
TableName: process.env.AWS_DYNAMODB_TABLE_NAME,
KeyConditionExpression: `#hash=:hash AND begins_with(#range,:range)`,
ExpressionAttributeNames: {
'#hash': 'pk',
'#range': 'sk',
},
ExpressionAttributeValues: {
':hash': createDelimitedAttribute(PkType.Template, name),
':range': createDelimitedAttributePrefix(PkType.TemplateVersion, version),
},
};
logger.silly(`templates.dao get: QueryInput: ${JSON.stringify(params)}`);
const results = await this.dbc.send(new QueryCommand(params));
if ((results?.Items?.length ?? 0) === 0) {
logger.debug('templates.dao get: exit: undefined');
return undefined;
}
logger.silly(`query result: ${JSON.stringify(results)}`);
const response = this.assemble(results.Items)?.[0];
logger.debug(`templates.dao get: exit: response:${JSON.stringify(response)}`);
return response;
}
public async listVersions(
name: string,
count?: number,
lastEvaluated?: TemplateVersionListPaginationKey
): Promise<[TemplateItem[], TemplateVersionListPaginationKey]> {
logger.debug(
`templates.dao listVersions: in: name:${name}, count:${count}, lastEvaluated:${JSON.stringify(
lastEvaluated
)}`
);
let exclusiveStartKey: DynamoDbPaginationKey;
if (lastEvaluated?.version) {
exclusiveStartKey = {
pk: createDelimitedAttribute(PkType.Template, name),
sk: createDelimitedAttribute(PkType.TemplateVersion, lastEvaluated.version),
siKey2: createDelimitedAttribute(PkType.Template, name, PkType.Template),
siSort2: createDelimitedAttribute(PkType.TemplateVersion, lastEvaluated.version),
};
}
const params: QueryCommandInput = {
TableName: process.env.AWS_DYNAMODB_TABLE_NAME,
IndexName: GSI2_INDEX_NAME,
KeyConditionExpression: `#hash=:hash AND begins_with(#range,:range)`,
ExpressionAttributeNames: {
'#hash': 'siKey2',
'#range': 'siSort2',
},
ExpressionAttributeValues: {
':hash': createDelimitedAttribute(PkType.Template, name, PkType.Template),
':range': createDelimitedAttributePrefix(PkType.TemplateVersion),
},
ExclusiveStartKey: exclusiveStartKey,
Limit: count,
};
logger.silly(`templates.dao listVersions: QueryInput: ${JSON.stringify(params)}`);
const results = await this.dbc.send(new QueryCommand(params));
if ((results?.Items?.length ?? 0) === 0) {
logger.debug('templates.dao listVersions: exit: undefined');
return [[], undefined];
}
logger.silly(`query result: ${JSON.stringify(results)}`);
const getComponentsParams: QueryCommandInput = {
TableName: process.env.AWS_DYNAMODB_TABLE_NAME,
KeyConditionExpression: `#hash=:hash AND begins_with(#range,:range)`,
ExpressionAttributeNames: {
'#hash': 'pk',
'#range': 'sk',
},
ExpressionAttributeValues: {
':hash': createDelimitedAttribute(PkType.Template, name),
':range': createDelimitedAttributePrefix(PkType.TemplateVersion),
},
ExclusiveStartKey: exclusiveStartKey,
Limit: count,
};
logger.silly(
`templates.dao listVersions: getComponentsParams: ${JSON.stringify(
getComponentsParams
)}`
);
let componentItems: unknown = [];
// The components were not populated before
const getComponentsResults = await this.dbc.send(new QueryCommand(getComponentsParams));
if ((getComponentsResults?.Items?.length ?? 0) > 0) {
componentItems = getComponentsResults.Items.filter((item) => {
const sk = expandDelimitedAttribute(item.sk);
return sk.length === 4 && sk[2] === PkType.Component;
});
logger.debug(
`templates.dao listVersions: filtering components componentsItems: ${JSON.stringify(
componentItems
)}`
);
}
const response = this.assemble(results.Items.concat(componentItems));
let paginationKey: TemplateVersionListPaginationKey;
if (results.LastEvaluatedKey) {
const lastEvaluatedVersion = Number(
expandDelimitedAttribute(results.LastEvaluatedKey.sk)[1]
);
paginationKey = {
version: lastEvaluatedVersion,
};
}
logger.debug(
`templates.dao listVersions: exit: response:${JSON.stringify(
response
)}, paginationKey:${paginationKey}`
);
return [response, paginationKey];
}
public async getTemplateIdByJobId(jobId: string): Promise<[string, number]> {
logger.debug(`templates.dao getTemplateIdByJobId: in: jobId:${jobId}`);
const params: QueryCommandInput = {
TableName: process.env.AWS_DYNAMODB_TABLE_NAME,
IndexName: GSI3_INDEX_NAME,
KeyConditionExpression: `#hash=:hash`,
ExpressionAttributeNames: {
'#hash': 'siKey3',
},
ExpressionAttributeValues: {
':hash': createDelimitedAttribute(PkType.IotJob, jobId),
},
};
logger.silly(`templates.dao getTemplateIdByJobId: QueryInput: ${JSON.stringify(params)}`);
const results = await this.dbc.send(new QueryCommand(params));
if ((results?.Items?.length ?? 0) === 0) {
logger.debug('templates.dao getTemplateIdByJobId: exit: undefined');
return undefined;
}
logger.silly(`query result: ${JSON.stringify(results)}`);
const name: string = results.Items[0]?.name;
const version: number = results.Items[0]?.version;
logger.debug(`templates.dao getTemplateIdByJobId: exit: ${[name, version]}`);
return [name, version];
}
public async save(template: TemplateItem): Promise<void> {
logger.debug(`templates.dao save: in: template:${JSON.stringify(template)}`);
ow(template, ow.object.nonEmpty);
ow(template.name, ow.string.nonEmpty);
ow(template.version, ow.number.greaterThan(0));
const params: BatchWriteCommandInput = {
RequestItems: {
[process.env.AWS_DYNAMODB_TABLE_NAME]: [],
},
};
// current template item
const templateDbId = createDelimitedAttribute(PkType.Template, template.name);
const t = {
PutRequest: {
Item: {
pk: templateDbId,
sk: createDelimitedAttribute(PkType.TemplateVersion, 'current'),
siKey1: PkType.Template,
name: template.name,
version: template.version,
jobConfig: template.jobConfig,
deploymentPolicies: template.deploymentPolicies,
createdAt: template.createdAt?.toISOString(),
updatedAt: template.updatedAt?.toISOString(),
},
},
};
params.RequestItems[process.env.AWS_DYNAMODB_TABLE_NAME].push(t);
// versioned template item
const tv = clone(t);
tv.PutRequest.Item.sk = createDelimitedAttribute(PkType.TemplateVersion, template.version);
delete tv.PutRequest.Item.siKey1;
tv.PutRequest.Item['siKey2'] = createDelimitedAttribute(
PkType.Template,
template.name,
PkType.Template
);
tv.PutRequest.Item['siSort2'] = tv.PutRequest.Item.sk;
params.RequestItems[process.env.AWS_DYNAMODB_TABLE_NAME].push(tv);
if ((template.components?.length ?? 0) > 0) {
template.components.forEach((component) => {
ow(component.key, ow.string.nonEmpty);
// current component items
const c = {
PutRequest: {
Item: {
pk: templateDbId,
sk: createDelimitedAttribute(
PkType.TemplateVersion,
'current',
PkType.Component,
component.key
),
key: component.key,
version: component.version,
configurationUpdate: component.configurationUpdate,
runWith: component.runWith,
},
},
};
params.RequestItems[process.env.AWS_DYNAMODB_TABLE_NAME].push(c);
// add versioned component items
const cv = clone(c);
const versionedComponentDbId = createDelimitedAttribute(
PkType.TemplateVersion,
template.version,
PkType.Component,
component.key
);
cv.PutRequest.Item.sk = versionedComponentDbId;
params.RequestItems[process.env.AWS_DYNAMODB_TABLE_NAME].push(cv);
});
}
const result = await this.dynamoDbUtils.batchWriteAll(params);
if (this.dynamoDbUtils.hasUnprocessedItems(result)) {
throw new Error('SAVE_TEMPLATE_FAILED');
}
logger.debug(`templates.dao save: exit:`);
}
public async associateDeployment(template: TemplateItem): Promise<void> {
logger.debug(
`templates.dao associateDeployment: in: template:${JSON.stringify(template)}`
);
const params: PutCommandInput = {
TableName: process.env.AWS_DYNAMODB_TABLE_NAME,
Item: {
pk: createDelimitedAttribute(PkType.Template, template.name),
sk: createDelimitedAttribute(
PkType.TemplateVersion,
template.version,
'deployment'
),
siKey2: createDelimitedAttribute(PkType.Deployment, template.deployment.id),
siSort2: createDelimitedAttribute(PkType.Deployment, template.deployment.jobId),
siKey3: createDelimitedAttribute(PkType.IotJob, template.deployment.jobId),
siSort3: createDelimitedAttribute(PkType.Deployment, template.deployment.id),
name: template.name,
version: template.version,
deployment: template.deployment.id,
jobId: template.deployment.jobId,
thingGroupName: template.deployment.thingGroupName,
},
};
await this.dbc.send(new PutCommand(params));
logger.debug(`templates.dao associateDeployment: exit:`);
}
public async list(
count?: number,
lastEvaluated?: TemplateListPaginationKey
): Promise<[TemplateItem[], TemplateListPaginationKey]> {
logger.debug(
`templates.dao list: in: count:${count}, lastEvaluated:${JSON.stringify(
lastEvaluated
)}`
);
let exclusiveStartKey: DynamoDbPaginationKey;
if (lastEvaluated?.name) {
exclusiveStartKey = {
pk: createDelimitedAttribute(PkType.Template, lastEvaluated.name),
siKey1: PkType.Template,
sk: createDelimitedAttribute(PkType.TemplateVersion, 'current'),
};
}
const params: QueryCommandInput = {
TableName: process.env.AWS_DYNAMODB_TABLE_NAME,
IndexName: GSI1_INDEX_NAME,
KeyConditionExpression: `#hash=:hash`,
ExpressionAttributeNames: {
'#hash': 'siKey1',
},
ExpressionAttributeValues: {
':hash': PkType.Template,
},
Select: 'ALL_ATTRIBUTES',
ExclusiveStartKey: exclusiveStartKey,
Limit: count,
};
logger.silly(`templates.dao list: params: ${JSON.stringify(params)}`);
const results = await this.dbc.send(new QueryCommand(params));
if ((results?.Items?.length ?? 0) === 0) {
logger.debug('templates.dao list: exit: undefined');
return [undefined, undefined];
}
logger.silly(`templates.dao list: results: ${JSON.stringify(results)}`);
const response = this.assemble(results.Items);
let paginationKey: TemplateListPaginationKey;
if (results.LastEvaluatedKey) {
const lastEvaluatedName = expandDelimitedAttribute(results.LastEvaluatedKey.pk)[1];
paginationKey = {
name: lastEvaluatedName,
};
}
logger.debug(
`templates.dao list: exit: response:${JSON.stringify(
response
)}, paginationKey:${paginationKey}`
);
return [response, paginationKey];
}
private assemble(items: DocumentDbClientItem[]): TemplateItem[] {
logger.debug(`templates.dao assemble: items:${JSON.stringify(items)}`);
if ((items?.length ?? 0) === 0) {
return undefined;
}
const t: { [version: string]: TemplateItem } = {};
const c: { [version: string]: Component[] } = {};
items.forEach((item) => {
const pk = expandDelimitedAttribute(item.pk);
const sk = expandDelimitedAttribute(item.sk);
const templateName = pk[1];
const templateVersion = sk[1];
const key = `${templateName}:::${templateVersion}`;
if (sk.length === 2) {
// template
t[key] = {
name: item.name,
version: item.version,
jobConfig: item.jobConfig,
deploymentPolicies: item.deploymentPolicies,
components: [],
createdAt: new Date(item.createdAt),
updatedAt: new Date(item.updatedAt),
};
} else if (sk.length === 4 && sk[2] === PkType.Component) {
// component
if (!c[key]) {
c[key] = [];
}
c[key].push({
key: item.key,
version: item.version,
configurationUpdate: item.configurationUpdate,
runWith: item.runWith,
});
}
});
Object.keys(t).forEach((k) => {
t[k].components = c[k];
});
logger.debug(`templates.dao assemble: exit:${JSON.stringify(t)}`);
return Object.values(t);
}
public async delete(template: TemplateItem): Promise<void> {
logger.debug(`templates.dao delete: in: template:${JSON.stringify(template)}`);
const params: BatchWriteCommandInput = {
RequestItems: {
[process.env.AWS_DYNAMODB_TABLE_NAME]: [],
},
};
// template
const templateDbId = createDelimitedAttribute(PkType.Template, template.name);
const t = {
DeleteRequest: {
Key: {
pk: templateDbId,
sk: createDelimitedAttributePrefix(PkType.TemplateVersion, template.version),
},
},
};
params.RequestItems[process.env.AWS_DYNAMODB_TABLE_NAME].push(t);
// components
if ((template.components?.length ?? 0) > 0) {
template.components.forEach((component) => {
const c = {
DeleteRequest: {
Key: {
pk: templateDbId,
sk: createDelimitedAttribute(
PkType.TemplateVersion,
template.version,
PkType.Component,
component.key
),
},
},
};
params.RequestItems[process.env.AWS_DYNAMODB_TABLE_NAME].push(c);
});
}
logger.silly(`templates.dao get: BatchWriteCommandInput: ${JSON.stringify(params)}`);
const result = await this.dynamoDbUtils.batchWriteAll(params);
if (this.dynamoDbUtils.hasUnprocessedItems(result)) {
throw new Error('DELETE_TEMPLATE_FAILED');
}
logger.debug(`templates.dao get: exit:}`);
}
}
export type TemplateListPaginationKey = {
name: string;
};
export type TemplateVersionListPaginationKey = {
version: number;
};