Added generated SDK + route

This commit is contained in:
DrMint 2023-08-21 02:53:33 +02:00
parent 084e726af7
commit f454b22569
4 changed files with 273 additions and 3 deletions

View File

@ -1,6 +1,5 @@
import { RowLabelArgs } from "payload/dist/admin/components/forms/RowLabel/types"; import { RowLabelArgs } from "payload/dist/admin/components/forms/RowLabel/types";
import { CollectionGroups, Collections, KeysTypes } from "../../constants"; import { CollectionGroups, Collections, KeysTypes } from "../../constants";
import { createGetByEndpoint } from "../../endpoints/createGetByEndpoint";
import { createGetSlugsEndpoint } from "../../endpoints/createGetSlugsEndpoint"; import { createGetSlugsEndpoint } from "../../endpoints/createGetSlugsEndpoint";
import { imageField } from "../../fields/imageField/imageField"; import { imageField } from "../../fields/imageField/imageField";
import { keysField } from "../../fields/keysField/keysField"; import { keysField } from "../../fields/keysField/keysField";
@ -8,6 +7,7 @@ import { slugField } from "../../fields/slugField/slugField";
import { translatedFields } from "../../fields/translatedFields/translatedFields"; import { translatedFields } from "../../fields/translatedFields/translatedFields";
import { buildVersionedCollectionConfig } from "../../utils/versionedCollectionConfig"; import { buildVersionedCollectionConfig } from "../../utils/versionedCollectionConfig";
import { AppearanceRowLabel } from "./components/AppearanceRowLabel"; import { AppearanceRowLabel } from "./components/AppearanceRowLabel";
import { getBySlugEndpoint } from "./endpoints/getBySlugEndpoint";
import { importFromStrapi } from "./endpoints/importFromStrapi"; import { importFromStrapi } from "./endpoints/importFromStrapi";
const fields = { const fields = {
@ -46,7 +46,7 @@ export const Weapons = buildVersionedCollectionConfig({
endpoints: [ endpoints: [
importFromStrapi, importFromStrapi,
createGetSlugsEndpoint(Collections.Weapons), createGetSlugsEndpoint(Collections.Weapons),
createGetByEndpoint(Collections.Weapons, "slug"), getBySlugEndpoint,
], ],
fields: [ fields: [
{ {

View File

@ -0,0 +1,124 @@
import { Collections } from "../../../constants";
import { createGetByEndpoint } from "../../../endpoints/createGetByEndpoint";
import { EndpointBasicWeapon, EndpointWeapon } from "../../../sdk";
import { Key, Language, Recorder, Weapon, WeaponsThumbnail } from "../../../types/collections";
import { isDefined, isUndefined } from "../../../utils/asserts";
export const getBySlugEndpoint = createGetByEndpoint(
Collections.Weapons,
"slug",
(weapon: Weapon): EndpointWeapon => {
let group: EndpointWeapon["group"] = undefined;
// We only send the group if the group has at least 2 weapons (1 weapon beside the current one)
// The weapons are ordered alphabetically using their slugs
if (
typeof weapon.group === "object" &&
isDefined(weapon.group.weapons) &&
weapon.group.weapons.length > 1
) {
const { slug, translations = [], weapons } = weapon.group;
const groupWeapons: EndpointBasicWeapon[] = [];
weapons.forEach((groupWeapon) => {
if (typeof groupWeapon === "object" && groupWeapon.id !== weapon.id) {
groupWeapons.push(convertWeaponToEndpointBasicWeapon(groupWeapon));
}
});
groupWeapons.sort((a, b) => a.slug.localeCompare(b.slug));
group = {
slug,
translations: translations.map(({ language, name }) => ({
language: getLanguageId(language),
name,
})),
weapons: groupWeapons,
};
}
return {
...convertWeaponToEndpointBasicWeapon(weapon),
appearances: weapon.appearances.map(({ categories, translations }) => ({
categories: categories.map(getKeyId),
translations: translations.map(
({
language,
sourceLanguage,
transcribers = [],
translators = [],
proofreaders = [],
...otherTranslatedProps
}) => ({
language: getLanguageId(language),
sourceLanguage: getLanguageId(sourceLanguage),
transcribers: transcribers.map(getRecorderId),
translators: translators.map(getRecorderId),
proofreaders: proofreaders.map(getRecorderId),
...otherTranslatedProps,
})
),
})),
group,
};
}
);
const getRecorderId = (recorder: string | Recorder) =>
typeof recorder === "object" ? recorder.id : recorder;
const getKeyId = (key: string | Key) => (typeof key === "object" ? key.id : key);
const getLanguageId = (language: string | Language) =>
typeof language === "object" ? language.id : language;
const getThumbnail = (thumbnail?: string | WeaponsThumbnail): WeaponsThumbnail | undefined => {
if (isUndefined(thumbnail)) return undefined;
if (typeof thumbnail === "string") return undefined;
delete thumbnail.weapon;
return thumbnail;
};
const convertWeaponToEndpointBasicWeapon = ({
slug,
thumbnail,
type,
appearances,
}: Weapon): EndpointBasicWeapon => {
const categories = new Set<string>();
appearances.forEach((appearance) =>
appearance.categories.forEach((category) => categories.add(getKeyId(category)))
);
const languages = new Set<string>();
appearances.forEach(({ translations }) =>
translations.forEach(({ language }) => languages.add(getLanguageId(language)))
);
const translations: EndpointWeapon["translations"] = [...languages.values()].map(
(targetLanguage) => {
const names = new Set<string>();
appearances.forEach(({ translations }) => {
const translation = translations.find(
({ language }) => getLanguageId(language) === targetLanguage
);
if (translation) {
names.add(translation.name);
}
});
const [name, ...aliases] = names;
if (isUndefined(name))
throw new Error("A weapon should always have a name for each of its translatiion");
return { language: targetLanguage, name: name, aliases };
}
);
return {
slug,
thumbnail: getThumbnail(thumbnail),
type: getKeyId(type),
categories: [...categories.values()],
translations,
};
};

129
src/sdk.ts Normal file
View File

@ -0,0 +1,129 @@
import { Collections } from "./constants";
import { WeaponsThumbnail } from "./types/collections";
class NodeCache {
constructor(_params: any) {}
getTtl(_key: string): number | undefined {
return undefined;
}
get<T>(_key: string): T | undefined {
return undefined;
}
set<T>(_key: string, _value: T, _ttl: number | string) {}
}
// END MOCKING SECTION
const REFRESH_FREQUENCY_IN_SEC = 60;
const CACHE = new NodeCache({
checkperiod: REFRESH_FREQUENCY_IN_SEC,
deleteOnExpire: true,
forceString: true,
maxKeys: 1,
});
const TOKEN_KEY = "token";
type PayloadLoginResponse = {
token: string;
exp: number;
};
const refreshToken = async () => {
const loginUrl = payloadApiUrl(Collections.Recorders, "login");
const loginResult = await fetch(loginUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: process.env.PAYLOAD_USER,
password: process.env.PAYLOAD_PASSWORD,
}),
});
logResponse(loginResult);
if (loginResult.status !== 200) {
throw new Error("Unable to login");
}
const loginJson = (await loginResult.json()) as PayloadLoginResponse;
const { token, exp } = loginJson;
const now = Math.floor(Date.now() / 1000);
const ttl = Math.floor(exp - now - REFRESH_FREQUENCY_IN_SEC * 2);
const ttlInMinutes = Math.floor(ttl / 60);
console.log("Token was refreshed. TTL is", ttlInMinutes, "minutes.");
CACHE.set(TOKEN_KEY, token, ttl);
return token;
};
const getToken = async (): Promise<string> => {
const cachedToken = CACHE.get<string>(TOKEN_KEY);
if (cachedToken !== undefined) {
const cachedTokenTtl = CACHE.getTtl(TOKEN_KEY) as number;
const diffInMinutes = Math.floor((cachedTokenTtl - Date.now()) / 1000 / 60);
console.log("Retrieved token from cache. TTL is", diffInMinutes, "minutes.");
return cachedToken;
}
console.log("Refreshing token");
return await refreshToken();
};
const injectAuth = async (init?: RequestInit): Promise<RequestInit> => ({
...init,
headers: { ...init?.headers, Authorization: `JWT ${await getToken()}` },
});
const logResponse = (res: Response) => console.log(res.status, res.statusText, res.url);
const payloadApiUrl = (collection: Collections, endpoint?: string): string =>
`${process.env.PAYLOAD_API_URL}/${collection}${endpoint === undefined ? "" : `/${endpoint}`}`;
const request = async (url: string, init?: RequestInit): Promise<Response> => {
const result = await fetch(url, await injectAuth(init));
logResponse(result);
if (result.status !== 200) {
throw new Error("Unhandled fetch error");
}
return result;
};
// SDK and Types
export type EndpointWeapon = EndpointBasicWeapon & {
appearances: {
categories: string[];
translations: {
language: string;
sourceLanguage: string;
name: string;
description?: string;
level1?: string;
level2?: string;
level3?: string;
level4?: string;
transcribers: string[];
translators: string[];
proofreaders: string[];
}[];
}[];
group?: {
slug: string;
translations: { language: string; name: string }[];
weapons: EndpointBasicWeapon[];
};
};
export type EndpointBasicWeapon = {
slug: string;
type: string;
categories: string[];
translations: { language: string; name: string; aliases: string[] }[];
thumbnail?: WeaponsThumbnail;
};
export const payload = {
getSlugsWeapons: async (): Promise<string[]> =>
await (await request(payloadApiUrl(Collections.Weapons, "slugs"))).json(),
getWeapon: async (slug: string): Promise<EndpointWeapon> =>
await (await request(payloadApiUrl(Collections.Weapons, `slug/${slug}`))).json(),
};

View File

@ -1,5 +1,6 @@
import "dotenv/config"; import "dotenv/config";
import express from "express"; import express from "express";
import { readFileSync } from "fs";
import path from "path"; import path from "path";
import payload from "payload"; import payload from "payload";
import { Collections, RecordersRoles } from "./constants"; import { Collections, RecordersRoles } from "./constants";
@ -46,7 +47,7 @@ const start = async () => {
email: process.env.SEEDING_ADMIN_EMAIL, email: process.env.SEEDING_ADMIN_EMAIL,
password: process.env.SEEDING_ADMIN_PASSWORD, password: process.env.SEEDING_ADMIN_PASSWORD,
username: process.env.SEEDING_ADMIN_USERNAME, username: process.env.SEEDING_ADMIN_USERNAME,
role: [RecordersRoles.Admin], role: [RecordersRoles.Admin, RecordersRoles.Api],
anonymize: false, anonymize: false,
}; };
await payload.create({ await payload.create({
@ -61,6 +62,22 @@ const start = async () => {
// Add your own express routes here // Add your own express routes here
app.use("/public", express.static(path.join(__dirname, "../public"))); app.use("/public", express.static(path.join(__dirname, "../public")));
app.get("/api/sdk", (_, res) => {
const collections = readFileSync(path.join(__dirname, "types/collections.ts"), "utf-8");
const constantsHeader = "/////////////// CONSTANTS ///////////////\n";
const constants = readFileSync(path.join(__dirname, "constants.ts"), "utf-8");
const sdkHeader = "////////////////// SDK //////////////////\n";
const sdkLines = readFileSync(path.join(__dirname, "sdk.ts"), "utf-8").split("\n");
const endMockingLine = sdkLines.findIndex((line) => line === "// END MOCKING SECTION") ?? 0;
const sdk =
`import NodeCache from "node-cache";\n\n` + sdkLines.slice(endMockingLine + 1).join("\n");
res.type("text/plain");
res.send([collections, constantsHeader, constants, sdkHeader, sdk].join("\n\n"));
});
app.get("/robots.txt", (_, res) => { app.get("/robots.txt", (_, res) => {
res.type("text/plain"); res.type("text/plain");
res.send("User-agent: *\nDisallow: /"); res.send("User-agent: *\nDisallow: /");