diff --git a/package-lock.json b/package-lock.json index 4cdc80c..9a66a43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,8 +19,7 @@ "joi": "^17.9.0", "js2xmlparser": "^5.0.0", "rxjs": "^7.8.0", - "sqlite3": "^5.1.6", - "uuidv4": "^6.2.13" + "sqlite3": "^5.1.6" }, "devDependencies": { "@types/express": "^4.17.17", @@ -690,12 +689,6 @@ "@types/send": "*" } }, - "node_modules/@types/uuid": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", - "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", - "license": "MIT" - }, "node_modules/@ungap/structured-clone": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.1.tgz", @@ -4413,26 +4406,6 @@ "node": ">= 0.4.0" } }, - "node_modules/uuidv4": { - "version": "6.2.13", - "resolved": "https://registry.npmjs.org/uuidv4/-/uuidv4-6.2.13.tgz", - "integrity": "sha512-AXyzMjazYB3ovL3q051VLH06Ixj//Knx7QnUSi1T//Ie3io6CpsPu9nVMOx5MoLWh6xV0B9J0hIaxungxXUbPQ==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "license": "MIT", - "dependencies": { - "@types/uuid": "8.3.4", - "uuid": "8.3.2" - } - }, - "node_modules/uuidv4/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index ed2e4b2..b6762fb 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,7 @@ "joi": "^17.9.0", "js2xmlparser": "^5.0.0", "rxjs": "^7.8.0", - "sqlite3": "^5.1.6", - "uuidv4": "^6.2.13" + "sqlite3": "^5.1.6" }, "devDependencies": { "@types/express": "^4.17.17", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b0e299f..eaa9d50 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -13,7 +13,7 @@ model Attribute { name String value String - @@index([arn]) + @@unique([arn, name]) } model Audit { @@ -25,22 +25,25 @@ model Audit { } model Secret { - versionId String @id - name String - description String? + versionId String @id + name String + description String? secretString String - accountId String - region String - createdAt DateTime @default(now()) + accountId String + region String + createdAt DateTime @default(now()) deletionDate DateTime? @@index([name]) } model SnsTopic { - name String @id + id Int @id @default(autoincrement()) + name String accountId String region String + + @@unique([accountId, region, name]) } model SnsTopicSubscription { @@ -52,11 +55,37 @@ model SnsTopicSubscription { region String } +model SqsQueue { + id Int @id @default(autoincrement()) + name String + accountId String + region String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + messages SqsQueueMessage[] + + @@unique([accountId, region, name]) +} + +model SqsQueueMessage { + id String @id + queueId Int + senderId String + message String + inFlightRelease DateTime + createdAt DateTime @default(now()) + + queue SqsQueue @relation(fields: [queueId], references: [id]) + + @@index([queueId]) +} + model Tag { id Int @id @default(autoincrement()) arn String name String value String - @@index([arn]) + @@unique([arn, name]) } diff --git a/src/abstract-action.handler.ts b/src/abstract-action.handler.ts index a41187a..d2ed356 100644 --- a/src/abstract-action.handler.ts +++ b/src/abstract-action.handler.ts @@ -1,5 +1,5 @@ +import { randomUUID } from 'crypto'; import { Action } from './action.enum'; -import * as uuid from 'uuid'; import * as Joi from 'joi'; export type AwsProperties = { @@ -34,7 +34,7 @@ export abstract class AbstractActionHandler ({ +export const DefaultActionHandlerProvider = (symbol: InjectionToken, format: Format, actions: Action[]): Provider => ({ provide: symbol, useFactory: (existingActionHandlers: ExistingActionHandlers) => { const cloned = { ...existingActionHandlers }; diff --git a/src/default-action-handler/existing-action-handlers.provider.ts b/src/default-action-handler/existing-action-handlers.provider.ts index 732baab..0550234 100644 --- a/src/default-action-handler/existing-action-handlers.provider.ts +++ b/src/default-action-handler/existing-action-handlers.provider.ts @@ -1,12 +1,14 @@ -import { Provider } from '@nestjs/common'; +import { InjectionToken, OptionalFactoryDependency, Provider } from '@nestjs/common'; + import { AbstractActionHandler } from '../abstract-action.handler'; +import { Action } from '../action.enum'; import { ExistingActionHandlers } from './default-action-handler.constants'; -export const ExistingActionHandlersProvider = (inject): Provider => ({ +export const ExistingActionHandlersProvider = (inject: Array): Provider => ({ provide: ExistingActionHandlers, useFactory: (...args: AbstractActionHandler[]) => args.reduce((m, h) => { m[h.action] = h; return m; - }, {}), + }, {} as Record), inject, }); diff --git a/src/sns/publish.handler.ts b/src/sns/publish.handler.ts index 2d3e1f7..b1d3040 100644 --- a/src/sns/publish.handler.ts +++ b/src/sns/publish.handler.ts @@ -1,6 +1,6 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import * as Joi from 'joi'; -import * as uuid from 'uuid'; +import { randomUUID } from 'crypto'; import { PrismaService } from '../_prisma/prisma.service'; import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler'; @@ -44,7 +44,7 @@ export class PublishHandler extends AbstractActionHandler { throw new BadRequestException(); } - const MessageId = uuid.v4(); + const MessageId = randomUUID(); const subscriptions = await this.prismaService.snsTopicSubscription.findMany({ where: { topicArn: arn } }); const topicAttributes = await this.attributeService.getByArn(arn); diff --git a/src/sqs/create-queue.handler.ts b/src/sqs/create-queue.handler.ts index d66f561..059ef5c 100644 --- a/src/sqs/create-queue.handler.ts +++ b/src/sqs/create-queue.handler.ts @@ -1,12 +1,12 @@ import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import * as Joi from 'joi'; + import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler'; import { Action } from '../action.enum'; -import * as Joi from 'joi'; -import { TagsService } from '../aws-shared-entities/tags.service'; -import { SqsQueue } from './sqs-queue.entity'; import { AttributesService } from '../aws-shared-entities/attributes.service'; +import { TagsService } from '../aws-shared-entities/tags.service'; +import { SqsQueueEntryService } from './sqs-queue-entry.service'; +import { SqsQueue } from './sqs-queue.entity'; type QueryParams = { QueueName: string; @@ -16,8 +16,7 @@ type QueryParams = { export class CreateQueueHandler extends AbstractActionHandler { constructor( - @InjectRepository(SqsQueue) - private readonly sqsQueueRepo: Repository, + private readonly sqsQueueEntryService: SqsQueueEntryService, private readonly tagsService: TagsService, private readonly attributeService: AttributesService, ) { @@ -32,11 +31,11 @@ export class CreateQueueHandler extends AbstractActionHandler { const { QueueName: name } = params; - const queue = await this.sqsQueueRepo.create({ + const queue = await this.sqsQueueEntryService.createQueue({ name, accountId: awsProperties.accountId, region: awsProperties.region, - }).save(); + }); const tags = TagsService.tagPairs(params); await this.tagsService.createMany(queue.arn, tags); diff --git a/src/sqs/delete-message-batch.handler.ts b/src/sqs/delete-message-batch.handler.ts index bcc95d6..d374afb 100644 --- a/src/sqs/delete-message-batch.handler.ts +++ b/src/sqs/delete-message-batch.handler.ts @@ -1,9 +1,10 @@ import { Injectable } from '@nestjs/common'; +import * as Joi from 'joi'; + import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler'; import { Action } from '../action.enum'; -import * as Joi from 'joi'; -import { SqsQueue } from './sqs-queue.entity'; import { SqsQueueEntryService } from './sqs-queue-entry.service'; +import { SqsQueue } from './sqs-queue.entity'; type QueryParams = { QueueUrl: string; diff --git a/src/sqs/delete-message.handler.ts b/src/sqs/delete-message.handler.ts index f73787c..2e28968 100644 --- a/src/sqs/delete-message.handler.ts +++ b/src/sqs/delete-message.handler.ts @@ -1,9 +1,10 @@ import { Injectable } from '@nestjs/common'; +import * as Joi from 'joi'; + import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler'; import { Action } from '../action.enum'; -import * as Joi from 'joi'; -import { SqsQueue } from './sqs-queue.entity'; import { SqsQueueEntryService } from './sqs-queue-entry.service'; +import { SqsQueue } from './sqs-queue.entity'; type QueryParams = { QueueUrl: string; diff --git a/src/sqs/delete-queue.handler.ts b/src/sqs/delete-queue.handler.ts index c166d8b..31640a9 100644 --- a/src/sqs/delete-queue.handler.ts +++ b/src/sqs/delete-queue.handler.ts @@ -1,13 +1,12 @@ import { BadRequestException, Injectable } from '@nestjs/common'; +import * as Joi from 'joi'; + import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler'; import { Action } from '../action.enum'; -import * as Joi from 'joi'; import { AttributesService } from '../aws-shared-entities/attributes.service'; -import { InjectRepository } from '@nestjs/typeorm'; -import { SqsQueue } from './sqs-queue.entity'; -import { Repository } from 'typeorm'; -import { SqsQueueEntryService } from './sqs-queue-entry.service'; import { TagsService } from '../aws-shared-entities/tags.service'; +import { SqsQueueEntryService } from './sqs-queue-entry.service'; +import { SqsQueue } from './sqs-queue.entity'; type QueryParams = { QueueUrl?: string, @@ -18,8 +17,6 @@ type QueryParams = { export class DeleteQueueHandler extends AbstractActionHandler { constructor( - @InjectRepository(SqsQueue) - private readonly sqsQueueRepo: Repository, private readonly tagsService: TagsService, private readonly attributeService: AttributesService, private readonly sqsQueueEntryService: SqsQueueEntryService, @@ -37,22 +34,15 @@ export class DeleteQueueHandler extends AbstractActionHandler { protected async handle(params: QueryParams, awsProperties: AwsProperties) { const [accountId, name] = SqsQueue.tryGetAccountIdAndNameFromPathOrArn(params.QueueUrl ?? params.__path); - const queue = await this.sqsQueueRepo.findOne({ where: { accountId , name } }); + const queue = await this.sqsQueueEntryService.findQueueByAccountIdAndName(accountId, name); - if(!queue) { + if (!queue) { throw new BadRequestException('ResourceNotFoundException'); } await this.sqsQueueEntryService.purge(accountId, name); await this.tagsService.deleteByArn(queue.arn); await this.attributeService.deleteByArn(queue.arn); - await queue.remove(); - } - - private async getAttributes(attributeNames: string[], queueArn: string) { - if (attributeNames.length === 0 || attributeNames.length === 1 && attributeNames[0] === 'All') { - return await this.attributeService.getByArn(queueArn); - } - return await this.attributeService.getByArnAndNames(queueArn, attributeNames); + await this.sqsQueueEntryService.deleteQueue(queue.id); } } diff --git a/src/sqs/get-queue-attributes.handler.ts b/src/sqs/get-queue-attributes.handler.ts index ec9dfff..c91e95e 100644 --- a/src/sqs/get-queue-attributes.handler.ts +++ b/src/sqs/get-queue-attributes.handler.ts @@ -1,25 +1,22 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; +import * as Joi from 'joi'; + import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler'; import { Action } from '../action.enum'; -import * as Joi from 'joi'; import { AttributesService } from '../aws-shared-entities/attributes.service'; -import { InjectRepository } from '@nestjs/typeorm'; -import { SqsQueue } from './sqs-queue.entity'; -import { Repository } from 'typeorm'; import { SqsQueueEntryService } from './sqs-queue-entry.service'; +import { SqsQueue } from './sqs-queue.entity'; type QueryParams = { QueueUrl?: string, 'AttributeName.1'?: string; __path: string; -} +} & Record; @Injectable() export class GetQueueAttributesHandler extends AbstractActionHandler { constructor( - @InjectRepository(SqsQueue) - private readonly sqsQueueRepo: Repository, private readonly attributeService: AttributesService, private readonly sqsQueueEntryService: SqsQueueEntryService, ) { @@ -42,23 +39,23 @@ export class GetQueueAttributesHandler extends AbstractActionHandler { m[a.name] = a.value; return m; - }, {}); + }, {} as Record); - const response = { + const response: Record = { ...attributeMap, ApproximateNumberOfMessages: `${queueMetrics.total}`, ApproximateNumberOfMessagesNotVisible: `${queueMetrics.inFlight}`, @@ -66,7 +63,8 @@ export class GetQueueAttributesHandler extends AbstractActionHandler ({ + return { + Attribute: Object.keys(response).map(k => ({ Name: k, Value: response[k], })) diff --git a/src/sqs/list-queues.handler.ts b/src/sqs/list-queues.handler.ts index 07fd01d..bc24230 100644 --- a/src/sqs/list-queues.handler.ts +++ b/src/sqs/list-queues.handler.ts @@ -1,20 +1,18 @@ import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import * as Joi from 'joi'; + +import { PrismaService } from '../_prisma/prisma.service'; import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler'; import { Action } from '../action.enum'; -import * as Joi from 'joi'; import { SqsQueue } from './sqs-queue.entity'; -type QueryParams = { -} +type QueryParams = {} @Injectable() export class ListQueuesHandler extends AbstractActionHandler { constructor( - @InjectRepository(SqsQueue) - private readonly sqsQueueRepo: Repository, + private readonly prismaService: PrismaService, ) { super(); } @@ -25,7 +23,14 @@ export class ListQueuesHandler extends AbstractActionHandler { protected async handle(params: QueryParams, awsProperties: AwsProperties) { - const queues = await this.sqsQueueRepo.find({ where: { accountId: awsProperties.accountId }}); + const rawQueues = await this.prismaService.sqsQueue.findMany({ + where: { + accountId: awsProperties.accountId, + region: awsProperties.region, + } + }); + + const queues = rawQueues.map(q => new SqsQueue(q)); return { QueueUrl: queues.map((q) => q.getUrl(awsProperties.host)) diff --git a/src/sqs/set-queue-attributes.handler.ts b/src/sqs/set-queue-attributes.handler.ts index 6aa5e38..6e46482 100644 --- a/src/sqs/set-queue-attributes.handler.ts +++ b/src/sqs/set-queue-attributes.handler.ts @@ -1,11 +1,11 @@ import { BadRequestException, Injectable } from '@nestjs/common'; +import * as Joi from 'joi'; + import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler'; import { Action } from '../action.enum'; -import * as Joi from 'joi'; import { AttributesService } from '../aws-shared-entities/attributes.service'; -import { InjectRepository } from '@nestjs/typeorm'; +import { SqsQueueEntryService } from './sqs-queue-entry.service'; import { SqsQueue } from './sqs-queue.entity'; -import { Repository } from 'typeorm'; type QueryParams = { 'Attribute.Name': string; @@ -17,9 +17,8 @@ type QueryParams = { export class SetQueueAttributesHandler extends AbstractActionHandler { constructor( - @InjectRepository(SqsQueue) - private readonly sqsQueueRepo: Repository, private readonly attributeService: AttributesService, + private readonly sqsQueueEntryService: SqsQueueEntryService, ) { super(); } @@ -34,7 +33,7 @@ export class SetQueueAttributesHandler extends AbstractActionHandler = {}; - private queueObjectCache: Record = {}; constructor( - @InjectRepository(SqsQueue) - private readonly sqsQueueRepo: Repository, + private readonly prismaService: PrismaService, ) {} - async findQueueByAccountIdAndName(accountId: string, name: string): Promise { - return await this.sqsQueueRepo.findOne({ where: { accountId, name } }); + async findQueueByAccountIdAndName(accountId: string, name: string): Promise { + const prisma = await this.prismaService.sqsQueue.findFirst({ where: { accountId, name } }); + return prisma ? new SqsQueue(prisma) : null; } - metrics(queueArn: string): Metrics { + async createQueue(data: Prisma.SqsQueueCreateInput): Promise { + const prisma = await this.prismaService.sqsQueue.create({ data }); + return new SqsQueue(prisma); + } + + async deleteQueue(id: number): Promise { + await this.prismaService.sqsQueue.delete({ where: { id }}); + } + + async metrics(queueId: number): Promise { const now = new Date(); - return this.getQueueList(queueArn).reduce((acc, e) => { - acc.total += 1; - acc.inFlight += e.inFlightReleaseDate > now ? 1 : 0; - return acc; - }, { total: 0, inFlight: 0 }); + const [total, inFlight] = await Promise.all([ + this.prismaService.sqsQueueMessage.count({ where: { queueId }}), + this.prismaService.sqsQueueMessage.count({ where: { queueId, inFlightRelease: { gt: now } }}), + ]); + + return { total, inFlight } } async publish(accountId: string, queueName: string, message: string) { - const queue = await this.sqsQueueRepo.findOne({ where: { accountId, name: queueName }}); + const prisma = await this.prismaService.sqsQueue.findFirst({ where: { accountId, name: queueName }}); - if (!queue) { + if (!prisma) { console.warn(`Warning bad subscription to ${queueName}`); return; } - this.getQueueList(queue.arn).push({ - id: uuid.v4(), - queueArn: queue.arn, - senderId: accountId, - message, - inFlightReleaseDate: new Date(), - createdAt: new Date(), + const queue = new SqsQueue(prisma); + + await this.prismaService.sqsQueueMessage.create({ + data: { + id: randomUUID(), + queueId: queue.id, + senderId: accountId, + message, + inFlightRelease: new Date(), + } }); } - async receiveMessages(accountId: string, queueName: string, maxNumberOfMessages = 10, visabilityTimeout = 0): Promise { + async receiveMessages(accountId: string, queueName: string, maxNumberOfMessages = 10, visabilityTimeout = 0): Promise { const queue = await this.getQueueHelper(accountId, queueName); const accessDate = new Date(); const newInFlightReleaseDate = new Date(accessDate); newInFlightReleaseDate.setSeconds(accessDate.getSeconds() + visabilityTimeout); - const records = this.getQueueList(queue.arn).filter(e => e.inFlightReleaseDate <= accessDate).slice(0, maxNumberOfMessages - 1); - records.forEach(e => e.inFlightReleaseDate = newInFlightReleaseDate); - return records; + const records = await this.prismaService.sqsQueueMessage.findMany({ + where: { + queueId: queue.id, + inFlightRelease: { + lte: accessDate, + } + }, + take: maxNumberOfMessages, + }); + + await this.prismaService.sqsQueueMessage.updateMany({ + data: { + inFlightRelease: newInFlightReleaseDate + }, + where: { + id: { + in: records.map(r => r.id) + } + } + }); + + return records.map(r => ({ ...r, inFlightRelease: newInFlightReleaseDate })); } - async deleteMessage(accountId: string, queueName: string, id: string): Promise { - - const queue = await this.getQueueHelper(accountId, queueName); - - const records = this.getQueueList(queue.arn); - const loc = records.findIndex(r => r.id === id); - records.splice(loc, 1); + async deleteMessage(id: string): Promise { + await this.prismaService.sqsQueueMessage.delete({ where: { id }}); } async purge(accountId: string, queueName: string) { - const queue = await this.sqsQueueRepo.findOne({ where: { accountId, name: queueName }}); - this.queues[queue.arn] = []; + const queue = await this.findQueueByAccountIdAndName(accountId, queueName); + + if (!queue) { + return; + } + + await this.prismaService.sqsQueueMessage.deleteMany({ where: { queueId: queue.id }}); } private async getQueueHelper(accountId: string, queueName: string): Promise { if (!this.queueObjectCache[`${accountId}/${queueName}`] || this.queueObjectCache[`${accountId}/${queueName}`][0] < new Date()) { - this.queueObjectCache[`${accountId}/${queueName}`] = [new Date(Date.now() + FIFTEEN_SECONDS), await this.sqsQueueRepo.findOne({ where: { accountId, name: queueName }})]; + const queue = await this.findQueueByAccountIdAndName(accountId, queueName); + + if (!queue) { + throw new BadRequestException('Queue not found'); + } + + this.queueObjectCache[`${accountId}/${queueName}`] = [new Date(Date.now() + FIFTEEN_SECONDS), queue]; } const [_, queue] = this.queueObjectCache[`${accountId}/${queueName}`]; - - if (!queue) { - throw new BadRequestException('Queue not found'); - } - return queue; } - - private getQueueList(arn: string): QueueEntry[] { - - if (!this.queues[arn]) { - this.queues[arn] = []; - } - - return this.queues[arn]; - } } diff --git a/src/sqs/sqs-queue.entity.ts b/src/sqs/sqs-queue.entity.ts index fbc837c..aef0c99 100644 --- a/src/sqs/sqs-queue.entity.ts +++ b/src/sqs/sqs-queue.entity.ts @@ -1,4 +1,5 @@ -import { BaseEntity, Column, CreateDateColumn, Entity, PrimaryColumn, UpdateDateColumn } from 'typeorm'; +import { SqsQueue as PrismaSqsQueue } from '@prisma/client'; + import { getPathFromUrl } from '../util/get-path-from-url'; const attributeSlotMap = { @@ -6,23 +7,24 @@ const attributeSlotMap = { 'Value': 'value', } -@Entity('sqs_queue') -export class SqsQueue extends BaseEntity { + export class SqsQueue implements PrismaSqsQueue { - @PrimaryColumn({ name: 'name' }) + id: number; name: string; - - @Column({ name: 'account_id', nullable: false }) accountId: string; - - @Column({ name: 'region', nullable: false }) region: string; + createdAt: Date; + updatedAt: Date; - @CreateDateColumn() - createdAt: string; + constructor(p: PrismaSqsQueue) { + this.id = p.id; + this.name = p.name; + this.accountId = p.accountId; + this.region = p.region; + this.createdAt = p.createdAt; + this.updatedAt = p.updatedAt; + } - @UpdateDateColumn() - updatedAt: string; get arn(): string { return `arn:aws:sqs:${this.region}:${this.accountId}:${this.name}`; @@ -39,8 +41,8 @@ export class SqsQueue extends BaseEntity { static getAccountIdAndNameFromArn(arn: string): [string, string] { const parts = arn.split(':'); - const name = parts.pop(); - const accountId = parts.pop(); + const name = parts.pop() as string; + const accountId = parts.pop() as string; return [accountId, name]; } @@ -53,18 +55,36 @@ export class SqsQueue extends BaseEntity { } static attributePairs(queryParams: Record): { key: string, value: string }[] { - const pairs = [null]; + const pairs: { key: string, value: string }[] = []; for (const param of Object.keys(queryParams)) { - const [type, idx, slot] = param.split('.'); + const components = this.breakdownAwsQueryParam(param); + + if (!components) { + continue; + } + + const [type, idx, slot] = components; + if (type === 'Attribute') { - if (!pairs[+idx]) { - pairs[+idx] = { key: '', value: ''}; + if (!pairs[idx]) { + pairs[idx] = { key: '', value: ''}; } - pairs[+idx][attributeSlotMap[slot]] = queryParams[param]; + pairs[+idx][slot] = queryParams[param]; } } - pairs.shift(); return pairs; } + + private static breakdownAwsQueryParam(paramKey: string): [string, number, 'key' | 'value'] | null { + + const parts = paramKey.split('.'); + + if (parts.length !== 3) { + return null; + } + + const [type, idx, slot] = parts; + return [type, +idx, attributeSlotMap[slot as 'Name' | 'Value'] as 'key' | 'value']; + } }