Added generated SDK + route
This commit is contained in:
parent
084e726af7
commit
f454b22569
|
@ -1,6 +1,5 @@
|
|||
import { RowLabelArgs } from "payload/dist/admin/components/forms/RowLabel/types";
|
||||
import { CollectionGroups, Collections, KeysTypes } from "../../constants";
|
||||
import { createGetByEndpoint } from "../../endpoints/createGetByEndpoint";
|
||||
import { createGetSlugsEndpoint } from "../../endpoints/createGetSlugsEndpoint";
|
||||
import { imageField } from "../../fields/imageField/imageField";
|
||||
import { keysField } from "../../fields/keysField/keysField";
|
||||
|
@ -8,6 +7,7 @@ import { slugField } from "../../fields/slugField/slugField";
|
|||
import { translatedFields } from "../../fields/translatedFields/translatedFields";
|
||||
import { buildVersionedCollectionConfig } from "../../utils/versionedCollectionConfig";
|
||||
import { AppearanceRowLabel } from "./components/AppearanceRowLabel";
|
||||
import { getBySlugEndpoint } from "./endpoints/getBySlugEndpoint";
|
||||
import { importFromStrapi } from "./endpoints/importFromStrapi";
|
||||
|
||||
const fields = {
|
||||
|
@ -46,7 +46,7 @@ export const Weapons = buildVersionedCollectionConfig({
|
|||
endpoints: [
|
||||
importFromStrapi,
|
||||
createGetSlugsEndpoint(Collections.Weapons),
|
||||
createGetByEndpoint(Collections.Weapons, "slug"),
|
||||
getBySlugEndpoint,
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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(),
|
||||
};
|
|
@ -1,5 +1,6 @@
|
|||
import "dotenv/config";
|
||||
import express from "express";
|
||||
import { readFileSync } from "fs";
|
||||
import path from "path";
|
||||
import payload from "payload";
|
||||
import { Collections, RecordersRoles } from "./constants";
|
||||
|
@ -46,7 +47,7 @@ const start = async () => {
|
|||
email: process.env.SEEDING_ADMIN_EMAIL,
|
||||
password: process.env.SEEDING_ADMIN_PASSWORD,
|
||||
username: process.env.SEEDING_ADMIN_USERNAME,
|
||||
role: [RecordersRoles.Admin],
|
||||
role: [RecordersRoles.Admin, RecordersRoles.Api],
|
||||
anonymize: false,
|
||||
};
|
||||
await payload.create({
|
||||
|
@ -61,6 +62,22 @@ const start = async () => {
|
|||
// Add your own express routes here
|
||||
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) => {
|
||||
res.type("text/plain");
|
||||
res.send("User-agent: *\nDisallow: /");
|
||||
|
|
Loading…
Reference in New Issue