diff --git a/openapi.yaml b/openapi.yaml index 2f9675c..4e1d497 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -58,37 +58,8 @@ paths: content: application/json: schema: - type: object - properties: - apiVersion: - type: string - kind: - type: string - metadata: - type: object - properties: - namespace: - type: string - name: - type: string - title: - type: string - annotations: - type: object - required: - - name - additionalProperties: true - spec: - type: object - properties: - owner: - type: string - type: - type: string - required: - - apiVersion - - kind - - metadata + $ref: "#/components/schemas/Entity" + responses: '201': description: Created @@ -283,7 +254,7 @@ paths: content: application/json: schema: - type: object + $ref: "#/components/schemas/RequestIdResponse" '400': description: Client failure content: @@ -304,9 +275,85 @@ paths: httpMethod: POST uri: Fn::Sub: "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${DeleteEntityFunction.Arn}/invocations" + put: + summary: Delete catalog item + description: Delete catalog item + parameters: + - $ref: "#/components/parameters/namespace" + - $ref: "#/components/parameters/kind" + - $ref: "#/components/parameters/name" + - $ref: "#/components/parameters/headerContentTypeJson" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Entity" + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/RequestIdResponse" + '400': + description: Client failure + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + '500': + description: Server failure + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + security: + - serverlessOpsCognitoPool: + - Fn::Sub: https://${Hostname}/catalog.write + x-amazon-apigateway-integration: + type: AWS_PROXY + httpMethod: POST + uri: + Fn::Sub: "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${UpsertEntityFunction.Arn}/invocations" components: schemas: + Entity: + type: object + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + properties: + namespace: + type: string + name: + type: string + title: + type: string + annotations: + type: object + required: + - name + additionalProperties: true + spec: + type: object + properties: + owner: + type: string + type: + type: string + required: + - owner + additionalProperties: true + required: + - apiVersion + - kind + - metadata CreateEntityResponse: type: object properties: diff --git a/src/handlers/CreateEntity/function.ts b/src/handlers/CreateEntity/function.ts index 0cd5b3f..c986106 100644 --- a/src/handlers/CreateEntity/function.ts +++ b/src/handlers/CreateEntity/function.ts @@ -31,7 +31,7 @@ interface Item extends Entity { itemType: string } -export async function putEntity(entity: Entity): Promise { +export async function putEntity(entity: Entity, upsert: boolean): Promise { const namespace = entity.metadata.namespace const kind = entity.kind.toLowerCase() @@ -47,7 +47,10 @@ export async function putEntity(entity: Entity): Promise { const params: PutItemCommandInput = { TableName: DDB_TABLE_NAME, Item: marshall(item), - ConditionExpression: 'attribute_not_exists(pk) AND attribute_not_exists(sk)' + } + + if (!upsert) { + params.ConditionExpression = 'attribute_not_exists(pk) AND attribute_not_exists(sk)' } try { @@ -65,7 +68,7 @@ export async function putEntity(entity: Entity): Promise { } -export async function handler (event: APIGatewayProxyEvent, context: Context): Promise { +export async function handler_create (event: APIGatewayProxyEvent, context: Context): Promise { LOGGER.debug('Received event', { event }) const event_id = context.awsRequestId @@ -74,7 +77,7 @@ export async function handler (event: APIGatewayProxyEvent, context: Context): P let statusCode: number let body: string try { - const output = await putEntity(entity) + await putEntity(entity, false) statusCode = 201 body = JSON.stringify({'request_id': event_id}) } catch (error) { @@ -99,3 +102,57 @@ export async function handler (event: APIGatewayProxyEvent, context: Context): P body } } + +export async function handler_upsert (event: APIGatewayProxyEvent, context: Context): Promise { + LOGGER.debug('Received event', { event }) + const event_id = context.awsRequestId + + const entity: Entity = JSON.parse(event.body || '{}') // Already validated body at Gateway + + // Upsert data must match request path + const namespace = entity.metadata.namespace + const kind = entity.kind.toLowerCase() + const name = entity.metadata.name + + if ( + namespace !== event.pathParameters?.namespace || + kind !== event.pathParameters?.kind || + name !== event.pathParameters?.name + ) { + return { + statusCode: 400, + body: JSON.stringify({ + name: 'BadRequest', + message: 'Entity metadata does not match request path' + }) + } + } + + let statusCode: number + let body: string + try { + await putEntity(entity, true) + statusCode = 201 + body = JSON.stringify({'request_id': event_id}) + } catch (error) { + LOGGER.error("Operation failed", { event }) + const fault = (error).$fault + switch (fault) { + case 'client': + statusCode = 400 + break; + default: + statusCode = 500 + break; + } + body = JSON.stringify({ + name: (error).name, + message: (error).message + }) + } + + return { + statusCode, + body + } +} \ No newline at end of file diff --git a/template.yaml b/template.yaml index 637ec37..a0fc061 100644 --- a/template.yaml +++ b/template.yaml @@ -70,7 +70,7 @@ Resources: Type: AWS::Serverless::Function Properties: CodeUri: ./dist/handlers/CreateEntity - Handler: function.handler + Handler: function.handler_create Policies: - DynamoDBCrudPolicy: TableName: !Ref DdbTable @@ -95,6 +95,35 @@ Resources: Principal: apigateway.amazonaws.com + UpsertEntityFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: ./dist/handlers/UpsertEntity + Handler: function.handler_upsert + Policies: + - DynamoDBCrudPolicy: + TableName: !Ref DdbTable + Metadata: + BuildMethod: esbuild + BuildProperties: + Minify: true + Target: "es2020" + Format: "esm" + MainFields: module,main # This is to help with ESM modules + Sourcemap: !Ref EnableSourceMaps + OutExtension: + - .js=.mjs + EntryPoints: + - function.js + + UpsertEntityFunctionInvokePermission: + Type: AWS::Lambda::Permission + Properties: + FunctionName: !GetAtt UpsertEntityFunction.Arn + Action: lambda:InvokeFunction + Principal: apigateway.amazonaws.com + + GetEntityFunction: Type: AWS::Serverless::Function Properties: @@ -123,6 +152,7 @@ Resources: Action: lambda:InvokeFunction Principal: apigateway.amazonaws.com + DeleteEntityFunction: Type: AWS::Serverless::Function Properties: