REST endpoints
Everything else is GraphQL at POST /graphql/. Operations other than login need Authorization: Bearer <jwt>.
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /health/ | none | Liveness probe: {status, database} |
POST | /api/v1/auth/token/ | none | {username, password} to {token, token_type, expires_in} |
POST | /api/v1/visits/{id}/photos/ | Bearer | Multipart image (+ caption); jpg, jpeg, png, webp; 5 MB default |
GET | /api/v1/visits/{id}/photos/ | Bearer | Photos of a visit with absolute URLs |
GET | /api/v1/reports/weekly.csv ยท .xlsx | Bearer | Weekly report as a file; week_start, group_by, upazila, union_name |
POST | /graphql/ | Bearer | GraphQL endpoint; GraphiQL IDE on GET when GRAPHIQL=true |
GET | /admin/ | staff session | Django admin |
GraphQL schema (SDL)
type AlertPage {
items: [AlertType!]!
total: Int!
offset: Int!
limit: Int!
}
type AlertType {
id: ID!
field: FieldType!
kind: String!
severity: String!
message: String!
value: Decimal
triggeredAt: DateTime!
resolvedAt: DateTime
resolvedBy: UserType
resolutionNote: String!
isOpen: Boolean!
}
type AreaType {
m2: Float!
hectare: Float!
acre: Float!
bigha: Float!
decimal: Float!
}
type AuthPayload {
token: String!
tokenType: String!
expiresInSeconds: Int!
user: UserType!
}
type AwdCheckResultType {
fieldsChecked: Int!
opened: Int!
resolved: Int!
unchanged: Int!
}
"""Date (isoformat)"""
scalar Date
"""Date with time (isoformat)"""
scalar DateTime
"""Decimal (fixed-point)"""
scalar Decimal
input FarmerInput {
name: String!
phone: String!
village: String!
unionName: String!
upazila: String!
district: String!
nid: String = null
}
type FarmerPage {
items: [FarmerType!]!
total: Int!
offset: Int!
limit: Int!
}
type FarmerType {
id: ID!
name: String!
phone: String!
nid: String!
village: String!
unionName: String!
upazila: String!
district: String!
createdAt: DateTime!
fields: [FieldType!]!
totalAreaM2: Decimal!
}
input FieldInput {
farmerId: ID!
name: String!
"""List of [lat, lng] pairs"""
polygon: JSON!
crop: String! = "boro"
awdThresholdCm: Int = null
}
type FieldPage {
items: [FieldType!]!
total: Int!
offset: Int!
limit: Int!
}
type FieldType {
id: ID!
farmer: FarmerType!
name: String!
crop: String!
polygon: JSON!
areaM2: Decimal!
centroidLat: Float!
centroidLng: Float!
unionName: String!
upazila: String!
district: String!
awdThresholdCm: Int!
isActive: Boolean!
createdAt: DateTime!
pipes: [PipeType!]!
seasons: [SeasonType!]!
area: AreaType!
perimeterM: Float!
activeSeason: SeasonType
latestReading: ReadingType
openAlerts: [AlertType!]!
lastIrrigation: IrrigationEventType
readings(limit: Int! = 50): [ReadingType!]!
}
type IrrigationEventType {
id: ID!
field: FieldType!
startedAt: DateTime!
endedAt: DateTime
method: String!
waterMm: Decimal
note: String!
loggedBy: UserType
durationMinutes: Int
}
input IrrigationInput {
fieldId: ID!
startedAt: DateTime = null
endedAt: DateTime = null
method: String! = "pump"
waterMm: Decimal = null
note: String! = ""
}
"""
The `JSON` scalar type represents JSON values as specified by [ECMA-404](https://ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf).
"""
scalar JSON @specifiedBy(url: "https://ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf")
type LocationType {
district: String!
upazila: String!
unionName: String!
fields: Int!
}
type Mutation {
"""Exchange username/password for a bearer JWT"""
login(username: String!, password: String!): AuthPayload!
createFarmer(input: FarmerInput!): FarmerType!
createField(input: FieldInput!): FieldType!
addReading(input: ReadingInput!): ReadingType!
logIrrigation(input: IrrigationInput!): IrrigationEventType!
createVisit(input: VisitInput!): VisitType!
resolveAlert(id: ID!, note: String! = ""): AlertType!
"""
Evaluate AWD rules for every active field now (also run by the scheduler)
"""
runAwdCheck: AwdCheckResultType!
}
type PipeType {
id: ID!
field: FieldType!
code: String!
depthCm: Int!
installedOn: Date!
deviceId: String!
isActive: Boolean!
hasDevice: Boolean!
latestReading: ReadingType
}
type Query {
me: UserType!
farmers(search: String = null, district: String = null, upazila: String = null, unionName: String = null, offset: Int! = 0, limit: Int! = 20): FarmerPage!
farmer(id: ID!): FarmerType
fields(farmerId: ID = null, district: String = null, upazila: String = null, unionName: String = null, crop: String = null, withOpenAlerts: Boolean = null, includeInactive: Boolean! = false, offset: Int! = 0, limit: Int! = 20): FieldPage!
field(id: ID!): FieldType
readings(fieldId: ID = null, pipeId: ID = null, since: DateTime = null, until: DateTime = null, readingSource: String = null, offset: Int! = 0, limit: Int! = 50): ReadingPage!
alerts(fieldId: ID = null, kind: String = null, severity: String = null, upazila: String = null, openOnly: Boolean! = true, offset: Int! = 0, limit: Int! = 50): AlertPage!
alert(id: ID!): AlertType
locations: [LocationType!]!
"""
Weekly AWD progress. group_by: field | union | upazila. Defaults to last full week.
"""
weeklyReport(weekStart: Date = null, groupBy: String! = "field", upazila: String = null, unionName: String = null): [ReportRowType!]!
}
input ReadingInput {
levelCm: Decimal!
pipeId: ID = null
pipeCode: String = null
recordedAt: DateTime = null
source: String! = "manual"
note: String! = ""
}
type ReadingPage {
items: [ReadingType!]!
total: Int!
offset: Int!
limit: Int!
}
type ReadingType {
id: ID!
pipe: PipeType!
levelCm: Decimal!
source: String!
recordedAt: DateTime!
recordedBy: UserType
note: String!
"""True when the level is at or below the field's AWD threshold"""
belowThreshold: Boolean!
}
type ReportRowType {
groupBy: String!
key: String!
label: String!
weekStart: Date!
weekEnd: Date!
fields: Int!
areaHa: Float!
readings: Int!
avgLevelCm: Float
minLevelCm: Float
fieldDaysBelowThreshold: Int!
irrigationEvents: Int!
irrigationPerField: Float!
estimatedWaterSavingPct: Float!
visits: Int!
alertsOpened: Int!
alertsOpenNow: Int!
fieldIds: [Int!]!
}
type SeasonType {
id: ID!
crop: String!
variety: String!
transplantedOn: Date!
awdStartOn: Date!
awdEndOn: Date
expectedHarvestOn: Date
isActive: Boolean!
}
type UserType {
id: ID!
username: String!
firstName: String!
lastName: String!
isStaff: Boolean!
}
input VisitInput {
fieldId: ID!
visitedAt: DateTime = null
cropStage: String! = ""
notes: String! = ""
lat: Float = null
lng: Float = null
}
type VisitPhotoType {
id: ID!
caption: String!
uploadedAt: DateTime!
url: String!
}
type VisitType {
id: ID!
field: FieldType!
visitor: UserType
visitedAt: DateTime!
cropStage: String!
notes: String!
lat: Float
lng: Float
photos: [VisitPhotoType!]!
}