Migrate orders backend from Django to Express + Apollo Server
All checks were successful
Build Docker Image / build (push) Successful in 1m37s

Replace Python/Django/Graphene stack with TypeScript/Express/Apollo Server.
Same 3 GraphQL endpoints (public/user/team), same JWT auth via Logto JWKS,
same Odoo proxy logic. No database needed (pure proxy).
This commit is contained in:
Ruslan Bakiev
2026-03-09 09:08:57 +07:00
parent 03dd05129e
commit c0321660b9
37 changed files with 3917 additions and 2278 deletions

11
src/schemas/public.ts Normal file
View File

@@ -0,0 +1,11 @@
export const publicTypeDefs = `#graphql
type Query {
health: String!
}
`
export const publicResolvers = {
Query: {
health: () => 'ok',
},
}

299
src/schemas/team.ts Normal file
View File

@@ -0,0 +1,299 @@
import { GraphQLError } from 'graphql'
import { requireScopes, type AuthContext } from '../auth.js'
const ODOO_INTERNAL_URL = process.env.ODOO_INTERNAL_URL || 'odoo:8069'
export const teamTypeDefs = `#graphql
type Company {
uuid: String
name: String
taxId: String
country: String
countryCode: String
active: Boolean
}
type Trip {
uuid: String
name: String
sequence: Int
company: Company
plannedLoadingDate: String
actualLoadingDate: String
realLoadingDate: String
plannedUnloadingDate: String
actualUnloadingDate: String
plannedWeight: Float
weightAtLoading: Float
weightAtUnloading: Float
}
type Stage {
uuid: String
name: String
sequence: Int
stageType: String
transportType: String
sourceLocationName: String
sourceLatitude: Float
sourceLongitude: Float
destinationLocationName: String
destinationLatitude: Float
destinationLongitude: Float
locationName: String
locationLatitude: Float
locationLongitude: Float
selectedCompany: Company
trips: [Trip]
}
type OrderLine {
uuid: String
productUuid: String
productName: String
quantity: Float
unit: String
priceUnit: Float
subtotal: Float
currency: String
notes: String
}
type Order {
uuid: String
name: String
teamUuid: String
userId: String
status: String
totalAmount: Float
currency: String
sourceLocationUuid: String
sourceLocationName: String
sourceLatitude: Float
sourceLongitude: Float
destinationLocationUuid: String
destinationLocationName: String
createdAt: String
updatedAt: String
notes: String
orderLines: [OrderLine]
stages: [Stage]
}
type Query {
getTeamOrders: [Order]
getOrder(orderUuid: String!): Order
}
`
interface OdooCompany {
uuid?: string
name?: string
tax_id?: string
country?: string
country_code?: string
active?: boolean
}
interface OdooTrip {
uuid?: string
name?: string
sequence?: number
company?: OdooCompany
planned_loading_date?: string
actual_loading_date?: string
real_loading_date?: string
planned_unloading_date?: string
actual_unloading_date?: string
planned_weight?: number
weight_at_loading?: number
weight_at_unloading?: number
}
interface OdooStage {
uuid?: string
name?: string
sequence?: number
stage_type?: string
transport_type?: string
source_location_name?: string
source_latitude?: number
source_longitude?: number
destination_location_name?: string
destination_latitude?: number
destination_longitude?: number
location_name?: string
location_latitude?: number
location_longitude?: number
selected_company?: OdooCompany
trips?: OdooTrip[]
}
interface OdooOrderLine {
uuid?: string
product_uuid?: string
product_name?: string
quantity?: number
unit?: string
price_unit?: number
subtotal?: number
currency?: string
notes?: string
}
interface OdooOrder {
uuid?: string
name?: string
team_uuid?: string
user_id?: string
status?: string
total_amount?: number
currency?: string
source_location_uuid?: string
source_location_name?: string
source_latitude?: number
source_longitude?: number
destination_location_uuid?: string
destination_location_name?: string
created_at?: string
updated_at?: string
notes?: string
order_lines?: OdooOrderLine[]
stages?: OdooStage[]
}
function mapCompany(c?: OdooCompany) {
if (!c) return null
return {
uuid: c.uuid ?? '',
name: c.name ?? '',
taxId: c.tax_id ?? '',
country: c.country ?? '',
countryCode: c.country_code ?? '',
active: c.active ?? true,
}
}
function mapTrip(t: OdooTrip) {
return {
uuid: t.uuid,
name: t.name,
sequence: t.sequence,
company: mapCompany(t.company),
plannedLoadingDate: t.planned_loading_date,
actualLoadingDate: t.actual_loading_date,
realLoadingDate: t.real_loading_date,
plannedUnloadingDate: t.planned_unloading_date,
actualUnloadingDate: t.actual_unloading_date,
plannedWeight: t.planned_weight,
weightAtLoading: t.weight_at_loading,
weightAtUnloading: t.weight_at_unloading,
}
}
function mapStage(s: OdooStage) {
return {
uuid: s.uuid,
name: s.name,
sequence: s.sequence,
stageType: s.stage_type,
transportType: s.transport_type,
sourceLocationName: s.source_location_name,
sourceLatitude: s.source_latitude,
sourceLongitude: s.source_longitude,
destinationLocationName: s.destination_location_name,
destinationLatitude: s.destination_latitude,
destinationLongitude: s.destination_longitude,
locationName: s.location_name,
locationLatitude: s.location_latitude,
locationLongitude: s.location_longitude,
selectedCompany: mapCompany(s.selected_company),
trips: (s.trips ?? []).map(mapTrip),
}
}
function mapOrder(o: OdooOrder) {
const stages = o.stages ?? []
const firstStage = stages[0]
return {
uuid: o.uuid,
name: o.name,
teamUuid: o.team_uuid,
userId: o.user_id,
status: 'active',
totalAmount: o.total_amount,
currency: o.currency,
sourceLocationUuid: o.source_location_uuid,
sourceLocationName: o.source_location_name,
sourceLatitude: o.source_latitude || firstStage?.source_latitude,
sourceLongitude: o.source_longitude || firstStage?.source_longitude,
destinationLocationUuid: o.destination_location_uuid,
destinationLocationName: o.destination_location_name,
createdAt: o.created_at,
updatedAt: o.updated_at,
notes: o.notes ?? '',
orderLines: (o.order_lines ?? []).map(l => ({
uuid: l.uuid,
productUuid: l.product_uuid,
productName: l.product_name,
quantity: l.quantity,
unit: l.unit,
priceUnit: l.price_unit,
subtotal: l.subtotal,
currency: l.currency,
notes: l.notes ?? '',
})),
stages: stages.map(mapStage),
}
}
async function fetchOdoo(path: string): Promise<Response> {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 10000)
try {
return await fetch(`http://${ODOO_INTERNAL_URL}${path}`, { signal: controller.signal })
} finally {
clearTimeout(timeout)
}
}
export const teamResolvers = {
Query: {
getTeamOrders: async (_: unknown, __: unknown, ctx: AuthContext) => {
requireScopes(ctx, 'teams:member')
if (!ctx.userId) throw new GraphQLError('User not authenticated')
if (!ctx.teamUuid) return []
try {
const res = await fetchOdoo(`/fastapi/orders/orders/team/${ctx.teamUuid}`)
if (!res.ok) return []
const data = (await res.json()) as OdooOrder[]
return data.map(mapOrder)
} catch (e) {
console.error('Error calling Odoo:', e)
return []
}
},
getOrder: async (_: unknown, args: { orderUuid: string }, ctx: AuthContext) => {
requireScopes(ctx, 'teams:member')
if (!ctx.userId || !ctx.teamUuid) {
throw new GraphQLError('User not authenticated')
}
try {
const res = await fetchOdoo(`/fastapi/orders/orders/${args.orderUuid}`)
if (!res.ok) return null
const data = (await res.json()) as OdooOrder
if (data.team_uuid !== ctx.teamUuid) {
throw new GraphQLError('Access denied: order belongs to different team')
}
return mapOrder(data)
} catch (e) {
if (e instanceof GraphQLError) throw e
console.error('Error calling Odoo:', e)
return null
}
},
},
}

11
src/schemas/user.ts Normal file
View File

@@ -0,0 +1,11 @@
export const userTypeDefs = `#graphql
type Query {
health: String!
}
`
export const userResolvers = {
Query: {
health: () => 'ok',
},
}