Compare commits
12 Commits
52cbed91f8
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7476e61d0b | ||
|
|
3e16b50c65 | ||
|
|
3229a527cc | ||
|
|
430cb1cff6 | ||
|
|
0bbe245e05 | ||
|
|
576a18a0e6 | ||
|
|
190a768d82 | ||
|
|
665aaf0922 | ||
|
|
9384566610 | ||
|
|
56a7734e8e | ||
|
|
e577b41a86 | ||
|
|
294f4077f0 |
25
.dockerignore
Normal file
25
.dockerignore
Normal file
@@ -0,0 +1,25 @@
|
||||
.git
|
||||
.gitignore
|
||||
.DS_Store
|
||||
*.log
|
||||
node_modules
|
||||
.pnpm-store
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
.venv
|
||||
venv
|
||||
.pytest_cache
|
||||
.mypy_cache
|
||||
coverage
|
||||
.coverage
|
||||
.nuxt
|
||||
.output
|
||||
dist
|
||||
build
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Build Docker Image
|
||||
name: Build and deploy Docker image
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -6,13 +6,12 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: builder
|
||||
env:
|
||||
IMAGE: gitea.dsrptlab.com/optovia/geo/geo
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Gitea Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
@@ -20,12 +19,11 @@ jobs:
|
||||
username: ${{ gitea.actor }}
|
||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||
|
||||
- name: Build and Push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: gitea.dsrptlab.com/optovia/geo/geo:latest
|
||||
- name: Build and push
|
||||
run: |
|
||||
docker build -t "$IMAGE:latest" -t "$IMAGE:${{ gitea.sha }}" .
|
||||
docker push "$IMAGE:latest"
|
||||
docker push "$IMAGE:${{ gitea.sha }}"
|
||||
|
||||
- name: Deploy to Dokploy
|
||||
run: curl -X POST "https://dokploy.optovia.ru/api/deploy/_9J00xDYcVjW0E8jhI-jj"
|
||||
run: curl -fsS -X POST "https://ind.dsrptlab.com/api/deploy/1CoHKHRVGNQ-QYRUNXc-F"
|
||||
|
||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "graphql-contracts"]
|
||||
path = graphql-contracts
|
||||
url = git@gitea.dsrptlab.com:optovia/geo-graphql-contracts.git
|
||||
20
Dockerfile
20
Dockerfile
@@ -1,23 +1,31 @@
|
||||
FROM node:22-alpine AS builder
|
||||
FROM node:22-alpine AS deps
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json ./
|
||||
RUN npm install
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
FROM deps AS builder
|
||||
|
||||
COPY tsconfig.json ./
|
||||
COPY src ./src
|
||||
RUN npm run build
|
||||
|
||||
FROM node:22-alpine
|
||||
FROM deps AS runtime-deps
|
||||
RUN npm prune --omit=dev
|
||||
|
||||
FROM node:22-alpine AS runtime
|
||||
|
||||
RUN apk add --no-cache curl jq
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json ./
|
||||
RUN npm install --omit=dev
|
||||
COPY --from=runtime-deps /app/node_modules ./node_modules
|
||||
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY scripts ./scripts
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["node", "dist/index.js"]
|
||||
CMD ["sh", "-c", ". /app/scripts/load-vault-env.sh && node dist/index.js"]
|
||||
|
||||
1
graphql-contracts
Submodule
1
graphql-contracts
Submodule
Submodule graphql-contracts added at f92ac6e367
2893
package-lock.json
generated
2893
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@@ -8,16 +8,14 @@
|
||||
"dev": "tsx --watch src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/server": "^4.11.3",
|
||||
"@fastify/cors": "^11.2.0",
|
||||
"@sentry/node": "^9.5.0",
|
||||
"arangojs": "^9.2.0",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^5.0.1",
|
||||
"h3-js": "^4.2.1"
|
||||
"fastify": "^5.8.5",
|
||||
"h3-js": "^4.2.1",
|
||||
"mercurius": "^16.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/node": "^22.13.0",
|
||||
"tsx": "^4.19.3",
|
||||
"typescript": "^5.7.3"
|
||||
|
||||
44
scripts/load-secrets.mjs
Normal file
44
scripts/load-secrets.mjs
Normal file
@@ -0,0 +1,44 @@
|
||||
import { InfisicalSDK } from "@infisical/sdk";
|
||||
import { writeFileSync } from "fs";
|
||||
|
||||
const INFISICAL_API_URL = process.env.INFISICAL_API_URL;
|
||||
const INFISICAL_CLIENT_ID = process.env.INFISICAL_CLIENT_ID;
|
||||
const INFISICAL_CLIENT_SECRET = process.env.INFISICAL_CLIENT_SECRET;
|
||||
const INFISICAL_PROJECT_ID = process.env.INFISICAL_PROJECT_ID;
|
||||
const INFISICAL_ENV = process.env.INFISICAL_ENV || "prod";
|
||||
const SECRET_PATHS = (process.env.INFISICAL_SECRET_PATHS || "/shared").split(",");
|
||||
|
||||
if (!INFISICAL_API_URL || !INFISICAL_CLIENT_ID || !INFISICAL_CLIENT_SECRET || !INFISICAL_PROJECT_ID) {
|
||||
process.stderr.write("Missing required Infisical environment variables\n");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const client = new InfisicalSDK({ siteUrl: INFISICAL_API_URL });
|
||||
|
||||
await client.auth().universalAuth.login({
|
||||
clientId: INFISICAL_CLIENT_ID,
|
||||
clientSecret: INFISICAL_CLIENT_SECRET,
|
||||
});
|
||||
|
||||
process.stderr.write(`Loading secrets from Infisical (env: ${INFISICAL_ENV})...\n`);
|
||||
|
||||
const envLines = [];
|
||||
|
||||
for (const secretPath of SECRET_PATHS) {
|
||||
const response = await client.secrets().listSecrets({
|
||||
projectId: INFISICAL_PROJECT_ID,
|
||||
environment: INFISICAL_ENV,
|
||||
secretPath: secretPath.trim(),
|
||||
expandSecretReferences: true,
|
||||
});
|
||||
|
||||
for (const secret of response.secrets) {
|
||||
const escapedValue = secret.secretValue.replace(/'/g, "'\\''");
|
||||
envLines.push(`export ${secret.secretKey}='${escapedValue}'`);
|
||||
}
|
||||
|
||||
process.stderr.write(` ${secretPath.trim()}: ${response.secrets.length} secrets loaded\n`);
|
||||
}
|
||||
|
||||
writeFileSync(".env.infisical", envLines.join("\n"));
|
||||
process.stderr.write("Secrets written to .env.infisical\n");
|
||||
60
scripts/load-vault-env.sh
Executable file
60
scripts/load-vault-env.sh
Executable file
@@ -0,0 +1,60 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
log() {
|
||||
printf '%s\n' "$*" >&2
|
||||
}
|
||||
|
||||
VAULT_ENABLED="${VAULT_ENABLED:-auto}"
|
||||
if [ "$VAULT_ENABLED" = "false" ] || [ "$VAULT_ENABLED" = "0" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -z "${VAULT_ADDR:-}" ] || [ -z "${VAULT_TOKEN:-}" ]; then
|
||||
if [ "$VAULT_ENABLED" = "true" ] || [ "$VAULT_ENABLED" = "1" ]; then
|
||||
log "Vault bootstrap is required but VAULT_ADDR or VAULT_TOKEN is missing."
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! command -v curl >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then
|
||||
log "Vault bootstrap requires curl and jq."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VAULT_KV_MOUNT="${VAULT_KV_MOUNT:-secret}"
|
||||
|
||||
load_secret_path() {
|
||||
path="$1"
|
||||
source_name="$2"
|
||||
if [ -z "$path" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
url="${VAULT_ADDR%/}/v1/${VAULT_KV_MOUNT}/data/${path}"
|
||||
response="$(curl -fsS -H "X-Vault-Token: $VAULT_TOKEN" "$url")" || {
|
||||
log "Failed to load Vault path ${VAULT_KV_MOUNT}/${path}."
|
||||
return 1
|
||||
}
|
||||
|
||||
encoded_items="$(printf '%s' "$response" | jq -r '.data.data // {} | to_entries[]? | @base64')"
|
||||
if [ -z "$encoded_items" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
old_ifs="${IFS}"
|
||||
IFS='
|
||||
'
|
||||
for encoded_item in $encoded_items; do
|
||||
key="$(printf '%s' "$encoded_item" | base64 -d | jq -r '.key')"
|
||||
value="$(printf '%s' "$encoded_item" | base64 -d | jq -r '.value | tostring')"
|
||||
export "$key=$value"
|
||||
done
|
||||
IFS="${old_ifs}"
|
||||
|
||||
log "Loaded Vault ${source_name} secrets from ${VAULT_KV_MOUNT}/${path}."
|
||||
}
|
||||
|
||||
load_secret_path "${VAULT_SHARED_PATH:-}" "shared"
|
||||
load_secret_path "${VAULT_PROJECT_PATH:-}" "project"
|
||||
15
src/db.ts
15
src/db.ts
@@ -1,6 +1,6 @@
|
||||
import { Database } from 'arangojs'
|
||||
|
||||
const ARANGODB_URL = process.env.ARANGODB_INTERNAL_URL || 'http://localhost:8529'
|
||||
const ARANGODB_URL = process.env.ARANGODB_URL || process.env.ARANGODB_INTERNAL_URL || 'http://localhost:8529'
|
||||
const ARANGODB_DATABASE = process.env.ARANGODB_DATABASE || 'optovia_maps'
|
||||
const ARANGODB_PASSWORD = process.env.ARANGODB_PASSWORD || ''
|
||||
|
||||
@@ -17,6 +17,19 @@ export function getDb(): Database {
|
||||
|
||||
export async function ensureGraph(): Promise<void> {
|
||||
const db = getDb()
|
||||
|
||||
const nodes = db.collection('nodes')
|
||||
if (!(await nodes.exists())) {
|
||||
console.log('Creating collection: nodes')
|
||||
await db.createCollection('nodes')
|
||||
}
|
||||
|
||||
const edges = db.collection('edges')
|
||||
if (!(await edges.exists())) {
|
||||
console.log('Creating edge collection: edges')
|
||||
await db.createEdgeCollection('edges')
|
||||
}
|
||||
|
||||
const graphs = await db.listGraphs()
|
||||
if (graphs.some(g => g.name === 'optovia_graph')) return
|
||||
|
||||
|
||||
@@ -14,22 +14,22 @@ export function distanceKm(lat1: number, lon1: number, lat2: number, lon2: numbe
|
||||
export type ArangoDoc = Record<string, any>
|
||||
|
||||
export interface RouteStage {
|
||||
from_uuid: string | null
|
||||
from_name: string | null
|
||||
from_lat: number | null
|
||||
from_lon: number | null
|
||||
to_uuid: string | null
|
||||
to_name: string | null
|
||||
to_lat: number | null
|
||||
to_lon: number | null
|
||||
distance_km: number
|
||||
travel_time_seconds: number
|
||||
transport_type: string | null
|
||||
fromUuid: string | null
|
||||
fromName: string | null
|
||||
fromLat: number | null
|
||||
fromLon: number | null
|
||||
toUuid: string | null
|
||||
toName: string | null
|
||||
toLat: number | null
|
||||
toLon: number | null
|
||||
distanceKm: number
|
||||
travelTimeSeconds: number
|
||||
transportType: string | null
|
||||
}
|
||||
|
||||
export interface RoutePath {
|
||||
total_distance_km: number
|
||||
total_time_seconds: number
|
||||
totalDistanceKm: number
|
||||
totalTimeSeconds: number
|
||||
stages: RouteStage[]
|
||||
}
|
||||
|
||||
@@ -37,17 +37,17 @@ function buildStage(fromDoc: ArangoDoc | undefined, toDoc: ArangoDoc | undefined
|
||||
const distance = edges.reduce((s, e) => s + (e.distance_km || 0), 0)
|
||||
const time = edges.reduce((s, e) => s + (e.travel_time_seconds || 0), 0)
|
||||
return {
|
||||
from_uuid: fromDoc?._key ?? null,
|
||||
from_name: fromDoc?.name ?? null,
|
||||
from_lat: fromDoc?.latitude ?? null,
|
||||
from_lon: fromDoc?.longitude ?? null,
|
||||
to_uuid: toDoc?._key ?? null,
|
||||
to_name: toDoc?.name ?? null,
|
||||
to_lat: toDoc?.latitude ?? null,
|
||||
to_lon: toDoc?.longitude ?? null,
|
||||
distance_km: distance,
|
||||
travel_time_seconds: time,
|
||||
transport_type: transportType,
|
||||
fromUuid: fromDoc?._key ?? null,
|
||||
fromName: fromDoc?.name ?? null,
|
||||
fromLat: fromDoc?.latitude ?? null,
|
||||
fromLon: fromDoc?.longitude ?? null,
|
||||
toUuid: toDoc?._key ?? null,
|
||||
toName: toDoc?.name ?? null,
|
||||
toLat: toDoc?.latitude ?? null,
|
||||
toLon: toDoc?.longitude ?? null,
|
||||
distanceKm: distance,
|
||||
travelTimeSeconds: time,
|
||||
transportType: transportType,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,8 +83,8 @@ export function buildRouteFromEdges(pathEdges: [string, string, ArangoDoc][], no
|
||||
stages.push(buildStage(nodeDocs.get(segmentStart!), nodeDocs.get(lastTo), currentType!, currentEdges))
|
||||
|
||||
return {
|
||||
total_distance_km: stages.reduce((s, st) => s + (st.distance_km || 0), 0),
|
||||
total_time_seconds: stages.reduce((s, st) => s + (st.travel_time_seconds || 0), 0),
|
||||
totalDistanceKm: stages.reduce((s, st) => s + (st.distanceKm || 0), 0),
|
||||
totalTimeSeconds: stages.reduce((s, st) => s + (st.travelTimeSeconds || 0), 0),
|
||||
stages,
|
||||
}
|
||||
}
|
||||
|
||||
46
src/index.ts
46
src/index.ts
@@ -1,33 +1,21 @@
|
||||
import express from 'express'
|
||||
import cors from 'cors'
|
||||
import { ApolloServer } from '@apollo/server'
|
||||
import { expressMiddleware } from '@apollo/server/express4'
|
||||
import * as Sentry from '@sentry/node'
|
||||
import { typeDefs, resolvers } from './schema.js'
|
||||
import Fastify from "fastify";
|
||||
import cors from "@fastify/cors";
|
||||
import mercurius from "mercurius";
|
||||
import { typeDefs, resolvers } from "./schema.js";
|
||||
|
||||
const PORT = parseInt(process.env.PORT || '8000', 10)
|
||||
const SENTRY_DSN = process.env.SENTRY_DSN || ''
|
||||
const PORT = Number.parseInt(process.env.PORT || "8000", 10);
|
||||
|
||||
if (SENTRY_DSN) {
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
tracesSampleRate: 0.01,
|
||||
release: process.env.RELEASE_VERSION || '1.0.0',
|
||||
environment: process.env.ENVIRONMENT || 'production',
|
||||
})
|
||||
}
|
||||
const app = Fastify();
|
||||
await app.register(cors, { origin: ["https://optovia.ru"], credentials: true });
|
||||
await app.register(mercurius, {
|
||||
schema: typeDefs,
|
||||
resolvers,
|
||||
path: "/graphql/public",
|
||||
graphiql: true,
|
||||
});
|
||||
|
||||
const app = express()
|
||||
app.use(cors({ origin: ['https://optovia.ru'], credentials: true }))
|
||||
app.get("/health", async () => ({ status: "ok" }));
|
||||
|
||||
const server = new ApolloServer({ typeDefs, resolvers, introspection: true })
|
||||
await server.start()
|
||||
|
||||
app.use('/graphql/public', express.json(), expressMiddleware(server) as unknown as express.RequestHandler)
|
||||
|
||||
app.get('/health', (_, res) => { res.json({ status: 'ok' }) })
|
||||
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`Geo server ready on port ${PORT}`)
|
||||
console.log(` /graphql/public - public (no auth)`)
|
||||
})
|
||||
await app.listen({ port: PORT, host: "0.0.0.0" });
|
||||
console.log(`Geo server ready on port ${PORT}`);
|
||||
console.log(` /graphql/public - public (no auth)`);
|
||||
|
||||
340
src/schema.ts
340
src/schema.ts
@@ -8,13 +8,13 @@ const MAX_EXPANSIONS = 20000
|
||||
|
||||
export const typeDefs = `#graphql
|
||||
type Edge {
|
||||
to_uuid: String
|
||||
to_name: String
|
||||
to_latitude: Float
|
||||
to_longitude: Float
|
||||
distance_km: Float
|
||||
travel_time_seconds: Int
|
||||
transport_type: String
|
||||
toUuid: String
|
||||
toName: String
|
||||
toLatitude: Float
|
||||
toLongitude: Float
|
||||
distanceKm: Float
|
||||
travelTimeSeconds: Int
|
||||
transportType: String
|
||||
}
|
||||
|
||||
type Node {
|
||||
@@ -23,51 +23,51 @@ export const typeDefs = `#graphql
|
||||
latitude: Float
|
||||
longitude: Float
|
||||
country: String
|
||||
country_code: String
|
||||
synced_at: String
|
||||
transport_types: [String]
|
||||
countryCode: String
|
||||
syncedAt: String
|
||||
transportTypes: [String]
|
||||
edges: [Edge]
|
||||
distance_km: Float
|
||||
distanceKm: Float
|
||||
}
|
||||
|
||||
type NodeConnections {
|
||||
hub: Node
|
||||
rail_node: Node
|
||||
auto_edges: [Edge]
|
||||
rail_edges: [Edge]
|
||||
railNode: Node
|
||||
autoEdges: [Edge]
|
||||
railEdges: [Edge]
|
||||
}
|
||||
|
||||
type Route {
|
||||
distance_km: Float
|
||||
distanceKm: Float
|
||||
geometry: JSON
|
||||
}
|
||||
|
||||
type RouteStage {
|
||||
from_uuid: String
|
||||
from_name: String
|
||||
from_lat: Float
|
||||
from_lon: Float
|
||||
to_uuid: String
|
||||
to_name: String
|
||||
to_lat: Float
|
||||
to_lon: Float
|
||||
distance_km: Float
|
||||
travel_time_seconds: Int
|
||||
transport_type: String
|
||||
fromUuid: String
|
||||
fromName: String
|
||||
fromLat: Float
|
||||
fromLon: Float
|
||||
toUuid: String
|
||||
toName: String
|
||||
toLat: Float
|
||||
toLon: Float
|
||||
distanceKm: Float
|
||||
travelTimeSeconds: Int
|
||||
transportType: String
|
||||
}
|
||||
|
||||
type RoutePath {
|
||||
total_distance_km: Float
|
||||
total_time_seconds: Int
|
||||
totalDistanceKm: Float
|
||||
totalTimeSeconds: Int
|
||||
stages: [RouteStage]
|
||||
}
|
||||
|
||||
type ProductRouteOption {
|
||||
source_uuid: String
|
||||
source_name: String
|
||||
source_lat: Float
|
||||
source_lon: Float
|
||||
distance_km: Float
|
||||
sourceUuid: String
|
||||
sourceName: String
|
||||
sourceLat: Float
|
||||
sourceLon: Float
|
||||
distanceKm: Float
|
||||
routes: [RoutePath]
|
||||
}
|
||||
|
||||
@@ -76,14 +76,14 @@ export const typeDefs = `#graphql
|
||||
latitude: Float
|
||||
longitude: Float
|
||||
count: Int
|
||||
expansion_zoom: Int
|
||||
expansionZoom: Int
|
||||
name: String
|
||||
}
|
||||
|
||||
type Product {
|
||||
uuid: String
|
||||
name: String
|
||||
offers_count: Int
|
||||
offersCount: Int
|
||||
}
|
||||
|
||||
type Supplier {
|
||||
@@ -91,41 +91,41 @@ export const typeDefs = `#graphql
|
||||
name: String
|
||||
latitude: Float
|
||||
longitude: Float
|
||||
distance_km: Float
|
||||
distanceKm: Float
|
||||
}
|
||||
|
||||
type OfferNode {
|
||||
uuid: String
|
||||
product_uuid: String
|
||||
product_name: String
|
||||
supplier_uuid: String
|
||||
supplier_name: String
|
||||
productUuid: String
|
||||
productName: String
|
||||
supplierUuid: String
|
||||
supplierName: String
|
||||
latitude: Float
|
||||
longitude: Float
|
||||
country: String
|
||||
country_code: String
|
||||
price_per_unit: String
|
||||
countryCode: String
|
||||
pricePerUnit: String
|
||||
currency: String
|
||||
quantity: String
|
||||
unit: String
|
||||
distance_km: Float
|
||||
distanceKm: Float
|
||||
}
|
||||
|
||||
type OfferWithRoute {
|
||||
uuid: String
|
||||
product_uuid: String
|
||||
product_name: String
|
||||
supplier_uuid: String
|
||||
supplier_name: String
|
||||
productUuid: String
|
||||
productName: String
|
||||
supplierUuid: String
|
||||
supplierName: String
|
||||
latitude: Float
|
||||
longitude: Float
|
||||
country: String
|
||||
country_code: String
|
||||
price_per_unit: String
|
||||
countryCode: String
|
||||
pricePerUnit: String
|
||||
currency: String
|
||||
quantity: String
|
||||
unit: String
|
||||
distance_km: Float
|
||||
distanceKm: Float
|
||||
routes: [RoutePath]
|
||||
}
|
||||
|
||||
@@ -133,32 +133,32 @@ export const typeDefs = `#graphql
|
||||
|
||||
type Query {
|
||||
node(uuid: String!): Node
|
||||
nodes(limit: Int, offset: Int, transport_type: String, country: String, search: String, west: Float, south: Float, east: Float, north: Float): [Node!]!
|
||||
nodes_count(transport_type: String, country: String, west: Float, south: Float, east: Float, north: Float): Int!
|
||||
hub_countries: [String!]!
|
||||
nearest_nodes(lat: Float!, lon: Float!, limit: Int): [Node!]!
|
||||
node_connections(uuid: String!, limit_auto: Int, limit_rail: Int): NodeConnections
|
||||
auto_route(from_lat: Float!, from_lon: Float!, to_lat: Float!, to_lon: Float!): Route
|
||||
rail_route(from_lat: Float!, from_lon: Float!, to_lat: Float!, to_lon: Float!): Route
|
||||
clustered_nodes(west: Float!, south: Float!, east: Float!, north: Float!, zoom: Int!, transport_type: String, node_type: String): [ClusterPoint!]!
|
||||
nodes(limit: Int, offset: Int, transportType: String, country: String, search: String, west: Float, south: Float, east: Float, north: Float): [Node!]!
|
||||
nodesCount(transportType: String, country: String, west: Float, south: Float, east: Float, north: Float): Int!
|
||||
hubCountries: [String!]!
|
||||
nearestNodes(lat: Float!, lon: Float!, limit: Int): [Node!]!
|
||||
nodeConnections(uuid: String!, limitAuto: Int, limitRail: Int): NodeConnections
|
||||
autoRoute(fromLat: Float!, fromLon: Float!, toLat: Float!, toLon: Float!): Route
|
||||
railRoute(fromLat: Float!, fromLon: Float!, toLat: Float!, toLon: Float!): Route
|
||||
clusteredNodes(west: Float!, south: Float!, east: Float!, north: Float!, zoom: Int!, transportType: String, nodeType: String): [ClusterPoint!]!
|
||||
products: [Product!]!
|
||||
offers_by_product(product_uuid: String!): [OfferNode!]!
|
||||
hubs_near_offer(offer_uuid: String!, limit: Int): [Node!]!
|
||||
offersByProduct(productUuid: String!): [OfferNode!]!
|
||||
hubsNearOffer(offerUuid: String!, limit: Int): [Node!]!
|
||||
suppliers: [Supplier!]!
|
||||
products_by_supplier(supplier_uuid: String!): [Product!]!
|
||||
offers_by_supplier_product(supplier_uuid: String!, product_uuid: String!): [OfferNode!]!
|
||||
products_near_hub(hub_uuid: String!, radius_km: Float): [Product!]!
|
||||
suppliers_for_product(product_uuid: String!): [Supplier!]!
|
||||
hubs_for_product(product_uuid: String!, radius_km: Float): [Node!]!
|
||||
offers_by_hub(hub_uuid: String!, product_uuid: String!, limit: Int): [ProductRouteOption!]!
|
||||
offer_to_hub(offer_uuid: String!, hub_uuid: String!): ProductRouteOption
|
||||
nearest_hubs(lat: Float!, lon: Float!, radius: Float, product_uuid: String, limit: Int): [Node!]!
|
||||
nearest_offers(lat: Float!, lon: Float!, radius: Float, product_uuid: String, hub_uuid: String, limit: Int): [OfferWithRoute!]!
|
||||
nearest_suppliers(lat: Float!, lon: Float!, radius: Float, product_uuid: String, limit: Int): [Supplier!]!
|
||||
route_to_coordinate(offer_uuid: String!, lat: Float!, lon: Float!): ProductRouteOption
|
||||
hubs_list(limit: Int, offset: Int, country: String, transport_type: String, west: Float, south: Float, east: Float, north: Float): [Node!]!
|
||||
suppliers_list(limit: Int, offset: Int, country: String, west: Float, south: Float, east: Float, north: Float): [Supplier!]!
|
||||
products_list(limit: Int, offset: Int, west: Float, south: Float, east: Float, north: Float): [Product!]!
|
||||
productsBySupplier(supplierUuid: String!): [Product!]!
|
||||
offersBySupplierProduct(supplierUuid: String!, productUuid: String!): [OfferNode!]!
|
||||
productsNearHub(hubUuid: String!, radiusKm: Float): [Product!]!
|
||||
suppliersForProduct(productUuid: String!): [Supplier!]!
|
||||
hubsForProduct(productUuid: String!, radiusKm: Float): [Node!]!
|
||||
offersByHub(hubUuid: String!, productUuid: String!, limit: Int): [ProductRouteOption!]!
|
||||
offerToHub(offerUuid: String!, hubUuid: String!): ProductRouteOption
|
||||
nearestHubs(lat: Float!, lon: Float!, radius: Float, productUuid: String, limit: Int): [Node!]!
|
||||
nearestOffers(lat: Float!, lon: Float!, radius: Float, productUuid: String, hubUuid: String, limit: Int): [OfferWithRoute!]!
|
||||
nearestSuppliers(lat: Float!, lon: Float!, radius: Float, productUuid: String, limit: Int): [Supplier!]!
|
||||
routeToCoordinate(offerUuid: String!, lat: Float!, lon: Float!): ProductRouteOption
|
||||
hubsList(limit: Int, offset: Int, country: String, transportType: String, west: Float, south: Float, east: Float, north: Float): [Node!]!
|
||||
suppliersList(limit: Int, offset: Int, country: String, west: Float, south: Float, east: Float, north: Float): [Supplier!]!
|
||||
productsList(limit: Int, offset: Int, west: Float, south: Float, east: Float, north: Float): [Product!]!
|
||||
}
|
||||
`
|
||||
|
||||
@@ -169,30 +169,42 @@ function mapNode(doc: ArangoDoc, includeEdges = false) {
|
||||
latitude: doc.latitude ?? null,
|
||||
longitude: doc.longitude ?? null,
|
||||
country: doc.country ?? null,
|
||||
country_code: doc.country_code ?? null,
|
||||
synced_at: doc.synced_at ?? null,
|
||||
transport_types: doc.transport_types || [],
|
||||
countryCode: doc.country_code ?? null,
|
||||
syncedAt: doc.synced_at ?? null,
|
||||
transportTypes: doc.transport_types || [],
|
||||
edges: includeEdges ? (doc.edges || []) : [],
|
||||
distance_km: doc.distance_km ?? null,
|
||||
distanceKm: doc.distance_km ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
function mapOffer(doc: ArangoDoc) {
|
||||
return {
|
||||
uuid: doc._key,
|
||||
product_uuid: doc.product_uuid ?? null,
|
||||
product_name: doc.product_name ?? null,
|
||||
supplier_uuid: doc.supplier_uuid ?? null,
|
||||
supplier_name: doc.supplier_name ?? null,
|
||||
productUuid: doc.product_uuid ?? null,
|
||||
productName: doc.product_name ?? null,
|
||||
supplierUuid: doc.supplier_uuid ?? null,
|
||||
supplierName: doc.supplier_name ?? null,
|
||||
latitude: doc.latitude ?? null,
|
||||
longitude: doc.longitude ?? null,
|
||||
country: doc.country ?? null,
|
||||
country_code: doc.country_code ?? null,
|
||||
price_per_unit: doc.price_per_unit ?? null,
|
||||
countryCode: doc.country_code ?? null,
|
||||
pricePerUnit: doc.price_per_unit ?? null,
|
||||
currency: doc.currency ?? null,
|
||||
quantity: doc.quantity ?? null,
|
||||
unit: doc.unit ?? null,
|
||||
distance_km: doc.distance_km ?? null,
|
||||
distanceKm: doc.distance_km ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
function mapEdge(doc: ArangoDoc) {
|
||||
return {
|
||||
toUuid: doc.to_uuid ?? doc._key ?? null,
|
||||
toName: doc.to_name ?? doc.name ?? null,
|
||||
toLatitude: doc.to_latitude ?? doc.latitude ?? null,
|
||||
toLongitude: doc.to_longitude ?? doc.longitude ?? null,
|
||||
distanceKm: doc.distance_km ?? null,
|
||||
travelTimeSeconds: doc.travel_time_seconds ?? null,
|
||||
transportType: doc.transport_type ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,11 +326,11 @@ async function resolveOfferToHubInternal(offerUuid: string, hubUuid: string): Pr
|
||||
}
|
||||
|
||||
return {
|
||||
source_uuid: offerUuid,
|
||||
source_name: offer.name || offer.product_name,
|
||||
source_lat: offerLat,
|
||||
source_lon: offerLon,
|
||||
distance_km: dm,
|
||||
sourceUuid: offerUuid,
|
||||
sourceName: offer.name || offer.product_name,
|
||||
sourceLat: offerLat,
|
||||
sourceLon: offerLon,
|
||||
distanceKm: dm,
|
||||
routes: route ? [route] : [],
|
||||
}
|
||||
}
|
||||
@@ -360,7 +372,7 @@ export const resolvers = {
|
||||
},
|
||||
|
||||
Product: {
|
||||
offers_count: async (parent: { uuid: string }) => {
|
||||
offersCount: async (parent: { uuid: string }) => {
|
||||
const db = getDb()
|
||||
try {
|
||||
const cursor = await db.query(`
|
||||
@@ -399,10 +411,10 @@ export const resolvers = {
|
||||
`, { from_id: `nodes/${args.uuid}` })
|
||||
const edges = await cursor.all()
|
||||
|
||||
return { ...mapNode(node, true), edges }
|
||||
return { ...mapNode(node, true), edges: edges.map(mapEdge) }
|
||||
},
|
||||
|
||||
nodes: async (_: unknown, args: { limit?: number; offset?: number; transport_type?: string; country?: string; search?: string; west?: number; south?: number; east?: number; north?: number }) => {
|
||||
nodes: async (_: unknown, args: { limit?: number; offset?: number; transportType?: string; country?: string; search?: string; west?: number; south?: number; east?: number; north?: number }) => {
|
||||
const db = getDb()
|
||||
const bounds = boundsFilter(args)
|
||||
|
||||
@@ -418,7 +430,7 @@ export const resolvers = {
|
||||
LIMIT @offset, @limit
|
||||
RETURN node
|
||||
`, {
|
||||
transport_type: args.transport_type ?? null,
|
||||
transport_type: args.transportType ?? null,
|
||||
country: args.country ?? null,
|
||||
search: args.search ?? null,
|
||||
offset: args.offset ?? 0,
|
||||
@@ -429,7 +441,7 @@ export const resolvers = {
|
||||
return nodes.map((n: ArangoDoc) => mapNode(n))
|
||||
},
|
||||
|
||||
nodes_count: async (_: unknown, args: { transport_type?: string; country?: string; west?: number; south?: number; east?: number; north?: number }) => {
|
||||
nodesCount: async (_: unknown, args: { transportType?: string; country?: string; west?: number; south?: number; east?: number; north?: number }) => {
|
||||
const db = getDb()
|
||||
const bounds = boundsFilter(args)
|
||||
|
||||
@@ -442,11 +454,11 @@ export const resolvers = {
|
||||
${bounds.filter}
|
||||
COLLECT WITH COUNT INTO length
|
||||
RETURN length
|
||||
`, { transport_type: args.transport_type ?? null, country: args.country ?? null, ...bounds.vars })
|
||||
`, { transport_type: args.transportType ?? null, country: args.country ?? null, ...bounds.vars })
|
||||
return (await cursor.next()) ?? 0
|
||||
},
|
||||
|
||||
hub_countries: async () => {
|
||||
hubCountries: async () => {
|
||||
const db = getDb()
|
||||
const cursor = await db.query(`
|
||||
FOR node IN nodes
|
||||
@@ -459,7 +471,7 @@ export const resolvers = {
|
||||
return cursor.all()
|
||||
},
|
||||
|
||||
nearest_nodes: async (_: unknown, args: { lat: number; lon: number; limit?: number }) => {
|
||||
nearestNodes: async (_: unknown, args: { lat: number; lon: number; limit?: number }) => {
|
||||
const db = getDb()
|
||||
const cursor = await db.query(`
|
||||
FOR node IN nodes
|
||||
@@ -474,7 +486,7 @@ export const resolvers = {
|
||||
return nodes.map((n: ArangoDoc) => mapNode(n))
|
||||
},
|
||||
|
||||
node_connections: async (_: unknown, args: { uuid: string; limit_auto?: number; limit_rail?: number }) => {
|
||||
nodeConnections: async (_: unknown, args: { uuid: string; limitAuto?: number; limitRail?: number }) => {
|
||||
const db = getDb()
|
||||
const nodesCol = db.collection('nodes')
|
||||
const hub = await nodesCol.document(args.uuid).catch(() => null)
|
||||
@@ -524,8 +536,8 @@ export const resolvers = {
|
||||
hub_lat: hub.latitude,
|
||||
hub_lon: hub.longitude,
|
||||
hub_has_rail: (hub.transport_types || []).includes('rail'),
|
||||
limit_auto: args.limit_auto ?? 12,
|
||||
limit_rail: args.limit_rail ?? 12,
|
||||
limit_auto: args.limitAuto ?? 12,
|
||||
limit_rail: args.limitRail ?? 12,
|
||||
})
|
||||
|
||||
const result = await cursor.next()
|
||||
@@ -533,16 +545,16 @@ export const resolvers = {
|
||||
|
||||
return {
|
||||
hub: result.hub ? mapNode(result.hub) : null,
|
||||
rail_node: result.rail_node ? mapNode(result.rail_node) : null,
|
||||
auto_edges: result.auto_edges || [],
|
||||
rail_edges: result.rail_edges || [],
|
||||
railNode: result.rail_node ? mapNode(result.rail_node) : null,
|
||||
autoEdges: (result.auto_edges || []).map(mapEdge),
|
||||
railEdges: (result.rail_edges || []).map(mapEdge),
|
||||
}
|
||||
},
|
||||
|
||||
auto_route: async (_: unknown, args: { from_lat: number; from_lon: number; to_lat: number; to_lon: number }) => {
|
||||
autoRoute: async (_: unknown, args: { fromLat: number; fromLon: number; toLat: number; toLon: number }) => {
|
||||
const url = new URL('/route', GRAPHHOPPER_URL)
|
||||
url.searchParams.append('point', `${args.from_lat},${args.from_lon}`)
|
||||
url.searchParams.append('point', `${args.to_lat},${args.to_lon}`)
|
||||
url.searchParams.append('point', `${args.fromLat},${args.fromLon}`)
|
||||
url.searchParams.append('point', `${args.toLat},${args.toLon}`)
|
||||
url.searchParams.append('profile', 'car')
|
||||
url.searchParams.append('instructions', 'false')
|
||||
url.searchParams.append('calc_points', 'true')
|
||||
@@ -553,16 +565,16 @@ export const resolvers = {
|
||||
const data = await res.json() as ArangoDoc
|
||||
if (data.paths?.length > 0) {
|
||||
const path = data.paths[0]
|
||||
return { distance_km: Math.round((path.distance || 0) / 10) / 100, geometry: path.points?.coordinates || [] }
|
||||
return { distanceKm: Math.round((path.distance || 0) / 10) / 100, geometry: path.points?.coordinates || [] }
|
||||
}
|
||||
} catch (e) { console.error('GraphHopper request failed:', e) }
|
||||
return null
|
||||
},
|
||||
|
||||
rail_route: async (_: unknown, args: { from_lat: number; from_lon: number; to_lat: number; to_lon: number }) => {
|
||||
railRoute: async (_: unknown, args: { fromLat: number; fromLon: number; toLat: number; toLon: number }) => {
|
||||
const url = new URL('/route', OPENRAILROUTING_URL)
|
||||
url.searchParams.append('point', `${args.from_lat},${args.from_lon}`)
|
||||
url.searchParams.append('point', `${args.to_lat},${args.to_lon}`)
|
||||
url.searchParams.append('point', `${args.fromLat},${args.fromLon}`)
|
||||
url.searchParams.append('point', `${args.toLat},${args.toLon}`)
|
||||
url.searchParams.append('profile', 'all_tracks')
|
||||
url.searchParams.append('calc_points', 'true')
|
||||
url.searchParams.append('points_encoded', 'false')
|
||||
@@ -572,14 +584,22 @@ export const resolvers = {
|
||||
const data = await res.json() as ArangoDoc
|
||||
if (data.paths?.length > 0) {
|
||||
const path = data.paths[0]
|
||||
return { distance_km: Math.round((path.distance || 0) / 10) / 100, geometry: path.points?.coordinates || [] }
|
||||
return { distanceKm: Math.round((path.distance || 0) / 10) / 100, geometry: path.points?.coordinates || [] }
|
||||
}
|
||||
} catch (e) { console.error('OpenRailRouting request failed:', e) }
|
||||
return null
|
||||
},
|
||||
|
||||
clustered_nodes: async (_: unknown, args: { west: number; south: number; east: number; north: number; zoom: number; transport_type?: string; node_type?: string }) => {
|
||||
return getClusteredNodes(args.west, args.south, args.east, args.north, args.zoom, args.transport_type, args.node_type)
|
||||
clusteredNodes: async (_: unknown, args: { west: number; south: number; east: number; north: number; zoom: number; transportType?: string; nodeType?: string }) => {
|
||||
const points = await getClusteredNodes(args.west, args.south, args.east, args.north, args.zoom, args.transportType, args.nodeType)
|
||||
return points.map((p: ArangoDoc) => ({
|
||||
id: p.id,
|
||||
latitude: p.latitude,
|
||||
longitude: p.longitude,
|
||||
count: p.count,
|
||||
expansionZoom: p.expansion_zoom ?? p.expansionZoom ?? null,
|
||||
name: p.name ?? null,
|
||||
}))
|
||||
},
|
||||
|
||||
products: async () => {
|
||||
@@ -595,22 +615,22 @@ export const resolvers = {
|
||||
return cursor.all()
|
||||
},
|
||||
|
||||
offers_by_product: async (_: unknown, args: { product_uuid: string }) => {
|
||||
offersByProduct: async (_: unknown, args: { productUuid: string }) => {
|
||||
const db = getDb()
|
||||
const cursor = await db.query(`
|
||||
FOR node IN nodes
|
||||
FILTER node.node_type == 'offer'
|
||||
FILTER node.product_uuid == @product_uuid
|
||||
RETURN node
|
||||
`, { product_uuid: args.product_uuid })
|
||||
`, { product_uuid: args.productUuid })
|
||||
const nodes = await cursor.all()
|
||||
return nodes.map(mapOffer)
|
||||
},
|
||||
|
||||
hubs_near_offer: async (_: unknown, args: { offer_uuid: string; limit?: number }) => {
|
||||
hubsNearOffer: async (_: unknown, args: { offerUuid: string; limit?: number }) => {
|
||||
const db = getDb()
|
||||
const nodesCol = db.collection('nodes')
|
||||
const offer = await nodesCol.document(args.offer_uuid).catch(() => null)
|
||||
const offer = await nodesCol.document(args.offerUuid).catch(() => null)
|
||||
if (!offer || offer.latitude == null || offer.longitude == null) return []
|
||||
|
||||
const cursor = await db.query(`
|
||||
@@ -639,7 +659,7 @@ export const resolvers = {
|
||||
return cursor.all()
|
||||
},
|
||||
|
||||
products_by_supplier: async (_: unknown, args: { supplier_uuid: string }) => {
|
||||
productsBySupplier: async (_: unknown, args: { supplierUuid: string }) => {
|
||||
const db = getDb()
|
||||
const cursor = await db.query(`
|
||||
FOR node IN nodes
|
||||
@@ -649,11 +669,11 @@ export const resolvers = {
|
||||
COLLECT product_uuid = node.product_uuid INTO offers
|
||||
LET first_offer = FIRST(offers).node
|
||||
RETURN { uuid: product_uuid, name: first_offer.product_name }
|
||||
`, { supplier_uuid: args.supplier_uuid })
|
||||
`, { supplier_uuid: args.supplierUuid })
|
||||
return cursor.all()
|
||||
},
|
||||
|
||||
offers_by_supplier_product: async (_: unknown, args: { supplier_uuid: string; product_uuid: string }) => {
|
||||
offersBySupplierProduct: async (_: unknown, args: { supplierUuid: string; productUuid: string }) => {
|
||||
const db = getDb()
|
||||
const cursor = await db.query(`
|
||||
FOR node IN nodes
|
||||
@@ -661,15 +681,15 @@ export const resolvers = {
|
||||
FILTER node.supplier_uuid == @supplier_uuid
|
||||
FILTER node.product_uuid == @product_uuid
|
||||
RETURN node
|
||||
`, { supplier_uuid: args.supplier_uuid, product_uuid: args.product_uuid })
|
||||
`, { supplier_uuid: args.supplierUuid, product_uuid: args.productUuid })
|
||||
const nodes = await cursor.all()
|
||||
return nodes.map(mapOffer)
|
||||
},
|
||||
|
||||
products_near_hub: async (_: unknown, args: { hub_uuid: string; radius_km?: number }) => {
|
||||
productsNearHub: async (_: unknown, args: { hubUuid: string; radiusKm?: number }) => {
|
||||
const db = getDb()
|
||||
const nodesCol = db.collection('nodes')
|
||||
const hub = await nodesCol.document(args.hub_uuid).catch(() => null)
|
||||
const hub = await nodesCol.document(args.hubUuid).catch(() => null)
|
||||
if (!hub || hub.latitude == null || hub.longitude == null) return []
|
||||
|
||||
const cursor = await db.query(`
|
||||
@@ -682,11 +702,11 @@ export const resolvers = {
|
||||
COLLECT product_uuid = node.product_uuid INTO offers
|
||||
LET first_offer = FIRST(offers).node
|
||||
RETURN { uuid: product_uuid, name: first_offer.product_name }
|
||||
`, { lat: hub.latitude, lon: hub.longitude, radius_km: args.radius_km ?? 500 })
|
||||
`, { lat: hub.latitude, lon: hub.longitude, radius_km: args.radiusKm ?? 500 })
|
||||
return cursor.all()
|
||||
},
|
||||
|
||||
suppliers_for_product: async (_: unknown, args: { product_uuid: string }) => {
|
||||
suppliersForProduct: async (_: unknown, args: { productUuid: string }) => {
|
||||
const db = getDb()
|
||||
const cursor = await db.query(`
|
||||
FOR node IN nodes
|
||||
@@ -695,11 +715,11 @@ export const resolvers = {
|
||||
FILTER node.supplier_uuid != null
|
||||
COLLECT supplier_uuid = node.supplier_uuid
|
||||
RETURN { uuid: supplier_uuid }
|
||||
`, { product_uuid: args.product_uuid })
|
||||
`, { product_uuid: args.productUuid })
|
||||
return cursor.all()
|
||||
},
|
||||
|
||||
hubs_for_product: async (_: unknown, args: { product_uuid: string; radius_km?: number }) => {
|
||||
hubsForProduct: async (_: unknown, args: { productUuid: string; radiusKm?: number }) => {
|
||||
const db = getDb()
|
||||
const cursor = await db.query(`
|
||||
FOR offer IN nodes
|
||||
@@ -719,17 +739,17 @@ export const resolvers = {
|
||||
uuid: hub_uuid, name: hub_name, latitude: hub_lat, longitude: hub_lon,
|
||||
country: hub_country, country_code: hub_country_code, transport_types: hub_transport
|
||||
}
|
||||
`, { product_uuid: args.product_uuid, radius_km: args.radius_km ?? 500 })
|
||||
`, { product_uuid: args.productUuid, radius_km: args.radiusKm ?? 500 })
|
||||
const hubs = await cursor.all()
|
||||
return hubs.map((h: ArangoDoc) => ({ ...mapNode(h), uuid: h.uuid }))
|
||||
},
|
||||
|
||||
offers_by_hub: async (_: unknown, args: { hub_uuid: string; product_uuid: string; limit?: number }) => {
|
||||
offersByHub: async (_: unknown, args: { hubUuid: string; productUuid: string; limit?: number }) => {
|
||||
const db = getDb()
|
||||
await ensureGraph()
|
||||
const nodesCol = db.collection('nodes')
|
||||
|
||||
const hub = await nodesCol.document(args.hub_uuid).catch(() => null)
|
||||
const hub = await nodesCol.document(args.hubUuid).catch(() => null)
|
||||
if (!hub || hub.latitude == null || hub.longitude == null) return []
|
||||
|
||||
const hubLat = hub.latitude as number
|
||||
@@ -737,11 +757,11 @@ export const resolvers = {
|
||||
const limit = args.limit ?? 10
|
||||
|
||||
// Priority queue: [cost, seq, nodeKey, phase]
|
||||
const queue: [number, number, string, Phase][] = [[0, 0, args.hub_uuid, 'end_auto']]
|
||||
const queue: [number, number, string, Phase][] = [[0, 0, args.hubUuid, 'end_auto']]
|
||||
let counter = 0
|
||||
const visited = new Map<string, number>()
|
||||
const predecessors = new Map<string, [string, ArangoDoc]>()
|
||||
const nodeDocs = new Map<string, ArangoDoc>([[args.hub_uuid, hub]])
|
||||
const nodeDocs = new Map<string, ArangoDoc>([[args.hubUuid, hub]])
|
||||
const foundRoutes: ArangoDoc[] = []
|
||||
let expansions = 0
|
||||
|
||||
@@ -753,7 +773,7 @@ export const resolvers = {
|
||||
if (visited.has(stateKey) && cost > visited.get(stateKey)!) continue
|
||||
|
||||
const nodeDoc = nodeDocs.get(nodeKey)
|
||||
if (nodeDoc && nodeDoc.product_uuid === args.product_uuid) {
|
||||
if (nodeDoc && nodeDoc.product_uuid === args.productUuid) {
|
||||
const pathEdges: [string, string, ArangoDoc][] = []
|
||||
let curState = stateKey
|
||||
let curKey = nodeKey
|
||||
@@ -772,11 +792,11 @@ export const resolvers = {
|
||||
if (srcLat != null && srcLon != null) dm = distanceKm(srcLat, srcLon, hubLat, hubLon)
|
||||
|
||||
foundRoutes.push({
|
||||
source_uuid: nodeKey,
|
||||
source_name: nodeDoc.name || nodeDoc.product_name,
|
||||
source_lat: srcLat,
|
||||
source_lon: srcLon,
|
||||
distance_km: dm,
|
||||
sourceUuid: nodeKey,
|
||||
sourceName: nodeDoc.name || nodeDoc.product_name,
|
||||
sourceLat: srcLat,
|
||||
sourceLon: srcLon,
|
||||
distanceKm: dm,
|
||||
routes: route ? [route] : [],
|
||||
})
|
||||
continue
|
||||
@@ -810,11 +830,11 @@ export const resolvers = {
|
||||
return foundRoutes
|
||||
},
|
||||
|
||||
offer_to_hub: async (_: unknown, args: { offer_uuid: string; hub_uuid: string }) => {
|
||||
return resolveOfferToHubInternal(args.offer_uuid, args.hub_uuid)
|
||||
offerToHub: async (_: unknown, args: { offerUuid: string; hubUuid: string }) => {
|
||||
return resolveOfferToHubInternal(args.offerUuid, args.hubUuid)
|
||||
},
|
||||
|
||||
nearest_hubs: async (_: unknown, args: { lat: number; lon: number; radius?: number; product_uuid?: string; limit?: number }) => {
|
||||
nearestHubs: async (_: unknown, args: { lat: number; lon: number; radius?: number; productUuid?: string; limit?: number }) => {
|
||||
const db = getDb()
|
||||
const radius = args.radius ?? 1000
|
||||
const limit = args.limit ?? 12
|
||||
@@ -822,7 +842,7 @@ export const resolvers = {
|
||||
let aql: string
|
||||
let bindVars: Record<string, unknown>
|
||||
|
||||
if (args.product_uuid) {
|
||||
if (args.productUuid) {
|
||||
aql = `
|
||||
FOR offer IN nodes
|
||||
FILTER offer.node_type == 'offer'
|
||||
@@ -843,7 +863,7 @@ export const resolvers = {
|
||||
LIMIT @limit
|
||||
RETURN MERGE(first_hub, {_key: hub_uuid, distance_km: hub_dist})
|
||||
`
|
||||
bindVars = { lat: args.lat, lon: args.lon, radius, product_uuid: args.product_uuid, limit }
|
||||
bindVars = { lat: args.lat, lon: args.lon, radius, product_uuid: args.productUuid, limit }
|
||||
} else {
|
||||
aql = `
|
||||
FOR hub IN nodes
|
||||
@@ -864,7 +884,7 @@ export const resolvers = {
|
||||
return hubs.map((n: ArangoDoc) => mapNode(n))
|
||||
},
|
||||
|
||||
nearest_offers: async (_: unknown, args: { lat: number; lon: number; radius?: number; product_uuid?: string; hub_uuid?: string; limit?: number }) => {
|
||||
nearestOffers: async (_: unknown, args: { lat: number; lon: number; radius?: number; productUuid?: string; hubUuid?: string; limit?: number }) => {
|
||||
const db = getDb()
|
||||
await ensureGraph()
|
||||
const radius = args.radius ?? 500
|
||||
@@ -876,7 +896,7 @@ export const resolvers = {
|
||||
FILTER offer.product_uuid != null
|
||||
FILTER offer.latitude != null AND offer.longitude != null
|
||||
`
|
||||
if (args.product_uuid) aql += ` FILTER offer.product_uuid == @product_uuid\n`
|
||||
if (args.productUuid) aql += ` FILTER offer.product_uuid == @product_uuid\n`
|
||||
aql += `
|
||||
LET dist = DISTANCE(offer.latitude, offer.longitude, @lat, @lon) / 1000
|
||||
FILTER dist <= @radius
|
||||
@@ -886,7 +906,7 @@ export const resolvers = {
|
||||
`
|
||||
|
||||
const bindVars: Record<string, unknown> = { lat: args.lat, lon: args.lon, radius, limit }
|
||||
if (args.product_uuid) bindVars.product_uuid = args.product_uuid
|
||||
if (args.productUuid) bindVars.product_uuid = args.productUuid
|
||||
|
||||
const cursor = await db.query(aql, bindVars)
|
||||
const offerNodes = await cursor.all()
|
||||
@@ -894,8 +914,8 @@ export const resolvers = {
|
||||
const offers = []
|
||||
for (const node of offerNodes) {
|
||||
let routes: RoutePath[] = []
|
||||
if (args.hub_uuid) {
|
||||
const routeResult = await resolveOfferToHubInternal(node._key, args.hub_uuid)
|
||||
if (args.hubUuid) {
|
||||
const routeResult = await resolveOfferToHubInternal(node._key, args.hubUuid)
|
||||
if (routeResult?.routes) routes = routeResult.routes as RoutePath[]
|
||||
}
|
||||
offers.push({ ...mapOffer(node), routes })
|
||||
@@ -903,7 +923,7 @@ export const resolvers = {
|
||||
return offers
|
||||
},
|
||||
|
||||
nearest_suppliers: async (_: unknown, args: { lat: number; lon: number; radius?: number; product_uuid?: string; limit?: number }) => {
|
||||
nearestSuppliers: async (_: unknown, args: { lat: number; lon: number; radius?: number; productUuid?: string; limit?: number }) => {
|
||||
const db = getDb()
|
||||
const radius = args.radius ?? 1000
|
||||
const limit = args.limit ?? 12
|
||||
@@ -914,7 +934,7 @@ export const resolvers = {
|
||||
FILTER offer.supplier_uuid != null
|
||||
FILTER offer.latitude != null AND offer.longitude != null
|
||||
`
|
||||
if (args.product_uuid) aql += ` FILTER offer.product_uuid == @product_uuid\n`
|
||||
if (args.productUuid) aql += ` FILTER offer.product_uuid == @product_uuid\n`
|
||||
aql += `
|
||||
LET dist = DISTANCE(offer.latitude, offer.longitude, @lat, @lon) / 1000
|
||||
FILTER dist <= @radius
|
||||
@@ -934,18 +954,18 @@ export const resolvers = {
|
||||
name: supplier_node != null ? supplier_node.name : first_offer.supplier_name,
|
||||
latitude: supplier_node != null ? supplier_node.latitude : first_offer.latitude,
|
||||
longitude: supplier_node != null ? supplier_node.longitude : first_offer.longitude,
|
||||
distance_km: supplier_dist
|
||||
distanceKm: supplier_dist
|
||||
}
|
||||
`
|
||||
|
||||
const bindVars: Record<string, unknown> = { lat: args.lat, lon: args.lon, radius, limit }
|
||||
if (args.product_uuid) bindVars.product_uuid = args.product_uuid
|
||||
if (args.productUuid) bindVars.product_uuid = args.productUuid
|
||||
|
||||
const cursor = await db.query(aql, bindVars)
|
||||
return cursor.all()
|
||||
},
|
||||
|
||||
route_to_coordinate: async (_: unknown, args: { offer_uuid: string; lat: number; lon: number }) => {
|
||||
routeToCoordinate: async (_: unknown, args: { offerUuid: string; lat: number; lon: number }) => {
|
||||
const db = getDb()
|
||||
const cursor = await db.query(`
|
||||
FOR hub IN nodes
|
||||
@@ -960,10 +980,10 @@ export const resolvers = {
|
||||
const hubs = await cursor.all()
|
||||
if (!hubs.length) return null
|
||||
|
||||
return resolveOfferToHubInternal(args.offer_uuid, hubs[0]._key)
|
||||
return resolveOfferToHubInternal(args.offerUuid, hubs[0]._key)
|
||||
},
|
||||
|
||||
hubs_list: async (_: unknown, args: { limit?: number; offset?: number; country?: string; transport_type?: string; west?: number; south?: number; east?: number; north?: number }) => {
|
||||
hubsList: async (_: unknown, args: { limit?: number; offset?: number; country?: string; transportType?: string; west?: number; south?: number; east?: number; north?: number }) => {
|
||||
const db = getDb()
|
||||
const bounds = boundsFilter(args)
|
||||
|
||||
@@ -978,12 +998,12 @@ export const resolvers = {
|
||||
SORT node.name ASC
|
||||
LIMIT @offset, @limit
|
||||
RETURN node
|
||||
`, { transport_type: args.transport_type ?? null, country: args.country ?? null, offset: args.offset ?? 0, limit: args.limit ?? 50, ...bounds.vars })
|
||||
`, { transport_type: args.transportType ?? null, country: args.country ?? null, offset: args.offset ?? 0, limit: args.limit ?? 50, ...bounds.vars })
|
||||
const nodes = await cursor.all()
|
||||
return nodes.map((n: ArangoDoc) => mapNode(n))
|
||||
},
|
||||
|
||||
suppliers_list: async (_: unknown, args: { limit?: number; offset?: number; country?: string; west?: number; south?: number; east?: number; north?: number }) => {
|
||||
suppliersList: async (_: unknown, args: { limit?: number; offset?: number; country?: string; west?: number; south?: number; east?: number; north?: number }) => {
|
||||
const db = getDb()
|
||||
const bounds = boundsFilter(args)
|
||||
|
||||
@@ -1002,11 +1022,11 @@ export const resolvers = {
|
||||
name: n.name ?? null,
|
||||
latitude: n.latitude ?? null,
|
||||
longitude: n.longitude ?? null,
|
||||
distance_km: null,
|
||||
distanceKm: null,
|
||||
}))
|
||||
},
|
||||
|
||||
products_list: async (_: unknown, args: { limit?: number; offset?: number; west?: number; south?: number; east?: number; north?: number }) => {
|
||||
productsList: async (_: unknown, args: { limit?: number; offset?: number; west?: number; south?: number; east?: number; north?: number }) => {
|
||||
const db = getDb()
|
||||
const bounds = boundsFilter(args)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user