Extending Generated Schema #
Generated query schemas provide standard fields, filtering, and pagination. You can extend them with additional fields, custom input types, or computed values without modifying generated files.
Adding Fields to Generated Types #
Create a companion file that adds fields to an existing generated type:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23import { builder } from "@lakeql/api/builder"
// Reference the generated type by name
const OrdersType = builder.objectRef<{ id: number; total: number }>("Orders")
// Add a computed field
builder.objectField(OrdersType, "formattedTotal", (t) =>
t.string({
resolve: (parent) => `$${parent.total.toFixed(2)}`,
})
)
// Add a field that fetches from another source
builder.objectField(OrdersType, "customerName", (t) =>
t.string({
nullable: true,
resolve: async (parent) => {
// Fetch customer name from a secondary source
return null
},
})
)
Extension files must be placed in the schema path directory (e.g.
src/schemas/custom/). Never place custom code inside generated directories —
it will be overwritten on the next pull.Custom Comparison Types #
Generated schemas include standard comparison operators (eq, neq, gt, lt, like, etc.). You can define custom input types for more specific filtering:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { builder } from "@lakeql/api/builder"
export const DateRangeInput = builder.inputType("DateRangeInput", {
fields: (t) => ({
from: t.string({ required: true }),
to: t.string({ required: true }),
}),
})
export const AmountRangeInput = builder.inputType("AmountRangeInput", {
fields: (t) => ({
min: t.float({ required: true }),
max: t.float({ required: true }),
}),
})
Use these inputs in custom query fields:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { builder } from "@lakeql/api/builder"
import { DateRangeInput, AmountRangeInput } from "../custom/date-range-input"
builder.queryField("ordersByRange", (t) =>
t.field({
type: ["Orders"],
args: {
dateRange: t.arg({ type: DateRangeInput, required: true }),
amountRange: t.arg({ type: AmountRangeInput }),
},
resolve: async (_root, args, _context) => {
// Build custom query with range filters
return []
},
})
)
Computed Fields #
Add fields that derive values from existing data at the GraphQL layer:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42import { builder } from "@lakeql/api/builder"
const OrdersType = builder.objectRef<{
total: number
tax_rate: number
status: string
created_at: Date
}>("Orders")
// Tax amount computed from total and tax_rate
builder.objectField(OrdersType, "taxAmount", (t) =>
t.float({
resolve: (parent) => parent.total * parent.tax_rate,
})
)
// Human-readable status
builder.objectField(OrdersType, "statusLabel", (t) =>
t.string({
resolve: (parent) => {
const labels: Record<string, string> = {
pending: "Pending Review",
shipped: "Shipped",
delivered: "Delivered",
cancelled: "Cancelled",
}
return labels[parent.status] ?? parent.status
},
})
)
// Age in days
builder.objectField(OrdersType, "ageInDays", (t) =>
t.int({
resolve: (parent) => {
const now = Date.now()
const created = new Date(parent.created_at).getTime()
return Math.floor((now - created) / (1000 * 60 * 60 * 24))
},
})
)
Custom Enum Types #
Define enum types for fields that have a fixed set of values:
1
2
3
4
5
6
7
8
9
10
11
12
import { builder } from "@lakeql/api/builder"
export const OrderStatus = builder.enumType("OrderStatus", {
values: [
"pending",
"processing",
"shipped",
"delivered",
"cancelled",
] as const,
})
Tips for Extension Files #
-
Don't modify generated files
— They'll be overwritten on the next
pull -
Use descriptive file names
— e.g.
orders-extensions.ts,orders-computed.ts -
Place in
schemas/custom/— Keep custom code separate from generated directories -
Test independently
— Extensions can introduce runtime errors if the parent type changes after a
pull