Testing custom endpoint

This commit is contained in:
DrMint 2023-07-27 23:36:20 +02:00
parent 39d96bd6ba
commit 9b6e68113c
11 changed files with 2875 additions and 705 deletions

4
.ncurc.yml Normal file
View File

@ -0,0 +1,4 @@
upgrade: true
interactive: true
format: "group"
reject:

3189
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -15,26 +15,29 @@
"generate:graphQLSchema": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:graphQLSchema",
"prettier": "prettier --list-different --end-of-line auto --write src",
"tsc": "tsc --noEmit",
"precommit": "npm run generate:types && npm run prettier && npm run tsc"
"precommit": "npm run generate:types && npm run prettier && npm run tsc",
"upgrade": "ncu"
},
"dependencies": {
"@fontsource/vollkorn": "^5.0.5",
"clean-deep": "^3.4.0",
"cross-env": "^7.0.3",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"iso-639-1": "^2.1.15",
"payload": "^1.11.1",
"payload": "^1.11.7",
"slugify": "^1.6.6",
"styled-components": "^6.0.5"
"styled-components": "^6.0.5",
"unset-value": "^2.0.1"
},
"devDependencies": {
"@types/dotenv": "^8.2.0",
"@types/express": "^4.17.9",
"@types/express": "^4.17.17",
"@types/react-router-dom": "^5.3.3",
"copyfiles": "^2.4.1",
"nodemon": "^2.0.6",
"nodemon": "^3.0.1",
"prettier": "^3.0.0",
"ts-node": "^9.1.1",
"typescript": "^4.8.4"
"ts-node": "^10.9.1",
"typescript": "^5.1.6"
}
}

View File

@ -7,7 +7,6 @@ import {
} from "../../constants";
import { slugField } from "../../fields/slugField/slugField";
import { imageField } from "../../fields/imageField/imageField";
import { isDefined, isUndefined } from "../../utils/asserts";
import { LibraryItemThumbnails } from "../LibraryItemThumbnails/LibraryItemThumbnails";
import { LibraryItem } from "../../types/collections";
import { Keys } from "../Keys/Keys";
@ -18,6 +17,8 @@ import { beforeDuplicateUnpublish } from "../../hooks/beforeDuplicateUnpublish";
import { beforeDuplicatePiping } from "../../hooks/beforeDuplicatePiping";
import { Currencies } from "../Currencies/Currencies";
import { optionalGroupField } from "../../fields/optionalGroupField/optionalGroupField";
import { RowLabel } from "./components/RowLabel";
import { getSlug } from "./endpoints/getSlug";
const fields = {
status: "status",
@ -70,7 +71,7 @@ export const LibraryItems = buildVersionedCollectionConfig(
singular: "Library Item",
plural: "Library Items",
},
() => ({
({ slug }) => ({
defaultSort: fields.slug,
admin: {
useAsTitle: fields.slug,
@ -87,6 +88,7 @@ export const LibraryItems = buildVersionedCollectionConfig(
},
preview: (doc) => `https://accords-library.com/library/${doc.slug}`,
},
endpoints: [getSlug(slug)],
fields: [
{
type: "row",
@ -239,6 +241,12 @@ export const LibraryItems = buildVersionedCollectionConfig(
{
name: fields.scansPages,
type: "array",
admin: {
initCollapsed: true,
components: {
RowLabel: ({ data }) => RowLabel(data),
},
},
fields: [
{
type: "row",

View File

@ -0,0 +1,27 @@
import React from "react";
import { styled } from "styled-components";
import { isDefined } from "../../../utils/asserts";
import { formatLanguageCode, shortenEllipsis } from "../../../utils/string";
interface Props {
page?: number;
image?: string;
}
const Container = styled.div`
display: flex;
place-items: center;
gap: 10px;
`;
const Title = styled.div`
font-weight: 600;
font-size: 1.2rem;
`;
export const RowLabel = ({ page, image }: Props): JSX.Element => (
<Container>
{isDefined(page) && <div className="pill pill--style-white">{`Page ${page}`}</div>}
{isDefined(image) && <Title>{image}</Title>}
</Container>
);

View File

@ -0,0 +1,56 @@
import { LibraryItems } from "../LibraryItems";
import { LibraryItem } from "../../../types/collections";
import cleanDeep from "clean-deep";
import { createBySlugEndpoint } from "../../../endpoints/createBySlugEndpoint";
type ProcessedLibraryItem = Omit<LibraryItem, "size" | "price" | "scans" | "id"> & {
size?: Omit<LibraryItem["size"][number], "id">;
price?: Omit<LibraryItem["price"][number], "id" | "currency"> & { currency: string };
scans?: Omit<LibraryItem["scans"][number], "id" | "obibelt" | "cover" | "dustjacket"> & {
obibelt: Omit<LibraryItem["scans"][number]["obibelt"][number], "id">;
cover: Omit<LibraryItem["scans"][number]["obibelt"][number], "id">;
dustjacket: Omit<LibraryItem["scans"][number]["obibelt"][number], "id">;
};
};
export const getSlug = (collectionSlug: string) =>
createBySlugEndpoint<LibraryItem>(collectionSlug, ({ id, size, price, scans, ...otherProps }) => {
const processedLibraryItem: ProcessedLibraryItem = {
size: processOptionalGroup(size),
price: processPrice(price),
scans: processScans(scans),
...otherProps,
};
return cleanDeep(processedLibraryItem, {
emptyStrings: false,
emptyArrays: false,
emptyObjects: false,
nullValues: true,
undefinedValues: true,
NaNValues: false,
});
});
const processScans = (scans: LibraryItem["scans"]): ProcessedLibraryItem["scans"] => {
if (!scans || scans.length === 0) return undefined;
const { cover, dustjacket, id, obibelt, ...otherProps } = scans[0];
return {
cover: processOptionalGroup(cover),
dustjacket: processOptionalGroup(dustjacket),
obibelt: processOptionalGroup(obibelt),
...otherProps,
};
};
const processPrice = (price: LibraryItem["price"]): ProcessedLibraryItem["price"] => {
if (!price || price.length === 0) return undefined;
const { currency, ...otherProps } = processOptionalGroup(price);
return { ...otherProps, currency: typeof currency === "string" ? currency : currency.id };
};
const processOptionalGroup = <T extends { id?: string }>(group: T[] | null | undefined) => {
if (!group || group.length === 0) return undefined;
const { id, ...otherProps } = group[0];
return otherProps;
};

View File

@ -0,0 +1,32 @@
import payload from "payload";
import { CollectionConfig } from "payload/types";
export const createBySlugEndpoint = <T>(
collection: string,
handler: (doc: T) => unknown
): CollectionConfig["endpoints"][number] => ({
path: "/slug/:slug",
method: "get",
handler: async (req, res) => {
if (!req.user) {
return res.status(403).send({
errors: [
{
message: "You are not allowed to perform this action.",
},
],
});
}
const result = await payload.find({
collection,
where: { slug: { equals: req.params.slug } },
});
if (result.docs.length === 0) {
return res.sendStatus(404);
}
res.status(200).send(handler(result.docs[0]));
},
});

View File

@ -2,21 +2,29 @@ import React from "react";
import { isDefined } from "../../utils/asserts";
import { formatLanguageCode, shortenEllipsis } from "../../utils/string";
import { Language } from "../../types/collections";
import { styled } from "styled-components";
interface Props {
language?: Language | string;
title?: string;
}
export const RowLabel = ({ language, title }: Props): JSX.Element => {
return (
<div style={{ display: "flex", placeItems: "center", gap: 10 }}>
{isDefined(language) && typeof language === "string" && (
<div className="pill pill--style-white">{formatLanguageCode(language)}</div>
)}
{isDefined(title) && (
<div style={{ fontWeight: 600, fontSize: "1.2rem" }}>{shortenEllipsis(title, 50)}</div>
)}
</div>
);
};
const Container = styled.div`
display: flex;
place-items: center;
gap: 10px;
`;
const Title = styled.div`
font-weight: 600;
font-size: 1.2rem;
`;
export const RowLabel = ({ language, title }: Props): JSX.Element => (
<Container>
{isDefined(language) && typeof language === "string" && (
<div className="pill pill--style-white">{formatLanguageCode(language)}</div>
)}
{isDefined(title) && <Title>{shortenEllipsis(title, 50)}</Title>}
</Container>
);

View File

@ -69,11 +69,6 @@ html[data-theme="light"] {
.collapsible__actions-wrap {
z-index: 1;
.array-actions__add,
.array-actions__duplicate {
display: none;
}
}
}
}

View File

@ -25,16 +25,17 @@ export type ContentFoldersTranslation = {
export interface Config {
collections: {
'library-items': LibraryItem;
"library-items": LibraryItem;
contents: Content;
'content-folders': ContentFolder;
"content-folders": ContentFolder;
posts: Post;
'content-thumbnails': ContentThumbnail;
'library-item-thumbnails': LibraryItemThumbnail;
'recorder-thumbnails': RecorderThumbnail;
'post-thumbnails': PostThumbnail;
"content-thumbnails": ContentThumbnail;
"library-item-thumbnails": LibraryItemThumbnail;
"recorder-thumbnails": RecorderThumbnail;
"post-thumbnails": PostThumbnail;
files: File;
languages: Language;
currencies: Currency;
recorders: Recorder;
keys: Key;
};
@ -51,6 +52,32 @@ export interface LibraryItem {
primary: boolean;
digital: boolean;
downloadable: boolean;
scans?: {
cover?: {
front?: string | LibraryItemThumbnail;
spine?: string | LibraryItemThumbnail;
back?: string | LibraryItemThumbnail;
id?: string;
}[];
dustjacket?: {
front?: string | LibraryItemThumbnail;
spine?: string | LibraryItemThumbnail;
back?: string | LibraryItemThumbnail;
id?: string;
}[];
obibelt?: {
front?: string | LibraryItemThumbnail;
spine?: string | LibraryItemThumbnail;
back?: string | LibraryItemThumbnail;
id?: string;
}[];
pages?: {
page: number;
image: string | LibraryItemThumbnail;
id?: string;
}[];
id?: string;
}[];
size?: {
width: number;
height: number;
@ -58,49 +85,50 @@ export interface LibraryItem {
id?: string;
}[];
price?: {
priceAmount: number;
amount: number;
currency: string | Currency;
id?: string;
}[];
itemType?: 'Textual' | 'Audio' | 'Video' | 'Game' | 'Other';
itemType?: "Textual" | "Audio" | "Video" | "Game" | "Other";
textual?: {
subtype?:
| {
value: string;
relationTo: 'keys';
relationTo: "keys";
}[]
| {
value: Key;
relationTo: 'keys';
relationTo: "keys";
}[];
languages?:
| {
value: string;
relationTo: 'languages';
relationTo: "languages";
}[]
| {
value: Language;
relationTo: 'languages';
relationTo: "languages";
}[];
pageCount?: number;
bindingType?: 'Paperback' | 'Hardcover';
pageOrder?: 'LeftToRight' | 'RightToLeft';
bindingType?: "Paperback" | "Hardcover";
pageOrder?: "LeftToRight" | "RightToLeft";
};
audio?: {
audioSubtype?:
| {
value: string;
relationTo: 'keys';
relationTo: "keys";
}[]
| {
value: Key;
relationTo: 'keys';
relationTo: "keys";
}[];
};
releaseDate?: string;
lastModifiedBy: string | Recorder;
updatedBy: string | Recorder;
updatedAt: string;
createdAt: string;
_status?: 'draft' | 'published';
_status?: "draft" | "published";
}
export interface LibraryItemThumbnail {
id: string;
@ -139,20 +167,23 @@ export interface LibraryItemThumbnail {
};
};
}
export interface Currency {
id: string;
}
export interface Key {
id: string;
slug: string;
type:
| 'Contents'
| 'LibraryAudio'
| 'LibraryVideo'
| 'LibraryTextual'
| 'LibraryGroup'
| 'Library'
| 'Weapons'
| 'GamePlatforms'
| 'Categories'
| 'Wordings';
| "Contents"
| "LibraryAudio"
| "LibraryVideo"
| "LibraryTextual"
| "LibraryGroup"
| "Library"
| "Weapons"
| "GamePlatforms"
| "Categories"
| "Wordings";
translations?: CategoryTranslations;
}
export interface Language {
@ -165,7 +196,7 @@ export interface Recorder {
avatar?: string | RecorderThumbnail;
languages?: string[] | Language[];
biographies?: RecorderBiographies;
role?: ('Admin' | 'Recorder')[];
role?: ("Admin" | "Recorder")[];
anonymize: boolean;
email: string;
resetPasswordToken?: string;
@ -212,15 +243,15 @@ export interface Content {
categories?:
| {
value: string;
relationTo: 'keys';
relationTo: "keys";
}[]
| {
value: Key;
relationTo: 'keys';
relationTo: "keys";
}[];
type?: {
value: string | Key;
relationTo: 'keys';
relationTo: "keys";
};
translations: {
language: string | Language;
@ -239,10 +270,10 @@ export interface Content {
audio?: string | File;
id?: string;
}[];
lastModifiedBy: string | Recorder;
updatedBy: string | Recorder;
updatedAt: string;
createdAt: string;
_status?: 'draft' | 'published';
_status?: "draft" | "published";
}
export interface ContentThumbnail {
id: string;
@ -279,19 +310,25 @@ export interface TextBlock {
}[];
id?: string;
blockName?: string;
blockType: 'textBlock';
blockType: "textBlock";
}
export interface Section {
content?: (Section_Section | Section_Tabs | TranscriptBlock | QuoteBlock | TextBlock)[];
id?: string;
blockName?: string;
blockType: 'section';
blockType: "section";
}
export interface Section_Section {
content?: (Section_Section_Section | Section_Section_Tabs | TranscriptBlock | QuoteBlock | TextBlock)[];
content?: (
| Section_Section_Section
| Section_Section_Tabs
| TranscriptBlock
| QuoteBlock
| TextBlock
)[];
id?: string;
blockName?: string;
blockType: 'section';
blockType: "section";
}
export interface Section_Section_Section {
content?: (
@ -303,25 +340,25 @@ export interface Section_Section_Section {
)[];
id?: string;
blockName?: string;
blockType: 'section';
blockType: "section";
}
export interface Section_Section_Section_Section {
content?: (Section_Section_Section_Section_Section | TranscriptBlock | QuoteBlock | TextBlock)[];
id?: string;
blockName?: string;
blockType: 'section';
blockType: "section";
}
export interface Section_Section_Section_Section_Section {
content?: (TranscriptBlock | QuoteBlock | TextBlock)[];
id?: string;
blockName?: string;
blockType: 'section';
blockType: "section";
}
export interface TranscriptBlock {
lines: (LineBlock | CueBlock)[];
id?: string;
blockName?: string;
blockType: 'transcriptBlock';
blockType: "transcriptBlock";
}
export interface LineBlock {
content: {
@ -329,13 +366,13 @@ export interface LineBlock {
}[];
id?: string;
blockName?: string;
blockType: 'lineBlock';
blockType: "lineBlock";
}
export interface CueBlock {
content: string;
id?: string;
blockName?: string;
blockType: 'cueBlock';
blockType: "cueBlock";
}
export interface QuoteBlock {
from: string;
@ -344,120 +381,120 @@ export interface QuoteBlock {
}[];
id?: string;
blockName?: string;
blockType: 'quoteBlock';
blockType: "quoteBlock";
}
export interface Section_Section_Section_Tabs {
tabs?: Section_Section_Section_Tabs_Tab[];
id?: string;
blockName?: string;
blockType: 'tabs';
blockType: "tabs";
}
export interface Section_Section_Section_Tabs_Tab {
content?: (Section_Section_Section_Tabs_Tab_Section | TranscriptBlock | QuoteBlock | TextBlock)[];
id?: string;
blockName?: string;
blockType: 'tab';
blockType: "tab";
}
export interface Section_Section_Section_Tabs_Tab_Section {
content?: (TranscriptBlock | QuoteBlock | TextBlock)[];
id?: string;
blockName?: string;
blockType: 'section';
blockType: "section";
}
export interface Section_Section_Tabs {
tabs?: Section_Section_Tabs_Tab[];
id?: string;
blockName?: string;
blockType: 'tabs';
blockType: "tabs";
}
export interface Section_Section_Tabs_Tab {
content?: (Section_Section_Tabs_Tab_Section | TranscriptBlock | QuoteBlock | TextBlock)[];
id?: string;
blockName?: string;
blockType: 'tab';
blockType: "tab";
}
export interface Section_Section_Tabs_Tab_Section {
content?: (Section_Section_Tabs_Tab_Section_Section | TranscriptBlock | QuoteBlock | TextBlock)[];
id?: string;
blockName?: string;
blockType: 'section';
blockType: "section";
}
export interface Section_Section_Tabs_Tab_Section_Section {
content?: (TranscriptBlock | QuoteBlock | TextBlock)[];
id?: string;
blockName?: string;
blockType: 'section';
blockType: "section";
}
export interface Section_Tabs {
tabs?: Section_Tabs_Tab[];
id?: string;
blockName?: string;
blockType: 'tabs';
blockType: "tabs";
}
export interface Section_Tabs_Tab {
content?: (Section_Tabs_Tab_Section | TranscriptBlock | QuoteBlock | TextBlock)[];
id?: string;
blockName?: string;
blockType: 'tab';
blockType: "tab";
}
export interface Section_Tabs_Tab_Section {
content?: (Section_Tabs_Tab_Section_Section | TranscriptBlock | QuoteBlock | TextBlock)[];
id?: string;
blockName?: string;
blockType: 'section';
blockType: "section";
}
export interface Section_Tabs_Tab_Section_Section {
content?: (Section_Tabs_Tab_Section_Section_Section | TranscriptBlock | QuoteBlock | TextBlock)[];
id?: string;
blockName?: string;
blockType: 'section';
blockType: "section";
}
export interface Section_Tabs_Tab_Section_Section_Section {
content?: (TranscriptBlock | QuoteBlock | TextBlock)[];
id?: string;
blockName?: string;
blockType: 'section';
blockType: "section";
}
export interface Tabs {
tabs?: Tabs_Tab[];
id?: string;
blockName?: string;
blockType: 'tabs';
blockType: "tabs";
}
export interface Tabs_Tab {
content?: (Tabs_Tab_Section | TranscriptBlock | QuoteBlock | TextBlock)[];
id?: string;
blockName?: string;
blockType: 'tab';
blockType: "tab";
}
export interface Tabs_Tab_Section {
content?: (Tabs_Tab_Section_Section | TranscriptBlock | QuoteBlock | TextBlock)[];
id?: string;
blockName?: string;
blockType: 'section';
blockType: "section";
}
export interface Tabs_Tab_Section_Section {
content?: (Tabs_Tab_Section_Section_Section | TranscriptBlock | QuoteBlock | TextBlock)[];
id?: string;
blockName?: string;
blockType: 'section';
blockType: "section";
}
export interface Tabs_Tab_Section_Section_Section {
content?: (Tabs_Tab_Section_Section_Section_Section | TranscriptBlock | QuoteBlock | TextBlock)[];
id?: string;
blockName?: string;
blockType: 'section';
blockType: "section";
}
export interface Tabs_Tab_Section_Section_Section_Section {
content?: (TranscriptBlock | QuoteBlock | TextBlock)[];
id?: string;
blockName?: string;
blockType: 'section';
blockType: "section";
}
export interface File {
id: string;
filename: string;
type: 'LibraryScans' | 'LibrarySoundtracks' | 'ContentVideo' | 'ContentAudio';
type: "LibraryScans" | "LibrarySoundtracks" | "ContentVideo" | "ContentAudio";
updatedAt: string;
createdAt: string;
}
@ -468,20 +505,20 @@ export interface ContentFolder {
subfolders?:
| {
value: string;
relationTo: 'content-folders';
relationTo: "content-folders";
}[]
| {
value: ContentFolder;
relationTo: 'content-folders';
relationTo: "content-folders";
}[];
contents?:
| {
value: string;
relationTo: 'contents';
relationTo: "contents";
}[]
| {
value: Content;
relationTo: 'contents';
relationTo: "contents";
}[];
}
export interface Post {
@ -491,20 +528,20 @@ export interface Post {
authors:
| {
value: string;
relationTo: 'recorders';
relationTo: "recorders";
}[]
| {
value: Recorder;
relationTo: 'recorders';
relationTo: "recorders";
}[];
categories?:
| {
value: string;
relationTo: 'keys';
relationTo: "keys";
}[]
| {
value: Key;
relationTo: 'keys';
relationTo: "keys";
}[];
translations: {
language: string | Language;
@ -520,10 +557,10 @@ export interface Post {
}[];
publishedDate: string;
hidden?: boolean;
lastModifiedBy: string | Recorder;
updatedBy: string | Recorder;
updatedAt: string;
createdAt: string;
_status?: 'draft' | 'published';
_status?: "draft" | "published";
}
export interface PostThumbnail {
id: string;

View File

@ -6,18 +6,15 @@ import {
} from "./collectionConfig";
import { Recorders } from "../collections/Recorders/Recorders";
const fields = { lastModifiedBy: "lastModifiedBy" };
const fields = { updatedBy: "updatedBy" };
const beforeChangeLastModifiedBy: CollectionBeforeChangeHook = async ({
data: { updatedBy, ...data },
req,
}) => ({
const beforeChangeUpdatedBy: CollectionBeforeChangeHook = async ({ data, req }) => ({
...data,
[fields.lastModifiedBy]: req.user.id,
[fields.updatedBy]: req.user.id,
});
const lastModifiedByField = (): RelationshipField => ({
name: fields.lastModifiedBy,
const updatedByField = (): RelationshipField => ({
name: fields.updatedBy,
type: "relationship",
required: true,
relationTo: Recorders.slug,
@ -42,8 +39,8 @@ export const buildVersionedCollectionConfig = (
versions: { drafts: { autosave: { interval: 2000 } } },
hooks: {
...otherHooks,
beforeChange: [...(beforeChange ?? []), beforeChangeLastModifiedBy],
beforeChange: [...(beforeChange ?? []), beforeChangeUpdatedBy],
},
fields: [...fields, lastModifiedByField()],
fields: [...fields, updatedByField()],
};
};