Mini backoffice for pet shop
Tags: devchallenge, awschallenge, amplify, fullstack
Published: 2024-05-27 01:28:20

*This is a submission for the [The AWS Amplify Fullstack TypeScript Challenge ](https://dev.to/challenges/awschallenge)*

## What I Built
<!-- Tell us what your app does! -->
   I developed in Angular a small application serving as a backoffice for a site selling pets. The idea here is to provide registered information on the platform about a pet and its seller, commonly called the owner. 
To be able to use the application, authentication by password/username (email) is necessary, the user can create one if they do not have one. Once the user is connected, he records the information on the owner of the animal, then records the pets one by one by selecting an owner from the previously created list.




## Demo and Code
<!-- Share a link to your Amplify App and source code. Include some screenshots as well. -->

Source code --> [Github repo](https://github.com/nivekalara237/amplify-angular-template.git)
Live demo --> [app](https://main.d2sce8krj6aawp.amplifyapp.com/auth)

Demo (1/3)
Password/email login


<img width="100%" style="height: 100%" src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u5fubi2uoui878339o82.gif"/>


Demo (2/3)
Owner registration
<img width="100%" style="height: 100%" src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qe8jo9k4mu1nt6hllli0.gif"/>


Demo (3/3)
Registering a pet


<img width="100%" style="height: 100%" src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mwsv6o51pugltzaxvacs.gif"/>

## Integrations
<!-- Tell us which qualifying technologies you integrated, and how you used them. -->
The following AWS services have been integrated: 

- S3
- AppSync
- Lambda
- Dyanmodb 
- Cognito/IAM
 

## 1. Serverless DynamoDB & AppSync

All data entered by users is saved in a Dynamodb table. Here is the table schema and the amplify configuration:

```ts
export const petsSchema = {
  PetOwner: a
    .model({
      OwnerID: a.id(),
      Name: a.string().required(),
      Email: a.email().required(),
      Phone: a.string(),
      Bio: a.string(),
      Picture: a.string(),
    })
    .authorization((allow) => allow.publicApiKey()),
  PetKind: a.enum(['FEMALE', 'MALE']),
  PetCategorySchema: a.enum(
    Object.keys(PetCategory).filter((v) => isNaN(v as any))
  ),
  Pet: a
    .model({
      ID: a.id(),
      NickelName: a.string().required(),
      Price: a.float().required(),
      Category: a.ref('PetCategorySchema').required(),
      Breed: a.string(),
      Rate: a.float().default(0.0),
      Kind: a.ref('PetKind'),
      BornDate: a.date().required(),
      Weight: a.float().required(),
      PetBio: a.string(),
      OwnerID: a.id(),
      //Owner: a.belongsTo('PetOwner', 'OwnerID'),
      Images: a.string().array(),
      DefaultImage: a.string(),
    })
    .authorization((allow) => allow.publicApiKey()),
};
```



## 2. S3

  For storing the pets' images and the owners photo, I used S3 storage service. How did I store the images in S3? when registering the pet our owner, I make a first record in the DynamoDB table to get the _ID, then I upload the file(s) to the S3 bucket then I update the information with the file ObjectKey.

> amplify/s3 configuration

```ts

export const storage = defineStorage({
  name: 'amplifyPetShop',
  triggers: {...}),
  },
  access: (allow) => ({
    'owner-pictures/*': [
      allow.guest.to(['read']),
      allow.authenticated.to(['read', 'delete', 'write']),
    ],
    'pets-thumbs/*': [
      allow.guest.to(['read']),
      allow.resource(createImageThumbs).to(['read', 'write', 'delete']),
      allow.entity('identity').to(['read', 'write', 'delete']),
    ],
    'pets/*': [
      allow.guest.to(['read']),
      allow.resource(createImageThumbs).to(['read']),// Lambda resource
      allow.entity('identity').to(['read', 'write', 'delete']),
    ],
  }),
});
```

```ts
const files =...; // <-- list of File
const prefix = "pets";
const arr = [...files].map((file) =>
      from(
        uploadData({
          data: file,
          path: `${prefix}${uuid}_${file.name}`,
        }).result.then((result) => ({ result, original: file.name }))
      )
    );
```

## 3. Serverless Lambda

I. n order to automate the creation of images in miniaturized versions (thumbnails) at a lower cost, I opted for a lambda which is triggered each time a file is loaded in the *Pets* folder, this lambda executes a function which generates a thumbnail image and it in turn loads it into S3 bucket. Here is the amplify configuration and code for the function in question.

> Amplify onfiguration

```ts
export const createImageThumbs = defineFunction({
  name: 'gen-image-thumbs',
  entry: './handler.ts',
  runtime: 20,
  memoryMB: 256,
});
```

>  and the lambda function code

```ts
export const handler: S3Handler = async (event: S3Event) => {
  const objectKeysUploaded = event.Records.map(
    (record: any) => record.s3.object.key
  );

  const srcBucket = process.env.AMPLIFY_PET_SHOP_BUCKET_NAME;
  const srcKey = event.Records[0].s3.object.key;
  if (srcKey.startsWith('pets/')) {
    const dstKey = `pets-thumbnails/${srcKey.split('pets/')[1]}`;
    const originalImage = await s3Client.send(
      new GetObjectCommand({
        Bucket: srcBucket,
        Key: dstKey,
      })
    );
    // @ts-ignore
    const resizedImage = await sharp(originalImage).resize(128).toBuffer();
    const command = new PutObjectCommand({
      Bucket: srcBucket,
      Key: `thumbnails/${objectKeysUploaded[0]}`,
      Body: resizedImage,
    });

    await s3Client
      .send(command)
      .then((value) => {
        console.log(`Thumbnail uploaded for objects ${srcKey}]`, value);
      })
      .catch((reason) => {
        console.error(reason);
      });
   }
};
```

   For the lambda to be executed by the S3 resource, we need to assign it the necessary rights, similarly the lambda function will also need the rights to read and write to the S3 bucket. Here is the amplify config:

```ts
'pets-thumbnails/*': [
      ...
      allow.resource(createImageThumbs).to(['read', 'write', 'delete']),
    ],
    'pets/*': [
      ...
      allow.resource(createImageThumbs).to(['read']),
    ],
```

  The first line gives the rights w/r to the lambda in the bucket to load the files, and the second line to download the original file.

And finally here is the trigger configuration:

```ts
export const storage = defineStorage({
  name: 'amplifyPetShop',
  triggers: {
    onUpload: defineFunction({
      entry: '../functions/create-image-thumbs/handler.ts',
      environment: {
        TARGET_BUCKET_NAME: 'pets-thumbs',
      },
    }),
  },
...
```

<!-- Reminder: Qualifying technologies are data, authentication, serverless functions, and file storage as outlined in the guidelines -->

**Connected Components and/or Feature Full**

<!-- Let us know if you developed UI using Amplify connected components for UX patterns, and/or if your project includes all four integrations to qualify for the additional prize categories. -->

  This project was developed with Angular, and for that occasion I used the connection plugin @amplify/angular-ui to manage the connection and usage registration pages, the rest of the project was developed with the components o [primeng](https://primeng.org).

<!-- Team Submissions: Please pick one member to publish the submission and credit teammates by listing their DEV usernames directly in the body of the post. -->

<!-- Don't forget to add a cover image (if you want). -->