🔧 Added graphQL bits
This commit is contained in:
47
graphql-server.ts
Normal file
47
graphql-server.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import express from "express";
|
||||||
|
import { ApolloServer } from "@apollo/server";
|
||||||
|
import { expressMiddleware } from "@as-integrations/express4";
|
||||||
|
import { typeDefs } from "./models/graphql/typedef";
|
||||||
|
import { resolvers } from "./models/graphql/resolvers";
|
||||||
|
import { ServiceBroker } from "moleculer";
|
||||||
|
import cors from "cors";
|
||||||
|
|
||||||
|
// Boot Moleculer broker in parallel
|
||||||
|
const broker = new ServiceBroker({ transporter: null }); // or your actual transporter config
|
||||||
|
|
||||||
|
async function startGraphQLServer() {
|
||||||
|
const app = express();
|
||||||
|
const apollo = new ApolloServer({
|
||||||
|
typeDefs,
|
||||||
|
resolvers,
|
||||||
|
});
|
||||||
|
|
||||||
|
await apollo.start();
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
"/graphql",
|
||||||
|
cors(),
|
||||||
|
express.json(),
|
||||||
|
expressMiddleware(apollo, {
|
||||||
|
context: async ({ req }) => ({
|
||||||
|
authToken: req.headers.authorization || null,
|
||||||
|
broker,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const PORT = 4000;
|
||||||
|
app.listen(PORT, () =>
|
||||||
|
console.log(`🚀 GraphQL server running at http://localhost:${PORT}/graphql`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
await broker.start(); // make sure Moleculer is up
|
||||||
|
await startGraphQLServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
bootstrap().catch((err) => {
|
||||||
|
console.error("❌ Failed to start GraphQL server:", err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -1,24 +1,59 @@
|
|||||||
import { gql } from "graphql-tag";
|
import { gql } from "graphql-tag";
|
||||||
|
|
||||||
export const typeDefs = gql`
|
export const typeDefs = gql`
|
||||||
type Query {
|
type Query {
|
||||||
comic(id: ID!): Comic
|
comic(id: ID!): Comic
|
||||||
comics(limit: Int = 10): [Comic]
|
comics(limit: Int = 10): [Comic]
|
||||||
}
|
wantedComics(limit: Int = 25, offset: Int = 0): ComicPage!
|
||||||
|
}
|
||||||
|
|
||||||
type Comic {
|
type Comic {
|
||||||
id: ID!
|
id: ID!
|
||||||
title: String
|
title: String!
|
||||||
volume: Int
|
volume: Int
|
||||||
issueNumber: String
|
issueNumber: String!
|
||||||
publicationDate: String
|
publicationDate: String
|
||||||
coverUrl: String
|
variant: String
|
||||||
creators: [Creator]
|
format: String
|
||||||
source: String
|
creators: [Creator!]!
|
||||||
}
|
arcs: [String!]
|
||||||
|
coverUrl: String
|
||||||
|
filePath: String
|
||||||
|
pageCount: Int
|
||||||
|
tags: [String!]
|
||||||
|
source: String
|
||||||
|
|
||||||
type Creator {
|
confidence: ConfidenceMap
|
||||||
name: String
|
provenance: ProvenanceMap
|
||||||
role: String
|
}
|
||||||
}
|
|
||||||
|
type Creator {
|
||||||
|
name: String!
|
||||||
|
role: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfidenceMap {
|
||||||
|
title: Float
|
||||||
|
volume: Float
|
||||||
|
issueNumber: Float
|
||||||
|
publicationDate: Float
|
||||||
|
creators: Float
|
||||||
|
variant: Float
|
||||||
|
format: Float
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProvenanceMap {
|
||||||
|
title: String
|
||||||
|
volume: String
|
||||||
|
issueNumber: String
|
||||||
|
publicationDate: String
|
||||||
|
creators: String
|
||||||
|
variant: String
|
||||||
|
format: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type ComicPage {
|
||||||
|
total: Int!
|
||||||
|
results: [Comic!]!
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
197
package-lock.json
generated
197
package-lock.json
generated
@@ -9,15 +9,17 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/server": "^4.12.2",
|
"@apollo/server": "^4.12.2",
|
||||||
|
"@as-integrations/express4": "^1.1.1",
|
||||||
"@bluelovers/fast-glob": "https://github.com/rishighan/fast-glob-v2-api.git",
|
"@bluelovers/fast-glob": "https://github.com/rishighan/fast-glob-v2-api.git",
|
||||||
"@elastic/elasticsearch": "^8.13.1",
|
"@elastic/elasticsearch": "^8.13.1",
|
||||||
"@jorgeferrero/stream-to-buffer": "^2.0.6",
|
"@jorgeferrero/stream-to-buffer": "^2.0.6",
|
||||||
|
"@ltv/moleculer-apollo-server-mixin": "^0.1.30",
|
||||||
"@npcz/magic": "^1.3.14",
|
"@npcz/magic": "^1.3.14",
|
||||||
"@root/walk": "^1.1.0",
|
"@root/walk": "^1.1.0",
|
||||||
"@socket.io/redis-adapter": "^8.1.0",
|
"@socket.io/redis-adapter": "^8.1.0",
|
||||||
"@types/jest": "^27.4.1",
|
"@types/jest": "^27.4.1",
|
||||||
"@types/mkdirp": "^1.0.0",
|
"@types/mkdirp": "^1.0.0",
|
||||||
"@types/node": "^13.9.8",
|
"@types/node": "^24.0.13",
|
||||||
"@types/string-similarity": "^4.0.0",
|
"@types/string-similarity": "^4.0.0",
|
||||||
"airdcpp-apisocket": "^3.0.0-beta.8",
|
"airdcpp-apisocket": "^3.0.0-beta.8",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.6.8",
|
||||||
@@ -25,6 +27,7 @@
|
|||||||
"bree": "^7.1.5",
|
"bree": "^7.1.5",
|
||||||
"calibre-opds": "^1.0.7",
|
"calibre-opds": "^1.0.7",
|
||||||
"chokidar": "^4.0.3",
|
"chokidar": "^4.0.3",
|
||||||
|
"cors": "^2.8.5",
|
||||||
"delay": "^5.0.0",
|
"delay": "^5.0.0",
|
||||||
"dotenv": "^10.0.0",
|
"dotenv": "^10.0.0",
|
||||||
"filename-parser": "^1.0.4",
|
"filename-parser": "^1.0.4",
|
||||||
@@ -43,7 +46,7 @@
|
|||||||
"moleculer-db": "^0.8.23",
|
"moleculer-db": "^0.8.23",
|
||||||
"moleculer-db-adapter-mongoose": "^0.9.2",
|
"moleculer-db-adapter-mongoose": "^0.9.2",
|
||||||
"moleculer-io": "^2.2.0",
|
"moleculer-io": "^2.2.0",
|
||||||
"moleculer-web": "^0.10.5",
|
"moleculer-web": "^0.10.8",
|
||||||
"mongoosastic-ts": "^6.0.3",
|
"mongoosastic-ts": "^6.0.3",
|
||||||
"mongoose": "^6.10.4",
|
"mongoose": "^6.10.4",
|
||||||
"mongoose-paginate-v2": "^1.3.18",
|
"mongoose-paginate-v2": "^1.3.18",
|
||||||
@@ -62,6 +65,7 @@
|
|||||||
"@types/lodash": "^4.14.168",
|
"@types/lodash": "^4.14.168",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.56.0",
|
"@typescript-eslint/eslint-plugin": "^5.56.0",
|
||||||
"@typescript-eslint/parser": "^5.56.0",
|
"@typescript-eslint/parser": "^5.56.0",
|
||||||
|
"concurrently": "^9.2.0",
|
||||||
"eslint": "^8.36.0",
|
"eslint": "^8.36.0",
|
||||||
"eslint-plugin-import": "^2.20.2",
|
"eslint-plugin-import": "^2.20.2",
|
||||||
"eslint-plugin-prefer-arrow": "^1.2.2",
|
"eslint-plugin-prefer-arrow": "^1.2.2",
|
||||||
@@ -77,7 +81,7 @@
|
|||||||
"uuid": "^9.0.0"
|
"uuid": "^9.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 18.x.x"
|
"node": ">= 22.x.x"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@aashutoshrathi/word-wrap": {
|
"node_modules/@aashutoshrathi/word-wrap": {
|
||||||
@@ -110,6 +114,24 @@
|
|||||||
"graphql": "14.x || 15.x || 16.x"
|
"graphql": "14.x || 15.x || 16.x"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@apollo/federation-internals": {
|
||||||
|
"version": "2.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@apollo/federation-internals/-/federation-internals-2.11.2.tgz",
|
||||||
|
"integrity": "sha512-GSFGL2fLox3EBszWKJvRkVLFA0hkJF9PHGMQH+WdB/12KVB3QHKwDyW1T9VZtxe2SJhNU3puleSxCsO16Bf3iA==",
|
||||||
|
"license": "Elastic-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/uuid": "^9.0.0",
|
||||||
|
"chalk": "^4.1.0",
|
||||||
|
"js-levenshtein": "^1.1.6",
|
||||||
|
"uuid": "^9.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.15.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"graphql": "^16.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@apollo/protobufjs": {
|
"node_modules/@apollo/protobufjs": {
|
||||||
"version": "1.2.7",
|
"version": "1.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/@apollo/protobufjs/-/protobufjs-1.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/@apollo/protobufjs/-/protobufjs-1.2.7.tgz",
|
||||||
@@ -197,6 +219,22 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@apollo/subgraph": {
|
||||||
|
"version": "2.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@apollo/subgraph/-/subgraph-2.11.2.tgz",
|
||||||
|
"integrity": "sha512-S14osF5Zc8pd6lzeNtX1QHboMcQK5PXcN9EumZyRYBF0TRbnEFLF8Me9zMcfR3QP7GCiggjd6PA2IAaPC9uCSQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@apollo/cache-control-types": "^1.0.2",
|
||||||
|
"@apollo/federation-internals": "2.11.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.15.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"graphql": "^16.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@apollo/usage-reporting-protobuf": {
|
"node_modules/@apollo/usage-reporting-protobuf": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@apollo/usage-reporting-protobuf/-/usage-reporting-protobuf-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@apollo/usage-reporting-protobuf/-/usage-reporting-protobuf-4.1.1.tgz",
|
||||||
@@ -360,6 +398,19 @@
|
|||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@as-integrations/express4": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@as-integrations/express4/-/express4-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-a2pur5nko91UaqWYwNRmcMEtmxgZH9eQzpner2ht/2CNSDuC+PHU3K+/uiISVgLC+2b+1TPzvutPejkN/+bsTw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@apollo/server": "^4.0.0 || 5.0.0-rc.0",
|
||||||
|
"express": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@aws-crypto/sha256-browser": {
|
"node_modules/@aws-crypto/sha256-browser": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz",
|
||||||
@@ -2413,6 +2464,19 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@ltv/moleculer-apollo-server-mixin": {
|
||||||
|
"version": "0.1.30",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ltv/moleculer-apollo-server-mixin/-/moleculer-apollo-server-mixin-0.1.30.tgz",
|
||||||
|
"integrity": "sha512-/t1/aGwGIgwpwL2IMJl7WF/NtOKbJcpIObc31ur2ZaAYSjV+3anhTZgb9R2ePwdjdkZq5882vqCDJ8gnrrbiDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@apollo/server": "^4.3.2",
|
||||||
|
"@apollo/subgraph": "^2.3.0",
|
||||||
|
"graphql": "^16.6.0",
|
||||||
|
"lodash.defaultsdeep": "^4.6.1",
|
||||||
|
"lodash.omit": "^4.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@mongodb-js/saslprep": {
|
"node_modules/@mongodb-js/saslprep": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.2.2.tgz",
|
||||||
@@ -3493,9 +3557,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "13.13.52",
|
"version": "24.0.13",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.52.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.13.tgz",
|
||||||
"integrity": "sha512-s3nugnZumCC//n4moGGe6tkNMyYEdaDBitVjwPxXmR5lnMG5dHePinH2EdxkG3Rh1ghFHHixAG4NJhpJW1rthQ=="
|
"integrity": "sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~7.8.0"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node-fetch": {
|
"node_modules/@types/node-fetch": {
|
||||||
"version": "2.6.12",
|
"version": "2.6.12",
|
||||||
@@ -3568,6 +3636,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/string-similarity/-/string-similarity-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/string-similarity/-/string-similarity-4.0.0.tgz",
|
||||||
"integrity": "sha512-dMS4S07fbtY1AILG/RhuwmptmzK1Ql8scmAebOTJ/8iBtK/KI17NwGwKzu1uipjj8Kk+3mfPxum56kKZE93mzQ=="
|
"integrity": "sha512-dMS4S07fbtY1AILG/RhuwmptmzK1Ql8scmAebOTJ/8iBtK/KI17NwGwKzu1uipjj8Kk+3mfPxum56kKZE93mzQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/uuid": {
|
||||||
|
"version": "9.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz",
|
||||||
|
"integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/webidl-conversions": {
|
"node_modules/@types/webidl-conversions": {
|
||||||
"version": "7.0.3",
|
"version": "7.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
|
||||||
@@ -5148,6 +5222,48 @@
|
|||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
|
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/concurrently": {
|
||||||
|
"version": "9.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.0.tgz",
|
||||||
|
"integrity": "sha512-IsB/fiXTupmagMW4MNp2lx2cdSN2FfZq78vF90LBB+zZHArbIQZjQtzXCiXnvTxCZSvXanTqFLWBjw2UkLx1SQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"chalk": "^4.1.2",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"rxjs": "^7.8.1",
|
||||||
|
"shell-quote": "^1.8.1",
|
||||||
|
"supports-color": "^8.1.1",
|
||||||
|
"tree-kill": "^1.2.2",
|
||||||
|
"yargs": "^17.7.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"conc": "dist/bin/concurrently.js",
|
||||||
|
"concurrently": "dist/bin/concurrently.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/concurrently/node_modules/supports-color": {
|
||||||
|
"version": "8.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
||||||
|
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"has-flag": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/content-disposition": {
|
"node_modules/content-disposition": {
|
||||||
"version": "0.5.4",
|
"version": "0.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||||
@@ -5207,6 +5323,7 @@
|
|||||||
"version": "2.8.5",
|
"version": "2.8.5",
|
||||||
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
|
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
|
||||||
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
|
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"object-assign": "^4",
|
"object-assign": "^4",
|
||||||
"vary": "^1"
|
"vary": "^1"
|
||||||
@@ -9503,6 +9620,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
|
||||||
"integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg=="
|
"integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/js-levenshtein": {
|
||||||
|
"version": "1.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz",
|
||||||
|
"integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-priority-queue": {
|
"node_modules/js-priority-queue": {
|
||||||
"version": "0.1.5",
|
"version": "0.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/js-priority-queue/-/js-priority-queue-0.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/js-priority-queue/-/js-priority-queue-0.1.5.tgz",
|
||||||
@@ -9812,6 +9938,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
||||||
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="
|
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.defaultsdeep": {
|
||||||
|
"version": "4.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz",
|
||||||
|
"integrity": "sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.isarguments": {
|
"node_modules/lodash.isarguments": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
|
||||||
@@ -9828,6 +9960,13 @@
|
|||||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
|
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.omit": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==",
|
||||||
|
"deprecated": "This package is deprecated. Use destructuring assignment syntax instead.",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.sortby": {
|
"node_modules/lodash.sortby": {
|
||||||
"version": "4.7.0",
|
"version": "4.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
|
||||||
@@ -10598,9 +10737,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/moleculer-web": {
|
"node_modules/moleculer-web": {
|
||||||
"version": "0.10.7",
|
"version": "0.10.8",
|
||||||
"resolved": "https://registry.npmjs.org/moleculer-web/-/moleculer-web-0.10.7.tgz",
|
"resolved": "https://registry.npmjs.org/moleculer-web/-/moleculer-web-0.10.8.tgz",
|
||||||
"integrity": "sha512-/UJtV+O7iQ3aSg/xi/sw3ZswhvzkigzGPjKOR5R97sm2FSihKuLTftUpXlk4dYls7/8c8WSz6H/M/40BenEx9Q==",
|
"integrity": "sha512-kQtyN8AccdBqSZUh+PRLYmLPy7RBd48j/raA5682wNDo1fPEKdCHa8d7tUjtmI53gELkQZKCv3GckyMqdxZYXQ==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/busboy": "^1.0.0",
|
"@fastify/busboy": "^1.0.0",
|
||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.0",
|
||||||
@@ -15001,6 +15141,16 @@
|
|||||||
"queue-microtask": "^1.2.2"
|
"queue-microtask": "^1.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rxjs": {
|
||||||
|
"version": "7.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
||||||
|
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/safe-array-concat": {
|
"node_modules/safe-array-concat": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.0.tgz",
|
||||||
@@ -15302,6 +15452,19 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/shell-quote": {
|
||||||
|
"version": "1.8.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
|
||||||
|
"integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/side-channel": {
|
"node_modules/side-channel": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
|
||||||
@@ -16090,6 +16253,16 @@
|
|||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tree-kill": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"tree-kill": "cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/truncate-utf8-bytes": {
|
"node_modules/truncate-utf8-bytes": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
|
||||||
@@ -16479,6 +16652,12 @@
|
|||||||
"node": ">=14.0"
|
"node": ">=14.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "7.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
|
||||||
|
"integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/undici/node_modules/@fastify/busboy": {
|
"node_modules/undici/node_modules/@fastify/busboy": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
|
||||||
|
|||||||
10
package.json
10
package.json
@@ -23,6 +23,7 @@
|
|||||||
"@types/lodash": "^4.14.168",
|
"@types/lodash": "^4.14.168",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.56.0",
|
"@typescript-eslint/eslint-plugin": "^5.56.0",
|
||||||
"@typescript-eslint/parser": "^5.56.0",
|
"@typescript-eslint/parser": "^5.56.0",
|
||||||
|
"concurrently": "^9.2.0",
|
||||||
"eslint": "^8.36.0",
|
"eslint": "^8.36.0",
|
||||||
"eslint-plugin-import": "^2.20.2",
|
"eslint-plugin-import": "^2.20.2",
|
||||||
"eslint-plugin-prefer-arrow": "^1.2.2",
|
"eslint-plugin-prefer-arrow": "^1.2.2",
|
||||||
@@ -39,15 +40,17 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/server": "^4.12.2",
|
"@apollo/server": "^4.12.2",
|
||||||
|
"@as-integrations/express4": "^1.1.1",
|
||||||
"@bluelovers/fast-glob": "https://github.com/rishighan/fast-glob-v2-api.git",
|
"@bluelovers/fast-glob": "https://github.com/rishighan/fast-glob-v2-api.git",
|
||||||
"@elastic/elasticsearch": "^8.13.1",
|
"@elastic/elasticsearch": "^8.13.1",
|
||||||
"@jorgeferrero/stream-to-buffer": "^2.0.6",
|
"@jorgeferrero/stream-to-buffer": "^2.0.6",
|
||||||
|
"@ltv/moleculer-apollo-server-mixin": "^0.1.30",
|
||||||
"@npcz/magic": "^1.3.14",
|
"@npcz/magic": "^1.3.14",
|
||||||
"@root/walk": "^1.1.0",
|
"@root/walk": "^1.1.0",
|
||||||
"@socket.io/redis-adapter": "^8.1.0",
|
"@socket.io/redis-adapter": "^8.1.0",
|
||||||
"@types/jest": "^27.4.1",
|
"@types/jest": "^27.4.1",
|
||||||
"@types/mkdirp": "^1.0.0",
|
"@types/mkdirp": "^1.0.0",
|
||||||
"@types/node": "^13.9.8",
|
"@types/node": "^24.0.13",
|
||||||
"@types/string-similarity": "^4.0.0",
|
"@types/string-similarity": "^4.0.0",
|
||||||
"airdcpp-apisocket": "^3.0.0-beta.8",
|
"airdcpp-apisocket": "^3.0.0-beta.8",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.6.8",
|
||||||
@@ -55,6 +58,7 @@
|
|||||||
"bree": "^7.1.5",
|
"bree": "^7.1.5",
|
||||||
"calibre-opds": "^1.0.7",
|
"calibre-opds": "^1.0.7",
|
||||||
"chokidar": "^4.0.3",
|
"chokidar": "^4.0.3",
|
||||||
|
"cors": "^2.8.5",
|
||||||
"delay": "^5.0.0",
|
"delay": "^5.0.0",
|
||||||
"dotenv": "^10.0.0",
|
"dotenv": "^10.0.0",
|
||||||
"filename-parser": "^1.0.4",
|
"filename-parser": "^1.0.4",
|
||||||
@@ -73,7 +77,7 @@
|
|||||||
"moleculer-db": "^0.8.23",
|
"moleculer-db": "^0.8.23",
|
||||||
"moleculer-db-adapter-mongoose": "^0.9.2",
|
"moleculer-db-adapter-mongoose": "^0.9.2",
|
||||||
"moleculer-io": "^2.2.0",
|
"moleculer-io": "^2.2.0",
|
||||||
"moleculer-web": "^0.10.5",
|
"moleculer-web": "^0.10.8",
|
||||||
"mongoosastic-ts": "^6.0.3",
|
"mongoosastic-ts": "^6.0.3",
|
||||||
"mongoose": "^6.10.4",
|
"mongoose": "^6.10.4",
|
||||||
"mongoose-paginate-v2": "^1.3.18",
|
"mongoose-paginate-v2": "^1.3.18",
|
||||||
@@ -89,7 +93,7 @@
|
|||||||
"xml2js": "^0.6.2"
|
"xml2js": "^0.6.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 18.x.x"
|
"node": ">= 22.x.x"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"coverageDirectory": "<rootDir>/coverage",
|
"coverageDirectory": "<rootDir>/coverage",
|
||||||
|
|||||||
@@ -12,180 +12,207 @@ import { IFolderData } from "threetwo-ui-typings";
|
|||||||
* @extends Service
|
* @extends Service
|
||||||
*/
|
*/
|
||||||
export default class ApiService extends Service {
|
export default class ApiService extends Service {
|
||||||
/**
|
/**
|
||||||
* The chokidar file system watcher instance.
|
* The chokidar file system watcher instance.
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private fileWatcher?: any;
|
private fileWatcher?: any;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an instance of ApiService.
|
* Creates an instance of ApiService.
|
||||||
* @param {ServiceBroker} broker - The Moleculer service broker instance.
|
* @param {ServiceBroker} broker - The Moleculer service broker instance.
|
||||||
*/
|
*/
|
||||||
public constructor(broker: ServiceBroker) {
|
public constructor(broker: ServiceBroker) {
|
||||||
super(broker);
|
super(broker);
|
||||||
this.parseServiceSchema({
|
this.parseServiceSchema({
|
||||||
name: "api",
|
name: "api",
|
||||||
mixins: [ApiGateway],
|
mixins: [ApiGateway],
|
||||||
settings: {
|
settings: {
|
||||||
port: process.env.PORT || 3000,
|
port: process.env.PORT || 3000,
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: "/api",
|
path: "/graphql",
|
||||||
whitelist: ["**"],
|
whitelist: ["graphql.*"],
|
||||||
cors: {
|
bodyParsers: {
|
||||||
origin: "*",
|
json: true,
|
||||||
methods: ["GET", "OPTIONS", "POST", "PUT", "DELETE"],
|
urlencoded: { extended: true },
|
||||||
allowedHeaders: ["*"],
|
},
|
||||||
exposedHeaders: [],
|
aliases: {
|
||||||
credentials: false,
|
"POST /": "graphql.wantedComics",
|
||||||
maxAge: 3600,
|
},
|
||||||
},
|
cors: {
|
||||||
use: [],
|
origin: "*",
|
||||||
mergeParams: true,
|
methods: ["GET", "OPTIONS", "POST"],
|
||||||
authentication: false,
|
allowedHeaders: ["*"],
|
||||||
authorization: false,
|
credentials: false,
|
||||||
autoAliases: true,
|
},
|
||||||
aliases: {},
|
},
|
||||||
callingOptions: {},
|
{
|
||||||
bodyParsers: {
|
path: "/api",
|
||||||
json: { strict: false, limit: "1MB" },
|
whitelist: ["**"],
|
||||||
urlencoded: { extended: true, limit: "1MB" },
|
cors: {
|
||||||
},
|
origin: "*",
|
||||||
mappingPolicy: "all",
|
methods: [
|
||||||
logging: true,
|
"GET",
|
||||||
},
|
"OPTIONS",
|
||||||
{
|
"POST",
|
||||||
path: "/userdata",
|
"PUT",
|
||||||
use: [ApiGateway.serveStatic(path.resolve("./userdata"))],
|
"DELETE",
|
||||||
},
|
],
|
||||||
{
|
allowedHeaders: ["*"],
|
||||||
path: "/comics",
|
exposedHeaders: [],
|
||||||
use: [ApiGateway.serveStatic(path.resolve("./comics"))],
|
credentials: false,
|
||||||
},
|
maxAge: 3600,
|
||||||
{
|
},
|
||||||
path: "/logs",
|
use: [],
|
||||||
use: [ApiGateway.serveStatic("logs")],
|
mergeParams: true,
|
||||||
},
|
authentication: false,
|
||||||
],
|
authorization: false,
|
||||||
log4XXResponses: false,
|
autoAliases: true,
|
||||||
logRequestParams: true,
|
aliases: {},
|
||||||
logResponseData: true,
|
callingOptions: {},
|
||||||
assets: { folder: "public", options: {} },
|
bodyParsers: {
|
||||||
},
|
json: { strict: false, limit: "1MB" },
|
||||||
events: {},
|
urlencoded: { extended: true, limit: "1MB" },
|
||||||
methods: {},
|
},
|
||||||
started: this.startWatcher,
|
mappingPolicy: "all",
|
||||||
stopped: this.stopWatcher,
|
logging: true,
|
||||||
});
|
},
|
||||||
}
|
{
|
||||||
|
path: "/userdata",
|
||||||
|
use: [
|
||||||
|
ApiGateway.serveStatic(path.resolve("./userdata")),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/comics",
|
||||||
|
use: [ApiGateway.serveStatic(path.resolve("./comics"))],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/logs",
|
||||||
|
use: [ApiGateway.serveStatic("logs")],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
log4XXResponses: false,
|
||||||
|
logRequestParams: true,
|
||||||
|
logResponseData: true,
|
||||||
|
assets: { folder: "public", options: {} },
|
||||||
|
},
|
||||||
|
events: {},
|
||||||
|
methods: {},
|
||||||
|
started: this.startWatcher,
|
||||||
|
stopped: this.stopWatcher,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes and starts the chokidar watcher on the COMICS_DIRECTORY.
|
* Initializes and starts the chokidar watcher on the COMICS_DIRECTORY.
|
||||||
* Debounces rapid events and logs initial scan completion.
|
* Debounces rapid events and logs initial scan completion.
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private startWatcher(): void {
|
private startWatcher(): void {
|
||||||
const rawDir = process.env.COMICS_DIRECTORY;
|
const rawDir = process.env.COMICS_DIRECTORY;
|
||||||
if (!rawDir) {
|
if (!rawDir) {
|
||||||
this.logger.error("COMICS_DIRECTORY not set; cannot start watcher");
|
this.logger.error("COMICS_DIRECTORY not set; cannot start watcher");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const watchDir = path.resolve(rawDir);
|
const watchDir = path.resolve(rawDir);
|
||||||
this.logger.info(`Watching comics folder at: ${watchDir}`);
|
this.logger.info(`Watching comics folder at: ${watchDir}`);
|
||||||
if (!fs.existsSync(watchDir)) {
|
if (!fs.existsSync(watchDir)) {
|
||||||
this.logger.error(`✖ Comics folder does not exist: ${watchDir}`);
|
this.logger.error(`✖ Comics folder does not exist: ${watchDir}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.fileWatcher = chokidar.watch(watchDir, {
|
this.fileWatcher = chokidar.watch(watchDir, {
|
||||||
persistent: true,
|
persistent: true,
|
||||||
ignoreInitial: true,
|
ignoreInitial: true,
|
||||||
followSymlinks: true,
|
followSymlinks: true,
|
||||||
depth: 10,
|
depth: 10,
|
||||||
usePolling: true,
|
usePolling: true,
|
||||||
interval: 5000,
|
interval: 5000,
|
||||||
atomic: true,
|
atomic: true,
|
||||||
awaitWriteFinish: { stabilityThreshold: 2000, pollInterval: 100 },
|
awaitWriteFinish: { stabilityThreshold: 2000, pollInterval: 100 },
|
||||||
ignored: (p) => p.endsWith(".dctmp") || p.includes("/.git/"),
|
ignored: (p) => p.endsWith(".dctmp") || p.includes("/.git/"),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Debounced handler for file system events, batching rapid triggers
|
* Debounced handler for file system events, batching rapid triggers
|
||||||
* into a 200ms window. Leading and trailing calls invoked.
|
* into a 200ms window. Leading and trailing calls invoked.
|
||||||
* @param {string} event - Type of file event (add, change, etc.).
|
* @param {string} event - Type of file event (add, change, etc.).
|
||||||
* @param {string} p - Path of the file or directory.
|
* @param {string} p - Path of the file or directory.
|
||||||
* @param {fs.Stats} [stats] - Optional file stats for add/change events.
|
* @param {fs.Stats} [stats] - Optional file stats for add/change events.
|
||||||
*/
|
*/
|
||||||
const debouncedEvent = debounce(
|
const debouncedEvent = debounce(
|
||||||
(event: string, p: string, stats?: fs.Stats) => {
|
(event: string, p: string, stats?: fs.Stats) => {
|
||||||
try {
|
try {
|
||||||
this.handleFileEvent(event, p, stats);
|
this.handleFileEvent(event, p, stats);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
`Error handling file event [${event}] for ${p}:`,
|
`Error handling file event [${event}] for ${p}:`,
|
||||||
err
|
err
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
200,
|
200,
|
||||||
{ leading: true, trailing: true }
|
{ leading: true, trailing: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
this.fileWatcher
|
this.fileWatcher
|
||||||
.on("ready", () => this.logger.info("Initial scan complete."))
|
.on("ready", () => this.logger.info("Initial scan complete."))
|
||||||
.on("error", (err) => this.logger.error("Watcher error:", err))
|
.on("error", (err) => this.logger.error("Watcher error:", err))
|
||||||
.on("add", (p, stats) => debouncedEvent("add", p, stats))
|
.on("add", (p, stats) => debouncedEvent("add", p, stats))
|
||||||
.on("change", (p, stats) => debouncedEvent("change", p, stats))
|
.on("change", (p, stats) => debouncedEvent("change", p, stats))
|
||||||
.on("unlink", (p) => debouncedEvent("unlink", p))
|
.on("unlink", (p) => debouncedEvent("unlink", p))
|
||||||
.on("addDir", (p) => debouncedEvent("addDir", p))
|
.on("addDir", (p) => debouncedEvent("addDir", p))
|
||||||
.on("unlinkDir", (p) => debouncedEvent("unlinkDir", p));
|
.on("unlinkDir", (p) => debouncedEvent("unlinkDir", p));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stops and closes the chokidar watcher, freeing resources.
|
* Stops and closes the chokidar watcher, freeing resources.
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private async stopWatcher(): Promise<void> {
|
private async stopWatcher(): Promise<void> {
|
||||||
if (this.fileWatcher) {
|
if (this.fileWatcher) {
|
||||||
this.logger.info("Stopping file watcher...");
|
this.logger.info("Stopping file watcher...");
|
||||||
await this.fileWatcher.close();
|
await this.fileWatcher.close();
|
||||||
this.fileWatcher = undefined;
|
this.fileWatcher = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles a filesystem event by logging and optionally importing new files.
|
* Handles a filesystem event by logging and optionally importing new files.
|
||||||
* @param event - The type of chokidar event ('add', 'change', 'unlink', etc.).
|
* @param event - The type of chokidar event ('add', 'change', 'unlink', etc.).
|
||||||
* @param filePath - The full path of the file or directory that triggered the event.
|
* @param filePath - The full path of the file or directory that triggered the event.
|
||||||
* @param stats - Optional fs.Stats data for 'add' or 'change' events.
|
* @param stats - Optional fs.Stats data for 'add' or 'change' events.
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private async handleFileEvent(
|
private async handleFileEvent(
|
||||||
event: string,
|
event: string,
|
||||||
filePath: string,
|
filePath: string,
|
||||||
stats?: fs.Stats
|
stats?: fs.Stats
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
this.logger.info(`File event [${event}]: ${filePath}`);
|
this.logger.info(`File event [${event}]: ${filePath}`);
|
||||||
if (event === "add" && stats) {
|
if (event === "add" && stats) {
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
const newStats = await fs.promises.stat(filePath);
|
const newStats = await fs.promises.stat(filePath);
|
||||||
if (newStats.mtime.getTime() === stats.mtime.getTime()) {
|
if (newStats.mtime.getTime() === stats.mtime.getTime()) {
|
||||||
this.logger.info(`Stable file detected: ${filePath}, importing.`);
|
this.logger.info(
|
||||||
const folderData: IFolderData = await this.broker.call(
|
`Stable file detected: ${filePath}, importing.`
|
||||||
"library.walkFolders",
|
);
|
||||||
{ basePathToWalk: filePath }
|
const folderData: IFolderData = await this.broker.call(
|
||||||
);
|
"library.walkFolders",
|
||||||
// this would have to be a call to importDownloadedComic
|
{ basePathToWalk: filePath }
|
||||||
await this.broker.call("importqueue.processImport", {
|
);
|
||||||
fileObject: {
|
// this would have to be a call to importDownloadedComic
|
||||||
filePath,
|
await this.broker.call("importqueue.processImport", {
|
||||||
fileSize: folderData[0].fileSize,
|
fileObject: {
|
||||||
},
|
filePath,
|
||||||
});
|
fileSize: folderData[0].fileSize,
|
||||||
}
|
},
|
||||||
}, 3000);
|
});
|
||||||
}
|
}
|
||||||
this.broker.broadcast(event, { path: filePath });
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
this.broker.broadcast(event, { path: filePath });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
116
services/graphql.service.ts
Normal file
116
services/graphql.service.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
// services/graphql.service.ts
|
||||||
|
import { gql as ApolloMixin } from "@ltv/moleculer-apollo-server-mixin";
|
||||||
|
import { print } from "graphql";
|
||||||
|
import { typeDefs } from "../models/graphql/typedef";
|
||||||
|
import { ServiceSchema } from "moleculer";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface representing the structure of an ElasticSearch result.
|
||||||
|
*/
|
||||||
|
interface SearchResult {
|
||||||
|
hits: {
|
||||||
|
total: { value: number };
|
||||||
|
hits: any[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GraphQL Moleculer Service exposing typed resolvers via @ltv/moleculer-apollo-server-mixin.
|
||||||
|
* Includes resolver for fetching comics marked as "wanted".
|
||||||
|
*/
|
||||||
|
const GraphQLService: ServiceSchema = {
|
||||||
|
name: "graphql",
|
||||||
|
mixins: [ApolloMixin],
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
/**
|
||||||
|
* Resolver for fetching comics marked as "wanted" in ElasticSearch.
|
||||||
|
*
|
||||||
|
* Queries the `search.issue` Moleculer action using a filtered ES query
|
||||||
|
* that matches issues or volumes with a `wanted` flag.
|
||||||
|
*
|
||||||
|
* @param {number} [limit=25] - Maximum number of results to return.
|
||||||
|
* @param {number} [offset=0] - Starting index for paginated results.
|
||||||
|
* @returns {Promise<{ total: number, comics: any[] }>} - Total number of matches and result set.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* query {
|
||||||
|
* wantedComics(limit: 10, offset: 0) {
|
||||||
|
* total
|
||||||
|
* comics {
|
||||||
|
* _id
|
||||||
|
* _source {
|
||||||
|
* title
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
wantedComics: {
|
||||||
|
params: {
|
||||||
|
limit: {
|
||||||
|
type: "number",
|
||||||
|
integer: true,
|
||||||
|
min: 1,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
offset: {
|
||||||
|
type: "number",
|
||||||
|
integer: true,
|
||||||
|
min: 0,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async handler(ctx) {
|
||||||
|
const { limit = 25, offset = 0 } = ctx.params;
|
||||||
|
|
||||||
|
const eSQuery = {
|
||||||
|
bool: {
|
||||||
|
should: [
|
||||||
|
{ exists: { field: "wanted.issues" } },
|
||||||
|
{ exists: { field: "wanted.volume" } },
|
||||||
|
],
|
||||||
|
minimum_should_match: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = (await ctx.broker.call("search.issue", {
|
||||||
|
query: eSQuery,
|
||||||
|
pagination: { size: limit, from: offset },
|
||||||
|
type: "wanted",
|
||||||
|
trigger: "wantedComicsGraphQL",
|
||||||
|
})) as SearchResult;
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
wantedComics: {
|
||||||
|
total: result?.hits?.total?.value || 0,
|
||||||
|
comics:
|
||||||
|
result?.hits?.hits.map((hit) => hit._source) ||
|
||||||
|
[],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
settings: {
|
||||||
|
apolloServer: {
|
||||||
|
typeDefs: print(typeDefs), // If typeDefs is AST; remove print if it's raw SDL string
|
||||||
|
resolvers: {
|
||||||
|
Query: {
|
||||||
|
wantedComics: "graphql.wantedComics",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
path: "/graphql",
|
||||||
|
playground: true,
|
||||||
|
introspection: true,
|
||||||
|
context: ({ ctx }: any) => ({
|
||||||
|
broker: ctx.broker,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GraphQLService;
|
||||||
Reference in New Issue
Block a user