Skip to content

Commit

Permalink
Form validation Thomas + Joanna (#45)
Browse files Browse the repository at this point in the history
* Update NewAssetForm with zod validation

* added routes for getting list of asset names and deleting asset by id

* Implement Joanna's form validation

* Update assets.py

* Speed up read_asset_names query, format files

---------

Co-authored-by: Thomas Shaw <printer.83mph@gmail.com>
  • Loading branch information
pojojojo21 and printer83mph authored Apr 23, 2024
1 parent 86ccc92 commit e3dc131
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 11 deletions.
34 changes: 33 additions & 1 deletion backend/routers/api_v1/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@
read_asset_exists,
read_asset_versions,
read_assets,
read_assets_names,
read_asset_info,
read_version_file,
update_asset,
remove_asset,
)
from database.connection import get_db
from schemas.models import Asset, AssetCreate, Version, VersionCreate
Expand Down Expand Up @@ -55,6 +57,17 @@ def get_assets(
)


@router.get(
"/names",
summary="Get a list of asset names",
description="Used for fetching a list of the names of assets stored in the database.",
)
def get_assets_names(
db: Session = Depends(get_db),
) -> Sequence[str]:
return read_assets_names(db)


@router.post(
"/",
summary="Create a new asset, not including initial version",
Expand All @@ -81,7 +94,11 @@ def get_asset_info(uuid: str, db: Annotated[Session, Depends(get_db)]) -> AssetI
return result


@router.put("/{uuid}", summary="Update asset metadata")
@router.put(
"/{uuid}",
summary="Update asset metadata",
description="Based on `uuid`, updates information for a specific asset.",
)
async def put_asset(
db: Annotated[Session, Depends(get_db)],
token: Annotated[str, Depends(oauth2_scheme)],
Expand All @@ -94,6 +111,21 @@ async def put_asset(
return result


@router.delete(
"/{uuid}",
summary="Delete asset metadata ONLY FOR DEV PURPOSES",
description="Based on `uuid`, deletes a specific asset.",
)
async def delete_asset(
uuid: str,
db: Session = Depends(get_db),
):
result = remove_asset(db, uuid)
if result is False:
raise HTTPException(status_code=404, detail="Asset not found")
return


@router.get("/{uuid}/versions", summary="Get a list of versions for a given asset")
def get_asset_versions(
db: Annotated[Session, Depends(get_db)],
Expand Down
16 changes: 15 additions & 1 deletion backend/util/crud/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from uuid import uuid4
from pydantic import BaseModel
import semver
from sqlalchemy import select
from sqlalchemy import select, delete
from sqlalchemy.orm import Session
import csv

Expand Down Expand Up @@ -77,6 +77,15 @@ def read_assets(
return db.execute(query).scalars().all()


def read_assets_names(db: Session):
# Select all assets in db
query = select(Asset.asset_name)
# Execute the query and fetch all results
asset_names = db.execute(query).scalars().all()

return asset_names


def create_asset(db: Session, asset: AssetCreate, author_pennkey: str):
db_asset = Asset(
asset_name=asset.asset_name,
Expand Down Expand Up @@ -106,6 +115,11 @@ def update_asset(db: Session, asset_id: str, asset: AssetCreate):
return db_asset


def remove_asset(db: Session, asset_id: str):
db.execute(delete(Asset).where(Asset.id == asset_id))
db.commit()


def read_asset_exists(db: Session, asset_id: str):
try:
query = select(Asset.asset_name).filter(Asset.id == asset_id).limit(1)
Expand Down
20 changes: 20 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"@electron-toolkit/eslint-config-prettier": "^2.0.0",
"@electron-toolkit/eslint-config-ts": "^1.0.1",
"@electron-toolkit/tsconfig": "^1.0.1",
"@hookform/resolvers": "^3.3.4",
"@types/node": "^18.19.9",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
Expand Down Expand Up @@ -65,6 +66,7 @@
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3",
"vite": "^5.0.12",
"zod": "^3.22.5",
"zustand": "^4.5.2"
}
}
35 changes: 28 additions & 7 deletions frontend/src/renderer/src/components/forms/new-asset-form.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,53 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { useMemo } from 'react';
import { Controller, SubmitHandler, useFieldArray, useForm } from 'react-hook-form';
import { BiImageAdd } from 'react-icons/bi';
import { MdCheck, MdDelete } from 'react-icons/md';
import z from 'zod';

import { useAssetNames } from '@renderer/hooks/use-all-assets';
import { useAssetsSearchRefetch } from '@renderer/hooks/use-assets-search';
import useDownloads from '@renderer/hooks/use-downloads';
import { getAuthToken } from '@renderer/lib/auth';
import fetchClient from '@renderer/lib/fetch-client';
import { encodeThumbnailImage } from '@renderer/lib/image-util';
import { Asset } from '@renderer/types';
import ErrorMessage from '../input/error-message';
import KeywordsInput from '../input/keywords-input';
import Label from '../input/label';
import TextInput from '../input/text-input';

export interface NewAssetFormData {
assetName: string;
keywords: { keyword: string }[];
thumbnailFile: File;
}

export default function NewAssetForm({ afterSubmit }: { afterSubmit?: SubmitHandler<Asset> }) {
const refetchSearch = useAssetsSearchRefetch();
const { mutate: mutateDownloads } = useDownloads();

const { assetNames } = useAssetNames();

const newAssetSchema = useMemo(
() =>
z.object({
assetName: z
.string()
.regex(/^[a-z][A-Za-z0-9]*$/, 'Must be in camelCase with no special characters')
.refine((assetName) => assetNames?.indexOf(assetName) === -1, {
message: 'Asset with this name already exists',
}),
keywords: z.array(z.object({ keyword: z.string() })),
thumbnailFile: z.instanceof(File, { message: 'Thumbnail file is required' }),
}),
[assetNames],
);

type NewAssetFormData = z.infer<typeof newAssetSchema>;

const {
register,
control,
handleSubmit,
formState: { isSubmitting },
formState: { isSubmitting, errors },
} = useForm<NewAssetFormData>({
defaultValues: { assetName: '', keywords: [], thumbnailFile: undefined },
resolver: zodResolver(newAssetSchema),
});
// field array for keywords input
const keywordsFieldArray = useFieldArray({ control, name: 'keywords' });
Expand Down Expand Up @@ -82,6 +101,7 @@ export default function NewAssetForm({ afterSubmit }: { afterSubmit?: SubmitHand
label="Asset Name"
placeholder="myAwesomeAsset"
{...register('assetName', { required: true })}
errorMessage={errors.assetName?.message}
/>
<KeywordsInput fieldArrayReturn={keywordsFieldArray} />

Expand Down Expand Up @@ -139,6 +159,7 @@ export default function NewAssetForm({ afterSubmit }: { afterSubmit?: SubmitHand
)}
</div>
</div>
<ErrorMessage errorMessage={errors.thumbnailFile?.message} />
</label>
)}
/>
Expand Down
32 changes: 32 additions & 0 deletions frontend/src/renderer/src/hooks/use-all-assets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import useSWR, { useSWRConfig } from 'swr';

import fetchClient from '@renderer/lib/fetch-client';

export function useAssetNames() {
const {
data: assetNames,
error,
isLoading,
isValidating,
mutate,
} = useSWR('/api/v1/assets/names', async () => {
const { data, error, response } = await fetchClient.GET('/api/v1/assets/names');

if (error) throw error;
if (!response.status.toString().startsWith('2'))
throw new Error(`Non-OK response with code ${response.status}: ${response.statusText}`);

return data;
});

return { assetNames, error, isLoading, isValidating, mutate };
}

// (probably don't need this function)
export function useAssetNamesRefetch() {
const { mutate } = useSWRConfig();

return async () => {
await mutate('/api/v1/assets/names');
};
}
69 changes: 67 additions & 2 deletions frontend/src/types/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,29 @@ export interface paths {
*/
post: operations['new_asset_api_v1_assets__post'];
};
'/api/v1/assets/names': {
/**
* Get a list of asset names
* @description Used for fetching a list of the names of assets stored in the database.
*/
get: operations['get_assets_names_api_v1_assets_names_get'];
};
'/api/v1/assets/{uuid}': {
/**
* Get info about a specific asset
* @description Based on `uuid`, fetches information on a specific asset.
*/
get: operations['get_asset_info_api_v1_assets__uuid__get'];
/** Update asset metadata */
/**
* Update asset metadata
* @description Based on `uuid`, updates information for a specific asset.
*/
put: operations['put_asset_api_v1_assets__uuid__put'];
/**
* Delete asset metadata
* @description Based on `uuid`, deletes a specific asset.
*/
delete: operations['delete_asset_api_v1_assets__uuid__delete'];
};
'/api/v1/assets/{uuid}/versions': {
/** Get a list of versions for a given asset */
Expand Down Expand Up @@ -280,6 +295,24 @@ export interface operations {
};
};
};
/**
* Get a list of asset names
* @description Used for fetching a list of the names of assets stored in the database.
*/
get_assets_names_api_v1_assets_names_get: {
responses: {
/** @description Successful Response */
200: {
content: {
'application/json': string[];
};
};
/** @description Not found */
404: {
content: never;
};
};
};
/**
* Get info about a specific asset
* @description Based on `uuid`, fetches information on a specific asset.
Expand Down Expand Up @@ -309,7 +342,10 @@ export interface operations {
};
};
};
/** Update asset metadata */
/**
* Update asset metadata
* @description Based on `uuid`, updates information for a specific asset.
*/
put_asset_api_v1_assets__uuid__put: {
parameters: {
path: {
Expand Down Expand Up @@ -340,6 +376,35 @@ export interface operations {
};
};
};
/**
* Delete asset metadata
* @description Based on `uuid`, deletes a specific asset.
*/
delete_asset_api_v1_assets__uuid__delete: {
parameters: {
path: {
uuid: string;
};
};
responses: {
/** @description Successful Response */
200: {
content: {
'application/json': unknown;
};
};
/** @description Not found */
404: {
content: never;
};
/** @description Validation Error */
422: {
content: {
'application/json': components['schemas']['HTTPValidationError'];
};
};
};
};
/** Get a list of versions for a given asset */
get_asset_versions_api_v1_assets__uuid__versions_get: {
parameters: {
Expand Down

0 comments on commit e3dc131

Please sign in to comment.