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 { 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: [
|
||||||
{
|
{
|
||||||
|
|
|
@ -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 "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: /");
|
||||||
|
|
Loading…
Reference in New Issue