Added files
This commit is contained in:
commit
231ffa472b
|
@ -0,0 +1,2 @@
|
||||||
|
.env
|
||||||
|
node_modules
|
|
@ -0,0 +1,8 @@
|
||||||
|
version: "3.3"
|
||||||
|
services:
|
||||||
|
meilisearch:
|
||||||
|
image: "getmeili/meilisearch:latest"
|
||||||
|
ports:
|
||||||
|
- "61876:7700"
|
||||||
|
volumes:
|
||||||
|
- "./meili_data:/meili_data"
|
|
@ -0,0 +1,20 @@
|
||||||
|
require("dotenv").config();
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
overwrite: true,
|
||||||
|
schema: {
|
||||||
|
[process.env.URL_GRAPHQL]: {
|
||||||
|
headers: { Authorization: `Bearer ${process.env.STRAPI_API_TOKEN}` },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
documents: [
|
||||||
|
"src/shared/fragments/*.graphql",
|
||||||
|
"src/core/graphql/operations/*.graphql",
|
||||||
|
"src/core/graphql/fragments/*.graphql",
|
||||||
|
],
|
||||||
|
generates: {
|
||||||
|
"src/core/graphql/generated.ts": {
|
||||||
|
plugins: ["typescript", "typescript-operations", "typescript-graphql-request"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"name": "search.accords-library.com",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"generate": "graphql-codegen --config graphql-codegen.config.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@digitak/esrun": "^3.2.15",
|
||||||
|
"dotenv": "^16.0.3",
|
||||||
|
"meilisearch": "^0.30.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@graphql-codegen/cli": "^2.16.1",
|
||||||
|
"@graphql-codegen/typescript": "2.8.5",
|
||||||
|
"@graphql-codegen/typescript-graphql-request": "^4.5.8",
|
||||||
|
"@graphql-codegen/typescript-operations": "^2.5.10",
|
||||||
|
"@types/node": "^18.11.17",
|
||||||
|
"graphql": "^16.6.0",
|
||||||
|
"graphql-request": "^5.1.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
module.exports = {
|
||||||
|
printWidth: 100,
|
||||||
|
tabWidth: 2,
|
||||||
|
useTabs: false,
|
||||||
|
semi: true,
|
||||||
|
singleQuote: false,
|
||||||
|
quoteProps: "as-needed",
|
||||||
|
jsxSingleQuote: false,
|
||||||
|
trailingComma: "es5",
|
||||||
|
bracketSpacing: true,
|
||||||
|
bracketSameLine: true,
|
||||||
|
arrowParens: "always",
|
||||||
|
rangeStart: 0,
|
||||||
|
rangeEnd: Infinity,
|
||||||
|
requirePragma: false,
|
||||||
|
insertPragma: false,
|
||||||
|
proseWrap: "preserve",
|
||||||
|
htmlWhitespaceSensitivity: "ignore",
|
||||||
|
endOfLine: "lf",
|
||||||
|
singleAttributePerLine: false,
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
npx esrun --tsconfig=./tsconfig.json src/app.ts
|
|
@ -0,0 +1,11 @@
|
||||||
|
import http from "http";
|
||||||
|
import { requestListener } from "./server";
|
||||||
|
import { synchronizeStrapiAndMeili } from "./syncho";
|
||||||
|
import * as dotenv from "dotenv";
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
await synchronizeStrapiAndMeili();
|
||||||
|
|
||||||
|
http.createServer(requestListener).listen(process.env.PORT, () => {
|
||||||
|
console.log(`Server started on port: ${process.env.PORT}`);
|
||||||
|
});
|
|
@ -0,0 +1,46 @@
|
||||||
|
fragment contentAttributes on Content {
|
||||||
|
slug
|
||||||
|
updatedAt
|
||||||
|
translations(pagination: { limit: -1 }) {
|
||||||
|
pre_title
|
||||||
|
title
|
||||||
|
subtitle
|
||||||
|
description
|
||||||
|
language {
|
||||||
|
data {
|
||||||
|
attributes {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
text_set {
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
categories(pagination: { limit: -1 }) {
|
||||||
|
data {
|
||||||
|
id
|
||||||
|
attributes {
|
||||||
|
name
|
||||||
|
short
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type {
|
||||||
|
data {
|
||||||
|
attributes {
|
||||||
|
slug
|
||||||
|
titles(filters: { language: { code: { eq: "en" } } }) {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
thumbnail {
|
||||||
|
data {
|
||||||
|
attributes {
|
||||||
|
...uploadImage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
fragment libraryItemAttributes on LibraryItem {
|
||||||
|
title
|
||||||
|
subtitle
|
||||||
|
slug
|
||||||
|
root_item
|
||||||
|
primary
|
||||||
|
descriptions(pagination: { limit: -1 }) {
|
||||||
|
description
|
||||||
|
language {
|
||||||
|
data {
|
||||||
|
attributes {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
thumbnail {
|
||||||
|
data {
|
||||||
|
attributes {
|
||||||
|
...uploadImage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
release_date {
|
||||||
|
...datePicker
|
||||||
|
}
|
||||||
|
price {
|
||||||
|
...pricePicker
|
||||||
|
}
|
||||||
|
categories(pagination: { limit: -1 }) {
|
||||||
|
data {
|
||||||
|
id
|
||||||
|
attributes {
|
||||||
|
name
|
||||||
|
short
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
metadata {
|
||||||
|
__typename
|
||||||
|
... on ComponentMetadataBooks {
|
||||||
|
subtype {
|
||||||
|
data {
|
||||||
|
attributes {
|
||||||
|
slug
|
||||||
|
titles(filters: { language: { code: { eq: "en" } } }) {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on ComponentMetadataGame {
|
||||||
|
platforms(pagination: { limit: -1 }) {
|
||||||
|
data {
|
||||||
|
id
|
||||||
|
attributes {
|
||||||
|
short
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on ComponentMetadataVideo {
|
||||||
|
subtype {
|
||||||
|
data {
|
||||||
|
attributes {
|
||||||
|
slug
|
||||||
|
titles(filters: { language: { code: { eq: "en" } } }) {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on ComponentMetadataAudio {
|
||||||
|
subtype {
|
||||||
|
data {
|
||||||
|
attributes {
|
||||||
|
slug
|
||||||
|
titles(filters: { language: { code: { eq: "en" } } }) {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on ComponentMetadataGroup {
|
||||||
|
subtype {
|
||||||
|
data {
|
||||||
|
attributes {
|
||||||
|
slug
|
||||||
|
titles(filters: { language: { code: { eq: "en" } } }) {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
subitems_type {
|
||||||
|
data {
|
||||||
|
attributes {
|
||||||
|
slug
|
||||||
|
titles(filters: { language: { code: { eq: "en" } } }) {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
fragment postAttributes on Post {
|
||||||
|
slug
|
||||||
|
hidden
|
||||||
|
date {
|
||||||
|
...datePicker
|
||||||
|
}
|
||||||
|
categories(pagination: { limit: -1 }) {
|
||||||
|
data {
|
||||||
|
id
|
||||||
|
attributes {
|
||||||
|
short
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
thumbnail {
|
||||||
|
data {
|
||||||
|
attributes {
|
||||||
|
...uploadImage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
translations(pagination: { limit: -1 }) {
|
||||||
|
language {
|
||||||
|
data {
|
||||||
|
attributes {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
title
|
||||||
|
excerpt
|
||||||
|
body
|
||||||
|
thumbnail {
|
||||||
|
data {
|
||||||
|
attributes {
|
||||||
|
...uploadImage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
fragment videoAttributes on Video {
|
||||||
|
uid
|
||||||
|
title
|
||||||
|
description
|
||||||
|
published_date {
|
||||||
|
year
|
||||||
|
month
|
||||||
|
day
|
||||||
|
}
|
||||||
|
views
|
||||||
|
channel {
|
||||||
|
data {
|
||||||
|
attributes {
|
||||||
|
title
|
||||||
|
uid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gone
|
||||||
|
duration
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
fragment wikiPageAttributes on WikiPage {
|
||||||
|
slug
|
||||||
|
thumbnail {
|
||||||
|
data {
|
||||||
|
attributes {
|
||||||
|
...uploadImage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
categories(pagination: { limit: -1 }) {
|
||||||
|
data {
|
||||||
|
id
|
||||||
|
attributes {
|
||||||
|
name
|
||||||
|
short
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
definitions(pagination: { limit: -1 }) {
|
||||||
|
translations {
|
||||||
|
language {
|
||||||
|
data {
|
||||||
|
attributes {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
definition
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tags(pagination: { limit: -1 }) {
|
||||||
|
data {
|
||||||
|
id
|
||||||
|
attributes {
|
||||||
|
slug
|
||||||
|
titles(filters: { language: { code: { eq: "en" } } }) {
|
||||||
|
language {
|
||||||
|
data {
|
||||||
|
attributes {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
translations(pagination: { limit: -1 }) {
|
||||||
|
title
|
||||||
|
aliases {
|
||||||
|
alias
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
body
|
||||||
|
}
|
||||||
|
summary
|
||||||
|
language {
|
||||||
|
data {
|
||||||
|
attributes {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,88 @@
|
||||||
|
import {
|
||||||
|
ContentAttributesFragment,
|
||||||
|
GetContentQuery,
|
||||||
|
GetLibraryItemQuery,
|
||||||
|
GetPostQuery,
|
||||||
|
GetVideoQuery,
|
||||||
|
GetWikiPageQuery,
|
||||||
|
LibraryItemAttributesFragment,
|
||||||
|
PostAttributesFragment,
|
||||||
|
VideoAttributesFragment,
|
||||||
|
WikiPageAttributesFragment,
|
||||||
|
} from "./generated";
|
||||||
|
|
||||||
|
export interface MeiliLibraryItem extends LibraryItemAttributesFragment {
|
||||||
|
id: string;
|
||||||
|
sortable_name: string;
|
||||||
|
sortable_price: number | undefined;
|
||||||
|
sortable_date: number | undefined;
|
||||||
|
untangible_group_item: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MeiliContent
|
||||||
|
extends Omit<ContentAttributesFragment, "translations" | "updatedAt"> {
|
||||||
|
id: string;
|
||||||
|
translations: (Omit<
|
||||||
|
NonNullable<NonNullable<ContentAttributesFragment["translations"]>[number]>,
|
||||||
|
"text_set" | "description"
|
||||||
|
> & {
|
||||||
|
displayable_description?: string | null;
|
||||||
|
})[];
|
||||||
|
sortable_updated_date: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MeiliVideo extends VideoAttributesFragment {
|
||||||
|
id: string;
|
||||||
|
sortable_published_date: number;
|
||||||
|
channel_uid?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MeiliPost extends PostAttributesFragment {
|
||||||
|
id: string;
|
||||||
|
sortable_date: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MeiliWikiPage extends Omit<WikiPageAttributesFragment, "translations"> {
|
||||||
|
id: string;
|
||||||
|
translations: (Omit<
|
||||||
|
NonNullable<NonNullable<WikiPageAttributesFragment["translations"]>[number]>,
|
||||||
|
"body"
|
||||||
|
> & {
|
||||||
|
displayable_description?: string | null;
|
||||||
|
})[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MeiliIndices {
|
||||||
|
LIBRARY_ITEM = "library-item",
|
||||||
|
CONTENT = "content",
|
||||||
|
VIDEOS = "video",
|
||||||
|
POST = "post",
|
||||||
|
WIKI_PAGE = "wiki-page",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MeiliDocumentsType =
|
||||||
|
| {
|
||||||
|
index: MeiliIndices.LIBRARY_ITEM;
|
||||||
|
documents: MeiliLibraryItem;
|
||||||
|
strapi: GetLibraryItemQuery["libraryItem"];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
index: MeiliIndices.CONTENT;
|
||||||
|
documents: MeiliContent;
|
||||||
|
strapi: GetContentQuery["content"];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
index: MeiliIndices.VIDEOS;
|
||||||
|
documents: MeiliVideo;
|
||||||
|
strapi: GetVideoQuery["video"];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
index: MeiliIndices.POST;
|
||||||
|
documents: MeiliPost;
|
||||||
|
strapi: GetPostQuery["post"];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
index: MeiliIndices.WIKI_PAGE;
|
||||||
|
documents: MeiliWikiPage;
|
||||||
|
strapi: GetWikiPageQuery["wikiPage"];
|
||||||
|
};
|
|
@ -0,0 +1,10 @@
|
||||||
|
query getContent($id: ID) {
|
||||||
|
content(id: $id) {
|
||||||
|
data {
|
||||||
|
id
|
||||||
|
attributes {
|
||||||
|
...contentAttributes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
query getContents {
|
||||||
|
contents(pagination: { limit: -1 }) {
|
||||||
|
data {
|
||||||
|
id
|
||||||
|
attributes {
|
||||||
|
...contentAttributes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
query getLibraryItem($id: ID) {
|
||||||
|
libraryItem(id: $id) {
|
||||||
|
data {
|
||||||
|
id
|
||||||
|
attributes {
|
||||||
|
...libraryItemAttributes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
query getLibraryItems {
|
||||||
|
libraryItems(pagination: { limit: -1 }) {
|
||||||
|
data {
|
||||||
|
id
|
||||||
|
attributes {
|
||||||
|
...libraryItemAttributes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
query getPost($id: ID) {
|
||||||
|
post(id: $id) {
|
||||||
|
data {
|
||||||
|
id
|
||||||
|
attributes {
|
||||||
|
...postAttributes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
query getPosts {
|
||||||
|
posts(pagination: { limit: -1 }) {
|
||||||
|
data {
|
||||||
|
id
|
||||||
|
attributes {
|
||||||
|
...postAttributes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
query getVideo($id: ID) {
|
||||||
|
video(id: $id) {
|
||||||
|
data {
|
||||||
|
id
|
||||||
|
attributes {
|
||||||
|
...videoAttributes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
query getVideos {
|
||||||
|
videos(pagination: { limit: -1 }) {
|
||||||
|
data {
|
||||||
|
id
|
||||||
|
attributes {
|
||||||
|
...videoAttributes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
query getWikiPage($id: ID) {
|
||||||
|
wikiPage(id: $id) {
|
||||||
|
data {
|
||||||
|
id
|
||||||
|
attributes {
|
||||||
|
...wikiPageAttributes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
query getWikiPages {
|
||||||
|
wikiPages(pagination: { limit: -1 }) {
|
||||||
|
data {
|
||||||
|
id
|
||||||
|
attributes {
|
||||||
|
...wikiPageAttributes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { GraphQLClient } from "graphql-request";
|
||||||
|
import { getSdk } from "./generated";
|
||||||
|
|
||||||
|
export const getReadySdk = (): ReturnType<typeof getSdk> => {
|
||||||
|
const client = new GraphQLClient(process.env.URL_GRAPHQL ?? "", {
|
||||||
|
headers: { Authorization: `Bearer ${process.env.STRAPI_API_TOKEN}` },
|
||||||
|
});
|
||||||
|
return getSdk(client);
|
||||||
|
};
|
|
@ -0,0 +1,79 @@
|
||||||
|
type JoinDot<K extends string, P extends string> = `${K}${"" extends K ? "" : "."}${P}`;
|
||||||
|
|
||||||
|
type PathDot<T, Acc extends string = ""> = T extends object
|
||||||
|
? {
|
||||||
|
[K in keyof T]: K extends string ? JoinDot<Acc, K> | PathDot<T[K], JoinDot<Acc, K>> : never;
|
||||||
|
}[keyof T]
|
||||||
|
: Acc;
|
||||||
|
|
||||||
|
type PathHead<T extends unknown[]> = T extends [infer head]
|
||||||
|
? head
|
||||||
|
: // eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
T extends [infer head, ...infer rest]
|
||||||
|
? head
|
||||||
|
: "";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
type PathRest<T extends unknown[]> = T extends [infer head, ...infer rest]
|
||||||
|
? rest extends []
|
||||||
|
? never
|
||||||
|
: rest
|
||||||
|
: never;
|
||||||
|
|
||||||
|
type PathLast<T extends unknown[]> = T["length"] extends 1 ? true : false;
|
||||||
|
|
||||||
|
type Recursive<T, Path extends unknown[]> = PathHead<Path> extends keyof T
|
||||||
|
? Omit<T, PathHead<Path>> & {
|
||||||
|
[P in PathHead<Path>]-?: PathLast<Path> extends true
|
||||||
|
? NonNullable<T[P]>
|
||||||
|
: Recursive<NonNullable<T[P]>, PathRest<Path>>;
|
||||||
|
}
|
||||||
|
: T;
|
||||||
|
|
||||||
|
type Split<Str, Acc extends string[] = []> = Str extends `${infer Head}.${infer Rest}`
|
||||||
|
? Split<Rest, [...Acc, Head]>
|
||||||
|
: Str extends `${infer Last}`
|
||||||
|
? [...Acc, Last]
|
||||||
|
: never;
|
||||||
|
|
||||||
|
export type SelectiveNonNullable<T, P extends PathDot<T>> = Recursive<NonNullable<T>, Split<P>>;
|
||||||
|
|
||||||
|
export const isDefined = <T>(t: T): t is NonNullable<T> => t !== null && t !== undefined;
|
||||||
|
|
||||||
|
export const isUndefined = <T>(t: T | null | undefined): t is null | undefined => !isDefined(t);
|
||||||
|
|
||||||
|
export const isDefinedAndNotEmpty = (string: string | null | undefined): string is string =>
|
||||||
|
isDefined(string) && string.length > 0;
|
||||||
|
|
||||||
|
export const filterDefined = <T>(t: T[] | null | undefined): NonNullable<T>[] =>
|
||||||
|
isUndefined(t) ? [] : (t.filter((item) => isDefined(item)) as NonNullable<T>[]);
|
||||||
|
|
||||||
|
export const filterHasAttributes = <T, P extends PathDot<T>>(
|
||||||
|
t: T[] | null | undefined,
|
||||||
|
paths: readonly P[]
|
||||||
|
): SelectiveNonNullable<T, typeof paths[number]>[] =>
|
||||||
|
isDefined(t)
|
||||||
|
? (t.filter((item) => hasAttributes(item, paths)) as unknown as SelectiveNonNullable<
|
||||||
|
T,
|
||||||
|
typeof paths[number]
|
||||||
|
>[])
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const hasAttributes = <T>(item: T, paths: readonly PathDot<T>[]): boolean =>
|
||||||
|
isDefined(item) && paths.every((path) => hasAttribute(item, path));
|
||||||
|
|
||||||
|
const hasAttribute = <T>(item: T, path: string): boolean => {
|
||||||
|
if (isDefined(item)) {
|
||||||
|
const [head, ...rest] = path.split(".");
|
||||||
|
if (isDefined(head) && Object.keys(item).includes(head)) {
|
||||||
|
const attribute = head as keyof T;
|
||||||
|
if (isDefined(item[attribute])) {
|
||||||
|
if (rest.length > 0) {
|
||||||
|
return hasAttribute(item[attribute], rest.join("."));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { isDefined, isUndefined } from "./asserts";
|
||||||
|
|
||||||
|
export type DatePickerFragment = {
|
||||||
|
year?: number | null;
|
||||||
|
month?: number | null;
|
||||||
|
day?: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const compareDate = (
|
||||||
|
a: DatePickerFragment | null | undefined,
|
||||||
|
b: DatePickerFragment | null | undefined
|
||||||
|
): number => {
|
||||||
|
if (isUndefined(a) || isUndefined(b)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return dateInDays(a) - dateInDays(b);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dateInDays = (date: DatePickerFragment | null | undefined): number =>
|
||||||
|
isDefined(date) ? (date.year ?? Infinity) * 365 + (date.month ?? 12) * 31 + (date.day ?? 31) : 0;
|
||||||
|
|
||||||
|
export const datePickerToDate = (date: DatePickerFragment): Date =>
|
||||||
|
new Date(date.year ?? 0, date.month ? date.month - 1 : 0, date.day ?? 1);
|
||||||
|
|
||||||
|
export const getUnixTime = (date: Date): number => Math.floor(date.getTime() / 1000);
|
|
@ -0,0 +1,221 @@
|
||||||
|
import { isDefinedAndNotEmpty } from "./asserts";
|
||||||
|
|
||||||
|
export const prettySlug = (slug?: string, parentSlug?: string): string => {
|
||||||
|
let newSlug = slug;
|
||||||
|
if (newSlug) {
|
||||||
|
if (isDefinedAndNotEmpty(parentSlug) && newSlug.startsWith(parentSlug))
|
||||||
|
newSlug = newSlug.substring(parentSlug.length + 1);
|
||||||
|
newSlug = newSlug.replaceAll("-", " ");
|
||||||
|
return capitalizeString(newSlug);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const prettyInlineTitle = (
|
||||||
|
pretitle: string | null | undefined,
|
||||||
|
title: string | null | undefined,
|
||||||
|
subtitle: string | null | undefined
|
||||||
|
): string => {
|
||||||
|
let result = "";
|
||||||
|
if (pretitle) result += `${pretitle}: `;
|
||||||
|
result += title;
|
||||||
|
if (subtitle) result += ` - ${subtitle}`;
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* eslint-disable id-denylist */
|
||||||
|
export const prettyItemSubType = (
|
||||||
|
metadata:
|
||||||
|
| {
|
||||||
|
__typename: "ComponentMetadataAudio";
|
||||||
|
subtype?: {
|
||||||
|
data?: {
|
||||||
|
attributes?: {
|
||||||
|
slug: string;
|
||||||
|
titles?:
|
||||||
|
| ({
|
||||||
|
title: string;
|
||||||
|
} | null)[]
|
||||||
|
| null;
|
||||||
|
} | null;
|
||||||
|
} | null;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
__typename: "ComponentMetadataBooks";
|
||||||
|
subtype?: {
|
||||||
|
data?: {
|
||||||
|
attributes?: {
|
||||||
|
slug: string;
|
||||||
|
titles?:
|
||||||
|
| ({
|
||||||
|
title: string;
|
||||||
|
} | null)[]
|
||||||
|
| null;
|
||||||
|
} | null;
|
||||||
|
} | null;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
__typename: "ComponentMetadataGame";
|
||||||
|
platforms?: {
|
||||||
|
data: {
|
||||||
|
id?: string | null;
|
||||||
|
attributes?: {
|
||||||
|
short: string;
|
||||||
|
} | null;
|
||||||
|
}[];
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
__typename: "ComponentMetadataGroup";
|
||||||
|
subtype?: {
|
||||||
|
data?: {
|
||||||
|
attributes?: {
|
||||||
|
slug: string;
|
||||||
|
titles?:
|
||||||
|
| ({
|
||||||
|
title: string;
|
||||||
|
} | null)[]
|
||||||
|
| null;
|
||||||
|
} | null;
|
||||||
|
} | null;
|
||||||
|
} | null;
|
||||||
|
subitems_type?: {
|
||||||
|
data?: {
|
||||||
|
attributes?: {
|
||||||
|
slug: string;
|
||||||
|
titles?:
|
||||||
|
| ({
|
||||||
|
title: string;
|
||||||
|
} | null)[]
|
||||||
|
| null;
|
||||||
|
} | null;
|
||||||
|
} | null;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
__typename: "ComponentMetadataVideo";
|
||||||
|
subtype?: {
|
||||||
|
data?: {
|
||||||
|
attributes?: {
|
||||||
|
slug: string;
|
||||||
|
titles?:
|
||||||
|
| ({
|
||||||
|
title: string;
|
||||||
|
} | null)[]
|
||||||
|
| null;
|
||||||
|
} | null;
|
||||||
|
} | null;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
| { __typename: "ComponentMetadataOther" }
|
||||||
|
| { __typename: "Error" }
|
||||||
|
| null
|
||||||
|
): string => {
|
||||||
|
if (metadata) {
|
||||||
|
switch (metadata.__typename) {
|
||||||
|
case "ComponentMetadataAudio":
|
||||||
|
case "ComponentMetadataBooks":
|
||||||
|
case "ComponentMetadataVideo":
|
||||||
|
return metadata.subtype?.data?.attributes?.titles &&
|
||||||
|
metadata.subtype.data.attributes.titles.length > 0 &&
|
||||||
|
metadata.subtype.data.attributes.titles[0]
|
||||||
|
? metadata.subtype.data.attributes.titles[0].title
|
||||||
|
: prettySlug(metadata.subtype?.data?.attributes?.slug);
|
||||||
|
case "ComponentMetadataGame":
|
||||||
|
return metadata.platforms?.data &&
|
||||||
|
metadata.platforms.data.length > 0 &&
|
||||||
|
metadata.platforms.data[0]?.attributes
|
||||||
|
? metadata.platforms.data[0].attributes.short
|
||||||
|
: "";
|
||||||
|
case "ComponentMetadataGroup": {
|
||||||
|
const firstPart =
|
||||||
|
metadata.subtype?.data?.attributes?.titles &&
|
||||||
|
metadata.subtype.data.attributes.titles.length > 0 &&
|
||||||
|
metadata.subtype.data.attributes.titles[0]
|
||||||
|
? metadata.subtype.data.attributes.titles[0].title
|
||||||
|
: prettySlug(metadata.subtype?.data?.attributes?.slug);
|
||||||
|
|
||||||
|
const secondPart =
|
||||||
|
metadata.subitems_type?.data?.attributes?.titles &&
|
||||||
|
metadata.subitems_type.data.attributes.titles.length > 0 &&
|
||||||
|
metadata.subitems_type.data.attributes.titles[0]
|
||||||
|
? metadata.subitems_type.data.attributes.titles[0].title
|
||||||
|
: prettySlug(metadata.subitems_type?.data?.attributes?.slug);
|
||||||
|
return `${secondPart} ${firstPart}`;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
/* eslint-enable id-denylist */
|
||||||
|
|
||||||
|
export const prettyShortenNumber = (number: number): string => {
|
||||||
|
if (number > 1_000_000) {
|
||||||
|
return `${(number / 1_000_000).toLocaleString(undefined, {
|
||||||
|
maximumSignificantDigits: 3,
|
||||||
|
})}M`;
|
||||||
|
} else if (number > 1_000) {
|
||||||
|
return `${(number / 1_000).toLocaleString(undefined, {
|
||||||
|
maximumSignificantDigits: 2,
|
||||||
|
})}K`;
|
||||||
|
}
|
||||||
|
return number.toLocaleString();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const prettyDuration = (seconds: number): string => {
|
||||||
|
let hours = 0;
|
||||||
|
let minutes = 0;
|
||||||
|
let remainingSeconds = seconds;
|
||||||
|
while (remainingSeconds > 60) {
|
||||||
|
minutes++;
|
||||||
|
remainingSeconds -= 60;
|
||||||
|
}
|
||||||
|
while (minutes > 60) {
|
||||||
|
hours++;
|
||||||
|
minutes -= 60;
|
||||||
|
}
|
||||||
|
let result = "";
|
||||||
|
if (hours) result += `${hours.toString().padStart(2, "0")}:`;
|
||||||
|
result += `${minutes.toString().padStart(2, "0")}:`;
|
||||||
|
result += remainingSeconds.toString().padStart(2, "0");
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const prettyURL = (url: string): string => {
|
||||||
|
const domain = new URL(url);
|
||||||
|
return domain.hostname.replace("www.", "");
|
||||||
|
};
|
||||||
|
|
||||||
|
const capitalizeString = (string: string): string => {
|
||||||
|
const capitalizeWord = (word: string): string => word.charAt(0).toUpperCase() + word.substring(1);
|
||||||
|
|
||||||
|
let words = string.split(" ");
|
||||||
|
words = words.map((word) => capitalizeWord(word));
|
||||||
|
return words.join(" ");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const slugify = (string: string | undefined): string => {
|
||||||
|
if (!string) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return string
|
||||||
|
.replace(/[ÀÁÂÃÄÅàáâãäåæÆ]/gu, "a")
|
||||||
|
.replace(/[çÇ]/gu, "c")
|
||||||
|
.replace(/[ðÐ]/gu, "d")
|
||||||
|
.replace(/[ÈÉÊËéèêë]/gu, "e")
|
||||||
|
.replace(/[ÏïÎîÍíÌì]/gu, "i")
|
||||||
|
.replace(/[Ññ]/gu, "n")
|
||||||
|
.replace(/[øØœŒÕõÔôÓóÒò]/gu, "o")
|
||||||
|
.replace(/[ÜüÛûÚúÙù]/gu, "u")
|
||||||
|
.replace(/[ŸÿÝý]/gu, "y")
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9- ]/gu, "")
|
||||||
|
.trim()
|
||||||
|
.replace(/ /gu, "-");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sJoin = (...args: (string | null | undefined)[]): string => args.join("");
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { isDefined } from "./asserts";
|
||||||
|
|
||||||
|
export const isUntangibleGroupItem = (
|
||||||
|
metadata:
|
||||||
|
| {
|
||||||
|
__typename: string;
|
||||||
|
// eslint-disable-next-line id-denylist
|
||||||
|
subtype?: { data?: { attributes?: { slug: string } | null } | null } | null;
|
||||||
|
}
|
||||||
|
| null
|
||||||
|
| undefined
|
||||||
|
): boolean =>
|
||||||
|
isDefined(metadata) &&
|
||||||
|
metadata.__typename === "ComponentMetadataGroup" &&
|
||||||
|
(metadata.subtype?.data?.attributes?.slug === "variant-set" ||
|
||||||
|
metadata.subtype?.data?.attributes?.slug === "relation-set");
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { Currencies } from "./localData";
|
||||||
|
import { PricePickerFragment } from "graphql/generated";
|
||||||
|
|
||||||
|
export const convertPrice = (
|
||||||
|
pricePicker: PricePickerFragment,
|
||||||
|
targetCurrency: Currencies[number]
|
||||||
|
): number => {
|
||||||
|
if (pricePicker.amount && pricePicker.currency?.data?.attributes && targetCurrency.attributes)
|
||||||
|
return (
|
||||||
|
(pricePicker.amount * pricePicker.currency.data.attributes.rate_to_usd) /
|
||||||
|
targetCurrency.attributes.rate_to_usd
|
||||||
|
);
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const convertMmToInch = (mm: number | null | undefined): string =>
|
||||||
|
mm ? (mm * 0.03937008).toPrecision(3) : "";
|
||||||
|
|
||||||
|
export const randomInt = (min: number, max: number): number =>
|
||||||
|
Math.floor(Math.random() * (max - min)) + min;
|
||||||
|
|
||||||
|
export const isInteger = (value: string): boolean => /^[+-]?[0-9]+$/u.test(value);
|
||||||
|
|
||||||
|
export const clamp = (value: number, min: number, max: number): number =>
|
||||||
|
Math.min(Math.max(value, min), max);
|
|
@ -0,0 +1,143 @@
|
||||||
|
import {
|
||||||
|
MeiliContent,
|
||||||
|
MeiliDocumentsType,
|
||||||
|
MeiliIndices,
|
||||||
|
MeiliWikiPage,
|
||||||
|
} from "core/graphql/meiliTypes";
|
||||||
|
import {
|
||||||
|
filterDefined,
|
||||||
|
filterHasAttributes,
|
||||||
|
isDefined,
|
||||||
|
isDefinedAndNotEmpty,
|
||||||
|
} from "core/helpers/asserts";
|
||||||
|
import { datePickerToDate, getUnixTime } from "core/helpers/date";
|
||||||
|
import { prettyInlineTitle } from "core/helpers/formatters";
|
||||||
|
import { isUntangibleGroupItem } from "core/helpers/libraryItem";
|
||||||
|
import { MeiliSearch } from "meilisearch";
|
||||||
|
|
||||||
|
export const getMeili = () =>
|
||||||
|
new MeiliSearch({
|
||||||
|
host: process.env.MEILISEARCH_URL ?? "",
|
||||||
|
apiKey: process.env.MEILISEARCH_MASTER_KEY,
|
||||||
|
});
|
||||||
|
|
||||||
|
type TransformFunction<I extends MeiliDocumentsType["index"]> = (
|
||||||
|
data: NonNullable<Extract<MeiliDocumentsType, { index: I }>["strapi"]>["data"]
|
||||||
|
) => Extract<MeiliDocumentsType, { index: I }>["documents"];
|
||||||
|
|
||||||
|
const transformVideo: TransformFunction<MeiliIndices.VIDEOS> = (data) => {
|
||||||
|
if (!data) throw new Error(`Data is empty ${MeiliIndices.VIDEOS}`);
|
||||||
|
if (!data.attributes || !data.id)
|
||||||
|
throw new Error(`Incorrect data stucture on ${MeiliIndices.VIDEOS}`);
|
||||||
|
const { id, attributes } = data;
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
...attributes,
|
||||||
|
sortable_published_date: getUnixTime(datePickerToDate(attributes.published_date)),
|
||||||
|
channel_uid: attributes.channel?.data?.attributes?.uid,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const transformContent: TransformFunction<MeiliIndices.CONTENT> = (data) => {
|
||||||
|
if (!data) throw new Error(`Data is empty ${MeiliIndices.CONTENT}`);
|
||||||
|
if (!data.attributes || !data.id)
|
||||||
|
throw new Error(`Incorrect data stucture on ${MeiliIndices.CONTENT}`);
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
attributes: { translations, updatedAt, ...otherAttributes },
|
||||||
|
} = data;
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
...otherAttributes,
|
||||||
|
translations: filterDefined(translations).map(
|
||||||
|
({ text_set, description, ...otherTranslatedFields }) => {
|
||||||
|
let displayable_description = "";
|
||||||
|
if (isDefinedAndNotEmpty(description)) displayable_description += description;
|
||||||
|
if (isDefinedAndNotEmpty(text_set?.text))
|
||||||
|
displayable_description += `\n\n${text_set?.text}`;
|
||||||
|
return {
|
||||||
|
...otherTranslatedFields,
|
||||||
|
displayable_description,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
),
|
||||||
|
sortable_updated_date: updatedAt,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const transformLibraryItem: TransformFunction<MeiliIndices.LIBRARY_ITEM> = (data) => {
|
||||||
|
if (!data) throw new Error(`Data is empty ${MeiliIndices.LIBRARY_ITEM}`);
|
||||||
|
if (!data.attributes || !data.id)
|
||||||
|
throw new Error(`Incorrect data stucture on ${MeiliIndices.LIBRARY_ITEM}`);
|
||||||
|
const { id, attributes } = data;
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
sortable_date: isDefined(attributes.release_date)
|
||||||
|
? getUnixTime(datePickerToDate(attributes.release_date))
|
||||||
|
: undefined,
|
||||||
|
sortable_name: prettyInlineTitle(undefined, attributes.title, attributes.subtitle),
|
||||||
|
sortable_price:
|
||||||
|
attributes.price?.currency?.data?.attributes && isDefined(attributes.price.amount)
|
||||||
|
? attributes.price.amount * attributes.price.currency.data.attributes.rate_to_usd
|
||||||
|
: undefined,
|
||||||
|
untangible_group_item: isUntangibleGroupItem(attributes.metadata?.[0]),
|
||||||
|
...attributes,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const transformPost: TransformFunction<MeiliIndices.POST> = (data) => {
|
||||||
|
if (!data) throw new Error(`Data is empty ${MeiliIndices.POST}`);
|
||||||
|
if (!data.attributes || !data.id)
|
||||||
|
throw new Error(`Incorrect data stucture on ${MeiliIndices.POST}`);
|
||||||
|
const { id, attributes } = data;
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
...attributes,
|
||||||
|
sortable_date: getUnixTime(datePickerToDate(attributes.date)),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const transformWikiPage: TransformFunction<MeiliIndices.WIKI_PAGE> = (data) => {
|
||||||
|
if (!data) throw new Error(`Data is empty ${MeiliIndices.POST}`);
|
||||||
|
if (!data.attributes || !data.id)
|
||||||
|
throw new Error(`Incorrect data stucture on ${MeiliIndices.POST}`);
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
attributes: { translations, definitions, ...otherAttributes },
|
||||||
|
} = data;
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
definitions,
|
||||||
|
translations: filterDefined(translations).map(({ summary, body, ...otherTransAttributes }) => {
|
||||||
|
let displayable_description = "";
|
||||||
|
if (isDefinedAndNotEmpty(summary)) displayable_description += summary;
|
||||||
|
if (isDefined(body) && isDefinedAndNotEmpty(body.body))
|
||||||
|
displayable_description += `\n\n${body.body}`;
|
||||||
|
definitions?.forEach((def) =>
|
||||||
|
def?.translations?.forEach((defTranslation) => {
|
||||||
|
if (
|
||||||
|
defTranslation?.language?.data?.attributes?.code ===
|
||||||
|
otherTransAttributes.language?.data?.attributes?.code &&
|
||||||
|
isDefined(defTranslation?.definition)
|
||||||
|
) {
|
||||||
|
displayable_description += `\n\n${defTranslation?.definition}`;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
summary,
|
||||||
|
...otherTransAttributes,
|
||||||
|
displayable_description,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
...otherAttributes,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const strapiToMeiliTransformFunctions = {
|
||||||
|
video: transformVideo,
|
||||||
|
"library-item": transformLibraryItem,
|
||||||
|
content: transformContent,
|
||||||
|
post: transformPost,
|
||||||
|
"wiki-page": transformWikiPage,
|
||||||
|
};
|
|
@ -0,0 +1,40 @@
|
||||||
|
import http from "http";
|
||||||
|
import { webhookHandler } from "webhook";
|
||||||
|
|
||||||
|
export const requestListener: http.RequestListener = async (req, res) => {
|
||||||
|
if (req.method !== "POST") {
|
||||||
|
res
|
||||||
|
.writeHead(405, { "Content-Type": "application/json" })
|
||||||
|
.end(JSON.stringify({ message: "Method Not Allowed. Use POST." }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.url !== "/strapi/") {
|
||||||
|
res
|
||||||
|
.writeHead(404, { "Content-Type": "application/json" })
|
||||||
|
.end(JSON.stringify({ message: "Route not found.", route: req.url }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.headers.authorization !== `Bearer ${process.env.WEBHOOK_TOKEN}`) {
|
||||||
|
res
|
||||||
|
.writeHead(403, { "Content-Type": "application/json" })
|
||||||
|
.end(JSON.stringify({ message: "Invalid auth token." }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve and parse body
|
||||||
|
const buffers: Uint8Array[] = [];
|
||||||
|
for await (const chunk of req) {
|
||||||
|
buffers.push(chunk);
|
||||||
|
}
|
||||||
|
const data = JSON.parse(Buffer.concat(buffers).toString());
|
||||||
|
|
||||||
|
await webhookHandler(data, res);
|
||||||
|
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json" }).end(
|
||||||
|
JSON.stringify({
|
||||||
|
message: "Done.",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,5 @@
|
||||||
|
fragment datePicker on ComponentBasicsDatepicker {
|
||||||
|
year
|
||||||
|
month
|
||||||
|
day
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
fragment pricePicker on ComponentBasicsPrice {
|
||||||
|
amount
|
||||||
|
currency {
|
||||||
|
data {
|
||||||
|
attributes {
|
||||||
|
symbol
|
||||||
|
code
|
||||||
|
rate_to_usd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
fragment uploadImage on UploadFile {
|
||||||
|
name
|
||||||
|
alternativeText
|
||||||
|
caption
|
||||||
|
width
|
||||||
|
height
|
||||||
|
url
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
import { MeiliDocumentsType, MeiliIndices } from "core/graphql/meiliTypes";
|
||||||
|
import { getReadySdk } from "core/graphql/sdk";
|
||||||
|
import { getMeili, strapiToMeiliTransformFunctions } from "helpers/meili";
|
||||||
|
|
||||||
|
export const synchronizeStrapiAndMeili = async () => {
|
||||||
|
const sdk = getReadySdk();
|
||||||
|
|
||||||
|
// [ LIBRARY ITEMS ]
|
||||||
|
{
|
||||||
|
const libraryItems = await sdk.getLibraryItems();
|
||||||
|
processIndex(
|
||||||
|
MeiliIndices.LIBRARY_ITEM,
|
||||||
|
libraryItems.libraryItems?.data.map((item) =>
|
||||||
|
strapiToMeiliTransformFunctions["library-item"](item)
|
||||||
|
),
|
||||||
|
["title", "subtitle", "descriptions"],
|
||||||
|
["sortable_name", "sortable_date", "sortable_price"],
|
||||||
|
["primary", "root_item", "id", "untangible_group_item"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// [ CONTENT ]
|
||||||
|
{
|
||||||
|
const content = await sdk.getContents();
|
||||||
|
processIndex(
|
||||||
|
MeiliIndices.CONTENT,
|
||||||
|
content.contents?.data.map((item) => strapiToMeiliTransformFunctions["content"](item)),
|
||||||
|
["translations"],
|
||||||
|
["slug", "sortable_updated_date"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// [ VIDEOS ]
|
||||||
|
{
|
||||||
|
const videos = await sdk.getVideos();
|
||||||
|
processIndex(
|
||||||
|
MeiliIndices.VIDEOS,
|
||||||
|
videos.videos?.data.map((item) => strapiToMeiliTransformFunctions["video"](item)),
|
||||||
|
["title", "channel", "description", "uid"],
|
||||||
|
["sortable_published_date", "duration", "views"],
|
||||||
|
["gone", "channel_uid"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// [ POSTS ]
|
||||||
|
{
|
||||||
|
const posts = await sdk.getPosts();
|
||||||
|
processIndex(
|
||||||
|
MeiliIndices.POST,
|
||||||
|
posts.posts?.data.map((item) => strapiToMeiliTransformFunctions["post"](item)),
|
||||||
|
["translations"],
|
||||||
|
["sortable_date"],
|
||||||
|
["hidden"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// [ WIKI PAGES ]
|
||||||
|
{
|
||||||
|
const wikiPages = await sdk.getWikiPages();
|
||||||
|
processIndex(
|
||||||
|
MeiliIndices.WIKI_PAGE,
|
||||||
|
wikiPages.wikiPages?.data.map((item) => strapiToMeiliTransformFunctions["wiki-page"](item)),
|
||||||
|
["translations", "definitions"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const processIndex = async <I extends MeiliDocumentsType["index"]>(
|
||||||
|
indexName: I,
|
||||||
|
data?: Extract<MeiliDocumentsType, { index: I }>["documents"][],
|
||||||
|
searchableAttributes?: (keyof NonNullable<typeof data>[number])[],
|
||||||
|
sortableAttributes?: (keyof NonNullable<typeof data>[number])[],
|
||||||
|
filterableAttributes?: (keyof NonNullable<typeof data>[number])[]
|
||||||
|
) => {
|
||||||
|
const meili = getMeili();
|
||||||
|
await meili.deleteIndexIfExists(indexName);
|
||||||
|
|
||||||
|
if (data && data.length > 0) {
|
||||||
|
await meili.createIndex(indexName);
|
||||||
|
const index = meili.index(indexName);
|
||||||
|
index.updateSettings({
|
||||||
|
searchableAttributes: searchableAttributes as string[],
|
||||||
|
sortableAttributes: sortableAttributes as string[],
|
||||||
|
filterableAttributes: filterableAttributes as string[],
|
||||||
|
pagination: { maxTotalHits: 10000 },
|
||||||
|
});
|
||||||
|
index.addDocuments(data);
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,97 @@
|
||||||
|
import { MeiliDocumentsType, MeiliIndices } from "core/graphql/meiliTypes";
|
||||||
|
import { getReadySdk } from "core/graphql/sdk";
|
||||||
|
import { getMeili, strapiToMeiliTransformFunctions } from "helpers/meili";
|
||||||
|
import http from "http";
|
||||||
|
|
||||||
|
const allowedEvents = ["entry.create", "entry.update", "entry.delete"] as const;
|
||||||
|
|
||||||
|
type CRUDEvents = typeof allowedEvents[number];
|
||||||
|
|
||||||
|
type StrapiEvent = {
|
||||||
|
event: CRUDEvents;
|
||||||
|
model: MeiliIndices;
|
||||||
|
entry: {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const webhookHandler = async (data: StrapiEvent, res: http.ServerResponse) => {
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
|
if (!allowedEvents.includes(data.event)) {
|
||||||
|
res.writeHead(406, { "Content-Type": "application/json" }).end(
|
||||||
|
JSON.stringify({
|
||||||
|
message: `The event given ${data.event} in not allowed.`,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sdk = getReadySdk();
|
||||||
|
|
||||||
|
switch (data.model) {
|
||||||
|
case MeiliIndices.LIBRARY_ITEM: {
|
||||||
|
processIndex(
|
||||||
|
data.model,
|
||||||
|
strapiToMeiliTransformFunctions["library-item"](
|
||||||
|
(await sdk.getLibraryItem({ id: data.entry.id })).libraryItem?.data
|
||||||
|
)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case MeiliIndices.CONTENT: {
|
||||||
|
processIndex(
|
||||||
|
data.model,
|
||||||
|
strapiToMeiliTransformFunctions["content"](
|
||||||
|
(await sdk.getContent({ id: data.entry.id })).content?.data
|
||||||
|
)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case MeiliIndices.VIDEOS: {
|
||||||
|
processIndex(
|
||||||
|
data.model,
|
||||||
|
strapiToMeiliTransformFunctions["video"](
|
||||||
|
(await sdk.getVideo({ id: data.entry.id })).video?.data
|
||||||
|
)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case MeiliIndices.POST: {
|
||||||
|
processIndex(
|
||||||
|
data.model,
|
||||||
|
strapiToMeiliTransformFunctions["post"](
|
||||||
|
(await sdk.getPost({ id: data.entry.id })).post?.data
|
||||||
|
)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case MeiliIndices.WIKI_PAGE: {
|
||||||
|
processIndex(
|
||||||
|
data.model,
|
||||||
|
strapiToMeiliTransformFunctions["wiki-page"](
|
||||||
|
(await sdk.getWikiPage({ id: data.entry.id })).wikiPage?.data
|
||||||
|
)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
console.log("Unrecognized data model", data.model);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const processIndex = async <I extends MeiliDocumentsType["index"]>(
|
||||||
|
indexName: I,
|
||||||
|
data: Extract<MeiliDocumentsType, { index: I }>["documents"] | undefined | null
|
||||||
|
) => {
|
||||||
|
const meili = getMeili();
|
||||||
|
const index = meili.index(indexName);
|
||||||
|
await index.addDocuments([data]);
|
||||||
|
};
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
// Type Checking
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"strict": true,
|
||||||
|
|
||||||
|
"target": "ESNext",
|
||||||
|
"importHelpers": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"baseUrl": "src"
|
||||||
|
},
|
||||||
|
"include": ["**/*.ts", "**/*.tsx"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue