Migrate orders backend from Django to Express + Apollo Server
All checks were successful
Build Docker Image / build (push) Successful in 1m37s
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:
11
src/schemas/public.ts
Normal file
11
src/schemas/public.ts
Normal 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
299
src/schemas/team.ts
Normal 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
11
src/schemas/user.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export const userTypeDefs = `#graphql
|
||||
type Query {
|
||||
health: String!
|
||||
}
|
||||
`
|
||||
|
||||
export const userResolvers = {
|
||||
Query: {
|
||||
health: () => 'ok',
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user